// Package daemoncontrol defines the private receipt used to discover and
// authenticate the local desktop daemon.
package daemoncontrol

import (
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/x/proc"
)

const (
	SchemaVersion           = 1
	legacyDesktopAPIVersion = 1
	DesktopAPIVersion       = 2
)

// OwnerKind identifies the process class that owns a daemon instance.
type OwnerKind string

const (
	OwnerKindElectron OwnerKind = "electron"
	OwnerKindCLI      OwnerKind = "cli"
	OwnerKindService  OwnerKind = "service"
)

// ProcessIdentity distinguishes one process lifetime from PID reuse.
type ProcessIdentity struct {
	PID       int       `json:"pid"`
	StartedAt time.Time `json:"processStartedAt"`
	StartID   string    `json:"processStartId,omitempty"`
}

// Owner describes the process that owns the daemon. ProcessIdentity is
// embedded so its JSON fields appear directly under owner.
type Owner struct {
	Kind OwnerKind `json:"kind"`
	ID   string    `json:"id"`
	ProcessIdentity
}

// Receipt is the versioned daemon discovery and control record.
type Receipt struct {
	SchemaVersion     int       `json:"schemaVersion"`
	DesktopAPIVersion int       `json:"desktopApiVersion"`
	InstanceID        string    `json:"instanceId"`
	PID               int       `json:"pid"`
	StartedAt         time.Time `json:"processStartedAt"`
	StartID           string    `json:"processStartId,omitempty"`
	BaseURL           string    `json:"baseURL"`
	Owner             Owner     `json:"owner"`
	ControlToken      string    `json:"controlToken"`
}

// Identity returns the daemon process identity recorded by the receipt.
func (r Receipt) Identity() ProcessIdentity {
	return ProcessIdentity{
		PID:       r.PID,
		StartedAt: r.StartedAt,
		StartID:   r.StartID,
	}
}

// Matches reports whether process has the same PID-safe lifetime identity.
func (identity ProcessIdentity) Matches(process proc.ProcessInfo) bool {
	return proc.ProcessIdentityMatches(proc.ProcessInfo{
		PID:       identity.PID,
		StartedAt: identity.StartedAt,
		StartID:   identity.StartID,
	}, process)
}

// Validate strictly validates the receipt and normalizes its base URL.
func (r *Receipt) Validate() error {
	if r == nil {
		return invalidReceipt("receipt is nil")
	}
	if r.DesktopAPIVersion != DesktopAPIVersion {
		return invalidReceipt("desktopApiVersion must be %d", DesktopAPIVersion)
	}
	return r.validateFields()
}

// validateForReconciliation recognizes v1 only for stale receipt cleanup.
func (r *Receipt) validateForReconciliation() error {
	if r == nil {
		return invalidReceipt("receipt is nil")
	}
	if r.DesktopAPIVersion != DesktopAPIVersion &&
		r.DesktopAPIVersion != legacyDesktopAPIVersion {
		return invalidReceipt("desktopApiVersion must be %d", DesktopAPIVersion)
	}
	return r.validateFields()
}

func (r *Receipt) validateFields() error {
	if r.SchemaVersion != SchemaVersion {
		return invalidReceipt("schemaVersion must be %d", SchemaVersion)
	}
	if strings.TrimSpace(r.InstanceID) == "" {
		return invalidReceipt("instanceId must not be blank")
	}
	if err := validateProcessIdentity("daemon", r.Identity()); err != nil {
		return err
	}
	if strings.TrimSpace(r.Owner.ID) == "" {
		return invalidReceipt("owner.id must not be blank")
	}
	switch r.Owner.Kind {
	case OwnerKindElectron, OwnerKindCLI, OwnerKindService:
	default:
		return invalidReceipt("owner.kind is unsupported")
	}
	if err := validateProcessIdentity("owner", r.Owner.ProcessIdentity); err != nil {
		return err
	}
	if strings.TrimSpace(r.ControlToken) == "" {
		return invalidReceipt("controlToken must not be blank")
	}
	baseURL, err := NormalizeBaseURL(r.BaseURL)
	if err != nil {
		return err
	}
	r.BaseURL = baseURL
	return nil
}

// NormalizeBaseURL validates a loopback-only HTTP origin and returns its
// canonical 127.0.0.1 representation.
func NormalizeBaseURL(raw string) (string, error) {
	parsed, err := url.Parse(raw)
	if err != nil {
		return "", invalidReceipt("baseURL is not a valid URL")
	}
	if parsed.Scheme != "http" {
		return "", invalidReceipt("baseURL scheme must be http")
	}
	if parsed.User != nil {
		return "", invalidReceipt("baseURL must not contain userinfo")
	}
	if parsed.Path != "" && parsed.Path != "/" {
		return "", invalidReceipt("baseURL must be a pure origin")
	}
	if parsed.RawPath != "" || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" || strings.Contains(raw, "#") {
		return "", invalidReceipt("baseURL must not contain path, query, or fragment data")
	}

	host := parsed.Hostname()
	switch host {
	case "127.0.0.1", "localhost", "::1":
	default:
		return "", invalidReceipt("baseURL hostname must be loopback")
	}
	portText := parsed.Port()
	if portText == "" {
		return "", invalidReceipt("baseURL must contain an explicit port")
	}
	port, err := strconv.Atoi(portText)
	if err != nil || port < 1 || port > 65535 {
		return "", invalidReceipt("baseURL port is invalid")
	}
	if parsed.Host != net.JoinHostPort(host, portText) &&
		parsed.Host != host+":"+portText {
		return "", invalidReceipt("baseURL host or port is invalid")
	}
	return "http://" + net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), nil
}

// String returns a JSON diagnostic representation with the control token
// redacted.
func (r Receipt) String() string {
	return r.DiagnosticJSON()
}

// DiagnosticJSON returns receipt JSON suitable for logs and diagnostics.
func (r Receipt) DiagnosticJSON() string {
	r.ControlToken = "[REDACTED]"
	data, err := json.Marshal(r)
	if err != nil {
		return `{"controlToken":"[REDACTED]"}`
	}
	return string(data)
}

func validateProcessIdentity(field string, identity ProcessIdentity) error {
	if identity.PID <= 0 {
		return invalidReceipt("%s.pid must be positive", field)
	}
	if identity.StartedAt.IsZero() {
		return invalidReceipt("%s.processStartedAt must not be zero", field)
	}
	if identity.StartID != strings.TrimSpace(identity.StartID) {
		return invalidReceipt("%s.processStartId must not contain surrounding whitespace", field)
	}
	if len(identity.StartID) > 256 {
		return invalidReceipt("%s.processStartId is too long", field)
	}
	return nil
}

func invalidReceipt(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrReceiptInvalid, fmt.Sprintf(format, args...))
}

var (
	ErrReceiptMissing       = errors.New("daemon receipt missing")
	ErrReceiptInvalid       = errors.New("daemon receipt invalid")
	ErrReceiptAmbiguous     = errors.New("daemon receipt state ambiguous")
	ErrDaemonAlreadyRunning = errors.New("daemon already running")
	ErrReceiptMismatch      = errors.New("daemon receipt mismatch")
)
