// Package homeassistant provides a client for the Home Assistant API.
package homeassistant

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/nugget/thane-ai-agent/internal/platform/httpkit"
)

// Client is a Home Assistant REST API client.
type Client struct {
	baseURL    string
	token      string
	httpClient *http.Client
	watcher    readyChecker // set via SetWatcher for health status
	ws         *WSClient
}

// readyChecker is satisfied by connwatch.Watcher. Defined here to avoid
// importing connwatch directly, keeping the dependency one-directional.
type readyChecker interface {
	IsReady() bool
}

// SetWatcher sets the connection watcher for health status queries.
func (c *Client) SetWatcher(w readyChecker) {
	c.watcher = w
}

// UseWSClient sets the shared Home Assistant WebSocket client used for
// registry/config APIs that are only exposed over WebSocket.
func (c *Client) UseWSClient(ws *WSClient) {
	c.ws = ws
}

// HasWSClient reports whether a WebSocket client is available for
// registry/config API calls.
func (c *Client) HasWSClient() bool {
	return c.ws != nil
}

// IsReady reports whether Home Assistant is currently reachable.
// Returns true if no watcher is configured (backward compatible).
func (c *Client) IsReady() bool {
	if c.watcher == nil {
		return true
	}
	return c.watcher.IsReady()
}

// NewClient creates a new Home Assistant client.
//
// Issue #53: Go net.Dial intermittently fails on macOS with "no route to host"
// for LAN targets due to ARP table race conditions. Retry with a short delay
// allows the ARP entry to refresh before the second attempt.
func NewClient(baseURL, token string, logger *slog.Logger) *Client {
	return &Client{
		baseURL: baseURL,
		token:   token,
		httpClient: httpkit.NewClient(
			httpkit.WithTimeout(30*time.Second),
			httpkit.WithRetry(3, 2*time.Second),
			httpkit.WithLogger(logger),
		),
	}
}

// State represents an entity state from Home Assistant.
type State struct {
	EntityID    string         `json:"entity_id"`
	State       string         `json:"state"`
	Attributes  map[string]any `json:"attributes"`
	LastChanged time.Time      `json:"last_changed"`
	LastUpdated time.Time      `json:"last_updated"`
}

type weatherForecastResponse struct {
	Forecast []map[string]any `json:"forecast"`
}

// APIStatus represents the HA API status response.
type APIStatus struct {
	Message string `json:"message"`
}

// Config represents basic HA configuration.
type Config struct {
	LocationName string  `json:"location_name"`
	Latitude     float64 `json:"latitude"`
	Longitude    float64 `json:"longitude"`
	Elevation    int     `json:"elevation"`
	UnitSystem   struct {
		Length      string `json:"length"`
		Mass        string `json:"mass"`
		Temperature string `json:"temperature"`
		Volume      string `json:"volume"`
	} `json:"unit_system"`
	TimeZone string `json:"time_zone"`
	Version  string `json:"version"`
}

// Ping checks if the API is reachable.
func (c *Client) Ping(ctx context.Context) error {
	var status APIStatus
	if err := c.get(ctx, "/api/", &status); err != nil {
		return err
	}
	if status.Message != "API running." {
		return fmt.Errorf("unexpected API status: %s", status.Message)
	}
	return nil
}

