package config

import (
	"fmt"
	"time"

	"github.com/kami/maven/internal/netscan"
	"github.com/kami/maven/internal/smarthome"
)

// SmartHomeConfig — the Home Assistant block (Vikunja #256). Dark until
// `"enabled": true`, and even then a discovered device is only ever PROPOSED
// into the act allowlist: Kami enables it on /tools, behind step-up, exactly as
// he would a shell tool. Finding a switch on the network is not the same as
// being allowed to flip it.
type SmartHomeConfig struct {
	// Provider — only "homeassistant" is implemented. MQTT / Zigbee2MQTT are
	// not: Home Assistant already fronts them, and a broker client is a
	// dependency this vendored module tree cannot take on tonight.
	Provider string `json:"provider,omitempty"`

	// URL — the instance base, "http://192.168.1.50:8123".
	//
	// Plain http is accepted and is what the deploy block uses. That is a
	// deliberate choice, not an oversight: the instance is on the LAN behind
	// wireguard, and a self-signed cert on a home box buys a warning rather
	// than a guarantee. It does mean the long-lived token crosses the LAN in
	// cleartext on every refresh, so the LAN is part of the trust boundary.
	URL string `json:"url,omitempty"`

	// Token — a long-lived access token. Use ${HA_TOKEN} and keep the value in
	// the gitignored env file, like the telegram credentials.
	Token string `json:"token,omitempty"`

	// Domains — entity domains to take. Empty ⇒ the controllable domains
	// EXCEPT lock (light, switch, fan, cover) plus sensor and binary_sensor
	// for reads. A lock is only enumerated when it is named here, because a
	// front door is not a lamp. Narrow it when the instance is large: a tool name the 1.7B
	// half-remembers is a wrong act.
	Domains []string `json:"domains,omitempty"`

	// MaxEntities — cap on the proposal catalogue. 0 ⇒ 40.
	MaxEntities int `json:"max_entities,omitempty"`

	// Timeout — per-call budget. 0 ⇒ 10s.
	Timeout Duration `json:"timeout,omitempty"`

	// Refresh — how often the entity list is re-read and new devices proposed.
	// 0 ⇒ 15m, and anything under MinSmartHomeRefresh is raised to it:
	// "refresh": "1s" used to pass validation and enumerate the whole instance
	// every second. Discovery is idempotent, so this only ever adds rows.
	Refresh Duration `json:"refresh,omitempty"`

	// Enabled — false (the default) keeps a written block dark, so it can be
	// reviewed before the house is wired to a voice.
	Enabled bool `json:"enabled,omitempty"`
}

// DefaultSmartHomeRefresh — how often the house is re-enumerated for new
// devices. Slow on purpose: discovery only adds proposals, and a flat does not
// grow a new lamp every minute.
const DefaultSmartHomeRefresh = 15 * time.Minute

// MinSmartHomeRefresh — the floor under SmartHomeConfig.Refresh. Enumerating
// every entity in the house is a full /api/states read; a misconfigured second
// would hammer the instance for proposals that are idempotent anyway.
const MinSmartHomeRefresh = time.Minute

// SmartHomeClient maps the config block onto the smarthome package's own type.
// Returns ok=false when nothing is configured or it is disabled, so validation
// and daemon wiring cannot drift on the mapping.
func (c *Config) SmartHomeClient() (smarthome.Config, bool) {
	if c.SmartHome == nil || !c.SmartHome.Enabled {
		return smarthome.Config{}, false
	}
	return smarthome.Config{
		URL:         c.SmartHome.URL,
		Token:       c.SmartHome.Token,
		Domains:     c.SmartHome.Domains,
		MaxEntities: c.SmartHome.MaxEntities,
		Timeout:     time.Duration(c.SmartHome.Timeout),
	}, true
}

// normaliseSmartHome applies the block's defaults. A block that is not enabled
// is the same as no block at all, so "off" stays in one place.
func (c *Config) normaliseSmartHome() {
	if c.SmartHome != nil && !c.SmartHome.Enabled {
		c.SmartHome = nil
	}
	if c.SmartHome != nil && c.SmartHome.Refresh <= 0 {
		c.SmartHome.Refresh = Duration(DefaultSmartHomeRefresh)
	}
	if c.SmartHome != nil && c.SmartHome.Refresh < Duration(MinSmartHomeRefresh) {
		c.SmartHome.Refresh = Duration(MinSmartHomeRefresh)
	}
}

// validateSmartHome fails a missing token or a bare hostname at startup, not at
// the first "выключи свет".
func (c *Config) validateSmartHome() error {
	hc, ok := c.SmartHomeClient()
	if !ok {
		return nil
	}
	if p := c.SmartHome.Provider; p != "" && p != "homeassistant" {
		return fmt.Errorf("smarthome: provider %q: only \"homeassistant\" is implemented", p)
	}
	return smarthome.Validate(hc)
}

// NetScanConfig — the LAN scanner block (Vikunja #257). Dark until
// `"enabled": true`.
//
// The important field is Subnets, and it is the ONLY source of a scan target.
// Nothing an utterance, a router or a scanned host says can widen or move the
// range: internal/netscan.Scanner.Scan takes no target argument at all. Each
// subnet must be private and no larger than netscan.MaxPrefixHosts addresses
// (a /22), enforced at config load rather than at the first spoken scan.
type NetScanConfig struct {
	// Subnets — CIDRs to scan, "192.168.1.0/24".
	Subnets []string `json:"subnets,omitempty"`

	// Ports — TCP ports to try per host. Empty ⇒ netscan.DefaultPorts
	// (22, 80, 443, 8080).
	Ports []int `json:"ports,omitempty"`

	// Timeout — per-connection budget. 0 ⇒ netscan.DefaultTimeout (400ms).
	Timeout Duration `json:"timeout,omitempty"`

	// Rate — connections per second across the whole scan. 0 ⇒
	// netscan.DefaultRate (100). Low on purpose: a scan should look like
	// background traffic, not a portscan.
	Rate int `json:"rate,omitempty"`

	// MaxHosts — cap on addresses probed per scan. 0 ⇒ netscan.DefaultMaxHosts
	// (256).
	MaxHosts int `json:"max_hosts,omitempty"`

	// Enabled — false (the default) keeps a written block dark.
	Enabled bool `json:"enabled,omitempty"`
}

// NetScanner maps the config block onto the netscan package's own type.
// ok=false when absent or disabled, so validation and daemon wiring cannot
// drift on the mapping.
func (c *Config) NetScanner() (netscan.Config, bool) {
	if c.NetScan == nil || !c.NetScan.Enabled {
		return netscan.Config{}, false
	}
	return netscan.Config{
		Subnets:  c.NetScan.Subnets,
		Ports:    c.NetScan.Ports,
		Timeout:  time.Duration(c.NetScan.Timeout),
		Rate:     c.NetScan.Rate,
		MaxHosts: c.NetScan.MaxHosts,
	}, true
}

// normaliseNetScan applies the block's defaults. Same rule as the house: not
// enabled is the same as no block at all.
func (c *Config) normaliseNetScan() {
	if c.NetScan != nil && !c.NetScan.Enabled {
		c.NetScan = nil
	}
}

// validateNetScan fails a scanner pointed at the public internet, or at a /8,
// here rather than after the packets have already left.
func (c *Config) validateNetScan() error {
	nc, ok := c.NetScanner()
	if !ok {
		return nil
	}
	return netscan.Validate(nc)
}
