package main

import (
	"context"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	"github.com/kami/maven/internal/config"
	"github.com/kami/maven/internal/phraser"
	"github.com/kami/maven/internal/smarthome"
	"github.com/kami/maven/internal/store"
)

// homeWiring — the Home Assistant client, when the `smarthome` block is present
// AND enabled. nil ⇒ the house is not wired, nothing was proposed, and an
// allowlist row that happens to look like a house row refuses to run.
//
// It lives on the voice wiring for the same reason MCP does: a house control IS
// an act. It goes through tool.Executor, the enabled allowlist and the confirm
// turn, all of which only exist on the voice/chat path.
type homeWiring struct {
	client  *smarthome.Client
	st      *store.Store
	refresh time.Duration
}

// wireSmartHome builds the client and proposes what it found. It never fails
// the daemon: an instance that is down at boot is logged and retried, because
// Maven starting is not contingent on someone else's process.
func wireSmartHome(cfg *config.Config, st *store.Store) *homeWiring {
	hc, ok := cfg.SmartHomeClient()
	if !ok || st == nil {
		return nil
	}
	if err := smarthome.Validate(hc); err != nil {
		// config.validate already ran this, so reaching here is a programming
		// error rather than a config one. Still not fatal: the house off is a
		// working Maven.
		log.Printf("smarthome: not wired: %v", err)
		return nil
	}
	w := &homeWiring{
		client:  smarthome.NewClient(hc),
		st:      st,
		refresh: time.Duration(cfg.SmartHome.Refresh),
	}
	// No first propose here. This runs inside wireVoice, inside run, before the
	// IPC socket is serving, and on the locked path inside the passkey unlock
	// handler. A Home Assistant box that is powered off but still on a routed
	// subnet black-holes the connection rather than refusing it, so a
	// synchronous enumeration held the daemon's start for the per-call timeout.
	// run does the first propose off the ticker instead.
	return w
}

// caller is the tool.HomeCaller seam.
func (w *homeWiring) caller() *smarthome.Client {
	if w == nil {
		return nil
	}
	return w.client
}

// propose writes a 'proposed' allowlist row for every controllable device. It
// does NOT enable anything: a reachable house is a place Maven may look, not a
// set of switches she may flip. Kami enables what he wants on /tools, behind
// step-up, which is the same gate a shell tool goes through.
//
// Sensors are read but never proposed — there is nothing to call on them.
func (w *homeWiring) propose(ctx context.Context) {
	if w == nil {
		return
	}
	ents, err := w.client.States(ctx)
	if err != nil {
		log.Printf("smarthome: read states: %v", err)
		return
	}
	now := time.Now()
	fresh, devices := 0, 0
	for _, e := range ents {
		svcs := smarthome.Services(e.Domain)
		if len(svcs) == 0 {
			continue
		}
		devices++
		for _, s := range svcs {
			name := smarthome.LocalName(e.ID, s.Verb)
			provenance := "дом: " + s.Name + " → " + e.Name + " (" + e.ID + ")"
			ok, err := w.st.ProposeSmartHomeTool(ctx, name, smarthome.Scope(e.Domain),
				smarthome.Cmd(e.ID, s.Name), provenance, now)
			if err != nil {
				log.Printf("smarthome: propose %s: %v", name, err)
				continue
			}
			if ok {
				fresh++
			}
		}
	}
	log.Printf("smarthome: %d entities, %d controllable", len(ents), devices)
	if fresh > 0 {
		log.Printf("smarthome: %d new device proposal(s) waiting on /tools", fresh)
	}
}

// run re-enumerates the house and picks up devices that appeared, until ctx is
// canceled.
func (w *homeWiring) run(ctx context.Context) {
	if w == nil {
		return
	}
	iv := w.refresh
	if iv <= 0 {
		iv = config.DefaultSmartHomeRefresh
	}
	t := time.NewTicker(iv)
	defer t.Stop()
	// The first enumeration, off the daemon's start path. wireSmartHome used to
	// do it synchronously and a dead house delayed the socket coming up.
	w.propose(ctx)
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			w.propose(ctx)
		}
	}
}

