package daemon

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"slices"
	"time"
)

const ProtocolVersion = 2

// Health describes the daemon's readiness and API contract.
type Health struct {
	OK           bool     `json:"ok"`
	Version      int      `json:"version"`
	Capabilities []string `json:"capabilities"`
	Build        string   `json:"build,omitempty"`
}

func decodeHealth(body []byte) (Health, error) {
	var wire struct {
		OK           *bool             `json:"ok"`
		Version      *int              `json:"version"`
		Capabilities []json.RawMessage `json:"capabilities"`
		Build        json.RawMessage   `json:"build"`
	}
	invalid := func(cause error) (Health, error) {
		return Health{}, &ProtocolError{Code: "invalid_health", Operation: "probe health", Cause: cause}
	}
	if err := json.Unmarshal(body, &wire); err != nil {
		return invalid(err)
	}
	if wire.OK == nil || !*wire.OK {
		return invalid(errors.New("health must report ok:true"))
	}
	if wire.Version == nil || *wire.Version <= 0 {
		return invalid(errors.New("health must report a positive integer version"))
	}
	if wire.Capabilities == nil {
		return invalid(errors.New("health must report a capabilities array"))
	}
	health := Health{OK: true, Version: *wire.Version, Capabilities: make([]string, 0, len(wire.Capabilities))}
	for _, raw := range wire.Capabilities {
		var capability string
		if string(raw) == "null" {
			return invalid(errors.New("health capability must be a string"))
		}
		if err := json.Unmarshal(raw, &capability); err != nil {
			return invalid(err)
		}
		health.Capabilities = append(health.Capabilities, capability)
	}
	if len(wire.Build) > 0 {
		if string(wire.Build) == "null" {
			return invalid(errors.New("health build must be a string"))
		}
		if err := json.Unmarshal(wire.Build, &health.Build); err != nil {
			return invalid(err)
		}
	}
	return health, nil
}

// ProbeHealth validates health without rediscovery or lifecycle changes.
func ProbeHealth(ctx context.Context, conn *Connection) (Health, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if conn == nil {
		return Health{}, errors.New("not connected to Albedo")
	}
	operation := operation{Name: "probe health", Method: http.MethodGet, Path: "/health", Policy: noRecovery}
	body, err := requestBytes(ctx, conn, operation, responseLimits{successStatus: http.StatusOK, bodyBytes: 64 * 1024, errorBytes: 64 * 1024})
	if err != nil {
		return Health{}, err
	}
	return decodeHealth(body)
}

// CheckCompatible checks the protocol and core capabilities used by this client.
func CheckCompatible(health Health) error {
	if !health.OK || health.Version <= 0 || health.Capabilities == nil {
		return &ProtocolError{Code: "invalid_health", Operation: "attach daemon", Cause: errors.New("invalid daemon health")}
	}
	failure := &CompatibilityError{Version: health.Version}
	for _, capability := range []string{"operation_receipts", "session_stream_generation", "agents_stream_overflow"} {
		if !slices.Contains(health.Capabilities, capability) {
			failure.MissingCapabilities = append(failure.MissingCapabilities, capability)
		}
	}
	if health.Version != ProtocolVersion || len(failure.MissingCapabilities) > 0 {
		return failure
	}
	return nil
}

// Attach validates an endpoint before exposing its API connection.
func Attach(ctx context.Context, snapshot ConnectionSnapshot, rediscover Rediscovery) (*Connection, error) {
	if snapshot.Port < 1 || snapshot.Port > 65535 || snapshot.Token == "" {
		return nil, &ProtocolError{Code: "invalid_endpoint", Operation: "attach daemon", Cause: errors.New("daemon endpoint requires a valid port and token")}
	}
	conn := NewConnection(snapshot, rediscover)
	health, err := ProbeHealth(ctx, conn)
	if err == nil {
		err = CheckCompatible(health)
	}
	if err != nil {
		conn.HTTPClient().CloseIdleConnections()
		return nil, err
	}
	snapshot.Version, snapshot.Build = health.Version, health.Build
	conn.snapshot.Store(&snapshot)
	return conn, nil
}

// capabilities reads a validated health response with ordinary read recovery.
func capabilities(ctx context.Context, conn *Connection) ([]string, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
	defer cancel()
	operation := operation{Name: "read capabilities", Method: http.MethodGet, Path: "/health", Policy: readRecovery}
	body, err := requestBytes(ctx, conn, operation, responseLimits{successStatus: http.StatusOK, bodyBytes: 64 * 1024, errorBytes: 64 * 1024})
	if err != nil {
		return nil, err
	}
	health, err := decodeHealth(body)
	return health.Capabilities, err
}

func checkCapability(ctx context.Context, conn *Connection, capability, feature string) error {
	capabilities, err := capabilities(ctx, conn)
	if err != nil {
		return err
	}
	if !slices.Contains(capabilities, capability) {
		return &UpgradeRequiredError{Feature: feature}
	}
	return nil
}

// CheckPromptSupport runs before a headless prompt can create or modify a session.
func CheckPromptSupport(ctx context.Context, conn *Connection) error {
	return checkCapability(ctx, conn, "submission_cancellation", "prompt submission cancellation")
}

func StopDaemon(ctx context.Context, conn *Connection) error {
	return acknowledge(ctx, conn, operation{Name: "stop daemon", Method: http.MethodPost, Path: "/shutdown", Body: map[string]any{}, Policy: noRecovery})
}