// GetConfig retrieves the Home Assistant configuration.
func (c *Client) GetConfig(ctx context.Context) (*Config, error) {
	var cfg Config
	if err := c.get(ctx, "/api/config", &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// GetStates retrieves all entity states.
func (c *Client) GetStates(ctx context.Context) ([]State, error) {
	var states []State
	if err := c.get(ctx, "/api/states", &states); err != nil {
		return nil, err
	}
	return states, nil
}

// GetState retrieves a single entity state.
func (c *Client) GetState(ctx context.Context, entityID string) (*State, error) {
	var state State
	if err := c.get(ctx, "/api/states/"+entityID, &state); err != nil {
		return nil, err
	}
	return &state, nil
}

// GetStateHistory retrieves recorder-backed state history for one entity across
// a time window. The result is sorted in Home Assistant's returned order, which
// is typically chronological.
func (c *Client) GetStateHistory(ctx context.Context, entityID string, startTime, endTime time.Time) ([]State, error) {
	if entityID == "" {
		return nil, nil
	}

	query := url.Values{}
	query.Set("filter_entity_id", entityID)
	query.Set("end_time", endTime.UTC().Format(time.RFC3339))
	query.Set("no_attributes", "1")
	query.Set("significant_changes_only", "0")

	path := "/api/history/period/" + startTime.UTC().Format(time.RFC3339)
	if encoded := query.Encode(); encoded != "" {
		path += "?" + encoded
	}

	var payload [][]State
	if err := c.get(ctx, path, &payload); err != nil {
		return nil, err
	}
	if len(payload) == 0 {
		return nil, nil
	}

	states := append([]State(nil), payload[0]...)
	for i := range states {
		if states[i].EntityID == "" {
			states[i].EntityID = entityID
		}
	}
	return states, nil
}

// GetWeatherForecasts retrieves forecast rows for a weather entity using Home
// Assistant's weather.get_forecasts response API. forecastType must be daily,
// hourly, or twice_daily.
func (c *Client) GetWeatherForecasts(ctx context.Context, entityID, forecastType string) ([]map[string]any, error) {
	if entityID == "" {
		return nil, nil
	}
	var err error
	forecastType, err = normalizeWeatherForecastType(forecastType)
	if err != nil {
		return nil, err
	}

	var response struct {
		ServiceResponse map[string]weatherForecastResponse `json:"service_response"`
	}
	body := map[string]any{
		"entity_id": entityID,
		"type":      forecastType,
	}
	if err := c.post(ctx, "/api/services/weather/get_forecasts?return_response", body, &response); err != nil {
		return nil, err
	}

	item, ok := response.ServiceResponse[entityID]
	if !ok {
		return nil, fmt.Errorf("weather forecast response missing entity %q", entityID)
	}
	return append([]map[string]any(nil), item.Forecast...), nil
}

func normalizeWeatherForecastType(value string) (string, error) {
	value = strings.ToLower(strings.TrimSpace(value))
	value = strings.ReplaceAll(value, "-", "_")
	if value == "" {
		return "daily", nil
	}
	switch value {
	case "daily", "hourly", "twice_daily":
		return value, nil
	default:
		return "", fmt.Errorf("invalid forecast type %q: must be one of daily, hourly, or twice_daily", value)
	}
}

// CallService calls a Home Assistant service.
func (c *Client) CallService(ctx context.Context, domain, service string, data map[string]any) error {
	path := fmt.Sprintf("/api/services/%s/%s", domain, service)
	return c.post(ctx, path, data, nil)
}

// Area represents a Home Assistant area.
type Area struct {
	AreaID              string   `json:"area_id"`
	Name                string   `json:"name"`
	Aliases             []string `json:"aliases"`
	Labels              []string `json:"labels"`
	Icon                string   `json:"icon"`
	FloorID             string   `json:"floor_id"`
	Picture             string   `json:"picture"`
	TemperatureEntityID string   `json:"temperature_entity_id"`
	HumidityEntityID    string   `json:"humidity_entity_id"`
}

// GetAreas retrieves all areas from the area registry.
func (c *Client) GetAreas(ctx context.Context) ([]Area, error) {
	if c.ws != nil {
		return c.ws.GetAreaRegistry(ctx)
	}
	var areas []Area
	if err := c.get(ctx, "/api/config/area_registry/list", &areas); err != nil {
		return nil, err
	}
	return areas, nil
}

// EntityRegistryEntry represents an entity from the registry with area info.
type EntityRegistryEntry struct {
	ID                  string            `json:"id"`
	EntityID            string            `json:"entity_id"`
	Name                string            `json:"name"`
	OriginalName        string            `json:"original_name"`
	Aliases             []string          `json:"aliases"`
	AreaID              string            `json:"area_id"`
	DeviceID            string            `json:"device_id"`
	Platform            string            `json:"platform"`
	DisabledBy          string            `json:"disabled_by"`
	HiddenBy            string            `json:"hidden_by"`
	EntityCategory      string            `json:"entity_category"`
	Labels              []string          `json:"labels"`
	Icon                string            `json:"icon"`
	OriginalIcon        string            `json:"original_icon"`
	DeviceClass         string            `json:"device_class"`
	OriginalDeviceClass string            `json:"original_device_class"`
	HasEntityName       bool              `json:"has_entity_name"`
	UniqueID            string            `json:"unique_id"`
	TranslationKey      string            `json:"translation_key"`
	Options             map[string]any    `json:"options"`
	Categories          map[string]string `json:"categories"`
	Capabilities        map[string]any    `json:"capabilities"`
}

// IsDisabled reports whether the entity is disabled in Home Assistant.
func (e EntityRegistryEntry) IsDisabled() bool {
	return e.DisabledBy != ""
}

// GetEntityRegistry retrieves the entity registry.
func (c *Client) GetEntityRegistry(ctx context.Context) ([]EntityRegistryEntry, error) {
	if c.ws != nil {
		return c.ws.GetEntityRegistryWS(ctx)
	}
	var entries []EntityRegistryEntry
	if err := c.get(ctx, "/api/config/entity_registry/list", &entries); err != nil {
		return nil, err
	}
	return entries, nil
}

// EntityInfo combines state and registry info for an entity.
type EntityInfo struct {
	EntityID     string
	FriendlyName string
	AreaID       string
	Domain       string
	State        string
}

// GetEntities retrieves entities, optionally filtered by domain.
func (c *Client) GetEntities(ctx context.Context, domain string) ([]EntityInfo, error) {
	states, err := c.GetStates(ctx)
	if err != nil {
		return nil, fmt.Errorf("get states: %w", err)
	}

	var entities []EntityInfo
	for _, s := range states {
		parts := splitEntityID(s.EntityID)
		if len(parts) != 2 {
			continue
		}
		entityDomain := parts[0]

		if domain != "" && entityDomain != domain {
			continue
		}

		friendlyName := ""
		if fn, ok := s.Attributes["friendly_name"].(string); ok {
			friendlyName = fn
		}

		entities = append(entities, EntityInfo{
			EntityID:     s.EntityID,
			FriendlyName: friendlyName,
			Domain:       entityDomain,
			State:        s.State,
		})
	}

	return entities, nil
}

func splitEntityID(entityID string) []string {
	for i, c := range entityID {
		if c == '.' {
			return []string{entityID[:i], entityID[i+1:]}
		}
	}
	return nil
}

// APIError represents a non-2xx Home Assistant REST response.
type APIError struct {
	StatusCode int
	Body       string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Body)
}

// get performs a GET request to the HA API.
func (c *Client) get(ctx context.Context, path string, result any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request %s: %w", path, err)
	}
	// Drain and close to ensure connection reuse even when result is nil.
	defer httpkit.DrainAndClose(resp.Body, 4096)

	if resp.StatusCode != http.StatusOK {
		body := httpkit.ReadErrorBody(resp.Body, 512)
		return &APIError{StatusCode: resp.StatusCode, Body: body}
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("decode response: %w", err)
		}
	}

	return nil
}

// post performs a POST request to the HA API.
func (c *Client) post(ctx context.Context, path string, data any, result any) error {
	var reqBody []byte
	if data != nil {
		var err error
		reqBody, err = json.Marshal(data)
		if err != nil {
			return fmt.Errorf("marshal data: %w", err)
		}
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqBody))
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request %s: %w", path, err)
	}
	// Drain and close to ensure connection reuse even when result is nil.
	defer httpkit.DrainAndClose(resp.Body, 4096)

	if resp.StatusCode != http.StatusOK {
		body := httpkit.ReadErrorBody(resp.Body, 512)
		return &APIError{StatusCode: resp.StatusCode, Body: body}
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("decode response: %w", err)
		}
	}

	return nil
}

// delete performs a DELETE request to the HA API.
func (c *Client) delete(ctx context.Context, path string, result any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.baseURL+path, nil)
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request %s: %w", path, err)
	}
	defer httpkit.DrainAndClose(resp.Body, 4096)

	if resp.StatusCode != http.StatusOK {
		body := httpkit.ReadErrorBody(resp.Body, 512)
		return &APIError{StatusCode: resp.StatusCode, Body: body}
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("decode response: %w", err)
		}
	}

	return nil
}