// homeSummary answers "что дома?" — a read of the current entity states, one
// short line. Read-only: it can never call a service, so it needs no confirm
// and no allowlist row.
func (w *homeWiring) homeSummary(ctx context.Context) (string, bool) {
	if w == nil {
		return "", false
	}
	ents, err := w.client.States(ctx)
	if err != nil {
		log.Printf("smarthome: summary: %v", err)
		return phraser.A(phraser.HomeUnreachable, nil), true
	}
	if len(ents) == 0 {
		return phraser.A(phraser.HomeEmpty, nil), true
	}
	var on []string
	var sensors []string
	dark := 0
	for _, e := range ents {
		switch {
		case e.Domain == "sensor" || e.Domain == "binary_sensor":
			if e.State == "" || e.State == "unavailable" {
				dark++
				continue
			}
			if len(sensors) < 3 {
				sensors = append(sensors, e.Name+" "+e.State+e.Unit)
			}
		case e.State == "unavailable" || e.State == "unknown" || e.State == "":
			// A lamp that is not reachable is not a lamp that is off. Counting
			// it as neither used to make "всё выключено" and "one device is
			// unreachable" read identically.
			dark++
		case e.State == "on" || e.State == "open" || e.State == "unlocked":
			on = append(on, e.Name)
		}
	}
	var parts []string
	switch {
	case len(on) > 0:
		shown, rest := on, 0
		if len(shown) > 5 {
			rest = len(shown) - 5
			shown = shown[:5]
		}
		// Silent truncation on a status read is the same failure as the cap
		// one layer up: she has to say the list is not the whole list.
		line := phraser.A(phraser.HomeOn, map[string]string{"items": strings.Join(shown, ", ")})
		if rest > 0 {
			line += fmt.Sprintf(" и ещё %d", rest)
		}
		parts = append(parts, line)
	case dark > 0 && len(sensors) == 0:
		// Nothing is on and everything she can see is unreachable. "всё
		// выключено" would be a claim about the house she cannot make.
		return phraser.A(phraser.HomeDark, map[string]string{
			"count": strconv.Itoa(dark),
			"word":  phraser.Devices(dark),
		}), true
	default:
		parts = append(parts, "всё выключено")
	}
	if len(sensors) > 0 {
		parts = append(parts, strings.Join(sensors, ", "))
	}
	if dark > 0 {
		parts = append(parts, fmt.Sprintf("%d %s не отвечают", dark, phraser.Devices(dark)))
	}
	return strings.Join(parts, "; ") + ".", true
}

// isHomeQuery recognises a question about the house, narrowly. "дома" on its
// own is not enough — "я дома" is a fact, not a question — so it takes a house
// marker AND an ask AND either a device word or the word "включ…". Weather
// wording bails out first: "какая температура на улице?" belongs to the weather
// source, and both questions contain "температура".
func isHomeQuery(u string) bool {
	s := strings.ToLower(strings.TrimSpace(u))
	if s == "" {
		return false
	}
	for _, w := range []string{"погод", "на улице", "прогноз"} {
		if strings.Contains(s, w) {
			return false
		}
	}
	for _, phrase := range []string{"что включено", "что выключено", "умный дом", "что в доме включено"} {
		if strings.Contains(s, phrase) {
			return true
		}
	}
	house := homeWord(s, "дома") || strings.Contains(s, "в доме") || strings.Contains(s, "в квартире")
	if !house {
		return false
	}
	ask := strings.Contains(s, "?") || homeWord(s, "что") || homeWord(s, "какая") ||
		homeWord(s, "какой") || homeWord(s, "сколько")
	if !ask {
		return false
	}
	for _, w := range []string{"свет", "лампа", "лампы", "розетк", "датчик", "температур", "включ", "выключ"} {
		if strings.Contains(s, w) {
			return true
		}
	}
	return false
}

// homeWord — whole-token membership, so "дома" does not fire on "домашний".
// Punctuation is trimmed off each token because a spoken question arrives with
// a question mark glued to the last word.
func homeWord(s, w string) bool {
	for _, tok := range strings.Fields(s) {
		if strings.Trim(tok, ".,!?;:") == w {
			return true
		}
	}
	return false
}
