// Package smarthome talks to a Home Assistant instance so Maven can read what
// the house is doing and change it (Vikunja #256,
// docs/plans/11-smarthome-integration.md).
//
// The shape of this package is copied deliberately from internal/mcp: a
// controllable entity becomes a PROPOSED row in the existing act allowlist,
// encoded in the columns that already exist — cmd
// ["smarthome", "<entity_id>", "<service>"], scope "smarthome:<domain>". So
// ProposeTool/EnableTool/DisableTool, tool.Matcher and the confirm turn need no
// change, and turning a light off in his flat goes through exactly the same
// gate as `restart nginx`.
//
// Two rules that are not negotiable here:
//
//   - Discovery only ever PROPOSES. Finding a switch on the network is not the
//     same as being allowed to flip it; Kami enables it on /tools, behind
//     step-up.
//   - Every control row is destructive=true. There is no read-only way to turn
//     the heating off. That means a spoken act always gets the confirm turn,
//     which is the point.
//
// MQTT / Zigbee2MQTT (steps 2 and 5 of the plan) are NOT here: they need a
// broker client dependency and the module cache in this repo is vendored, and
// there is no broker on this network to test one against. Home Assistant's REST
// API is stdlib-only and already fronts Zigbee2MQTT when it is present.
package smarthome

import (
	"errors"
	"strings"
)

var (
	// ErrNotConfigured — no smarthome block, or it is disabled.
	ErrNotConfigured = errors.New("smarthome: not configured")
	// ErrUnknownEntity — the entity vanished between discovery and the call.
	ErrUnknownEntity = errors.New("smarthome: unknown entity")
	// ErrNotControllable — the entity's domain has no service Maven will call.
	ErrNotControllable = errors.New("smarthome: entity is not controllable")
)

// cmdPrefix marks an allowlist row as a Home Assistant service call rather than
// a process. It is never run as a binary — tool.Executor branches on it before
// it ever reaches exec.
const cmdPrefix = "smarthome"

// Entity is one thing in the house, as Home Assistant sees it.
type Entity struct {
	// ID — the Home Assistant entity_id, "light.living_room".
	ID string
	// Domain — the part before the dot. Decides which services apply.
	Domain string
	// Name — friendly_name when the instance has one, else ID.
	Name string
	// State — "on", "off", "22.5", …
	State string
	// Unit — unit_of_measurement, for sensors.
	Unit string
}

// Service is one thing Maven can do to an entity.
type Service struct {
	// Name — the Home Assistant service, "turn_on".
	Name string
	// Verb — the local suffix used to build the allowlist row name.
	Verb string
}

// controllable maps a domain to the services Maven will expose for it. A domain
// that is not in this table gets no control row at all — the list is an
// allowlist, not a default, so a new HA integration cannot quietly hand her a
// verb nobody reviewed. set_temperature and set_brightness take a value and are
// deliberately absent: a spoken number that the router got wrong is a wrong act
// on real hardware, and on/off is the whole of what a voice turn can defend.
var controllable = map[string][]Service{
	"light":  {{Name: "turn_on", Verb: "on"}, {Name: "turn_off", Verb: "off"}},
	"switch": {{Name: "turn_on", Verb: "on"}, {Name: "turn_off", Verb: "off"}},
	"fan":    {{Name: "turn_on", Verb: "on"}, {Name: "turn_off", Verb: "off"}},
	"cover":  {{Name: "open_cover", Verb: "open"}, {Name: "close_cover", Verb: "close"}},
	"lock":   {{Name: "lock", Verb: "lock"}, {Name: "unlock", Verb: "unlock"}},
}

// Services returns the services exposed for an entity, nil when its domain is
// not controllable (a sensor, a person, a weather entity: readable, not
// flippable).
func Services(domain string) []Service { return controllable[domain] }

// DomainOf splits "light.living_room" into "light". Empty when the id has no
// dot, which Home Assistant guarantees it does.
func DomainOf(entityID string) string {
	i := strings.IndexByte(entityID, '.')
	if i <= 0 {
		return ""
	}
	return entityID[:i]
}

// LocalName is the allowlist row name for one entity+service. Prefixed so a
// house row is recognisable on /tools without opening the config, and so it
// cannot collide with a shell tool Kami named himself.
func LocalName(entityID, verb string) string {
	return "home_" + strings.ReplaceAll(entityID, ".", "_") + "_" + verb
}

// Scope is the store scope for an entity's domain.
func Scope(domain string) string { return cmdPrefix + ":" + domain }

// Cmd is the allowlist cmd column for an entity+service.
func Cmd(entityID, service string) []string { return []string{cmdPrefix, entityID, service} }

// ParseCmd recognises a Home Assistant row. ok=false ⇒ an ordinary process row,
// and the caller execs it as it always did.
func ParseCmd(cmd []string) (entityID, service string, ok bool) {
	if len(cmd) != 3 || cmd[0] != cmdPrefix {
		return "", "", false
	}
	if cmd[1] == "" || cmd[2] == "" {
		return "", "", false
	}
	return cmd[1], cmd[2], true
}
