package agent

import (
	"fmt"
	"log"
	"strings"

	"go.yaml.in/yaml/v3"

	"github.com/tidbcloud/AgentTeam/desktop/internal/crypto"
)

// ---------------------------------------------------------------------------
// Config (declarative configuration)
// ---------------------------------------------------------------------------

// LLMMode controls how an agent resolves its upstream LLM endpoint.
type LLMMode string

const (
	// LLMModeRuntimeDefault means the agent inherits the runtime's own saved
	// configuration instead of carrying an inline upstream endpoint.
	LLMModeRuntimeDefault LLMMode = "runtime_default"
	// LLMModeInline means the agent itself carries an explicit upstream
	// endpoint configuration.
	LLMModeInline LLMMode = "inline"

	LLMCredentialKindMaaSCapability = "maas_capability"
)

type legacyLLMConfig struct {
	Mode    string            `yaml:"mode,omitempty"`
	BaseURL string            `yaml:"base_url,omitempty"`
	APIKey  string            `yaml:"api_key,omitempty"`
	Headers map[string]string `yaml:"headers,omitempty"`
	WireAPI string            `yaml:"wire_api,omitempty"`
}

type legacyPrefixedLLMConfig struct {
	Mode    string            `yaml:"llm_mode,omitempty"`
	BaseURL string            `yaml:"llm_base_url,omitempty"`
	APIKey  string            `yaml:"llm_api_key,omitempty"`
	Headers map[string]string `yaml:"llm_headers,omitempty"`
	WireAPI string            `yaml:"llm_wire_api,omitempty"`
}

func resolveLLMMode(value string) LLMMode {
	switch LLMMode(strings.TrimSpace(value)) {
	case LLMModeInline:
		return LLMModeInline
	default:
		return LLMModeRuntimeDefault
	}
}

func normalizeHeaders(headers map[string]string) map[string]string {
	if len(headers) == 0 {
		return map[string]string{}
	}
	normalized := make(map[string]string, len(headers))
	for key, value := range headers {
		trimmedKey := strings.TrimSpace(key)
		if trimmedKey == "" {
			continue
		}
		normalized[trimmedKey] = value
	}
	return normalized
}

func normalizeLLMFields(mode string, baseURL string, apiKey string, headers map[string]string, wireAPI string) (string, string, string, map[string]string, string) {
	return string(resolveLLMMode(mode)), strings.TrimSpace(baseURL), strings.TrimSpace(apiKey), normalizeHeaders(headers), strings.TrimSpace(wireAPI)
}

// NormalizeAgentLLMFields normalizes persisted agent LLM settings before they
// are validated, stored, or injected into a runtime launch. It preserves the
// public helper boundary used by API/profile/channel code while keeping the
// legacy-prefixed and nested YAML compatibility logic local to Config.
func NormalizeAgentLLMFields(mode string, baseURL string, apiKey string, headers map[string]string, wireAPI string) (string, string, string, map[string]string, string) {
	return normalizeLLMFields(mode, baseURL, apiKey, headers, wireAPI)
}

// RuntimePolicySnapshot is the current server-owned runtime launch policy
// identity cached with AgentTeam-synced agents. It is used to validate local resume
// state and is not a historical launch override. RuntimeDebugEnabled remains a
// launch-level setting on Config because toggling it should restart the runtime
// process.
type RuntimePolicySnapshot struct {
	SchemaVersion  int    `yaml:"schema_version,omitempty"`
	Generation     int64  `yaml:"generation,omitempty"`
	Fingerprint    string `yaml:"fingerprint,omitempty"`
	Runtime        string `yaml:"runtime,omitempty"`
	RuntimeType    string `yaml:"runtime_type,omitempty"`
	PermissionMode string `yaml:"permission_mode,omitempty"`
}

// Config holds the complete definition of an Agent persona.
// It references a Runtime by name and attaches execution resources
// (policy constraints). The Cloud Memory identity is materialized alongside the agent workspace
// and accessed via the docs API.
type Config struct {
	// ID is the unique identifier for this agent (e.g., "fullstack-dev").
	ID string `yaml:"id"`
	// Name is the human-readable display name (e.g., "Full-Stack Developer").
	Name string `yaml:"name,omitempty"`
	// Runtime is the name of the Runtime this agent binds to
	// (e.g., "opencode", "claude", "codex"). Auto-detected from PATH.
	Runtime string `yaml:"runtime"`
	// PermissionMode controls the local runtime sandbox requested by AgentTeam.
	PermissionMode string `yaml:"permission_mode,omitempty"`
	// RuntimePolicy caches the current AgentTeam server runtime policy identity.
	RuntimePolicy RuntimePolicySnapshot `yaml:"runtime_policy,omitempty"`
	// RuntimeDebugEnabled enables extra runtime and hosted-model diagnostics for
	// this AgentTeam-synced agent without changing prompt behavior.
	RuntimeDebugEnabled bool `yaml:"runtime_debug_enabled,omitempty"`
	// Description explains what this Agent persona is for.
	Description string `yaml:"description,omitempty"`
	// Labels is an optional set of free-form tags for categorisation.
	Labels []string `yaml:"labels,omitempty"`
	// LLMMode controls whether the agent uses the runtime's default configuration
	// or an inline upstream endpoint.
	LLMMode string `yaml:"mode,omitempty"`
	// BaseURL is the inline upstream API base URL.
	BaseURL string `yaml:"base_url,omitempty"`
	// APIKey is the inline upstream credential used by the runtime.
	APIKey string `yaml:"api_key,omitempty"`
	// Headers stores extra inline upstream headers.
	Headers map[string]string `yaml:"headers,omitempty"`
	// WireAPI is one of openai, responses, or anthropic.
	WireAPI string `yaml:"wire_api,omitempty"`
	// CredentialKind marks how a short-lived upstream credential should be issued.
	CredentialKind string `yaml:"credential_kind,omitempty"`
	// Model optionally overrides the runtime's default model.
	Model string `yaml:"model,omitempty"`
	// SmallModel optionally specifies a smaller/faster model for lightweight tasks.
	SmallModel string `yaml:"small_model,omitempty"`
	// ReasoningEffort optionally tunes runtime-specific reasoning depth.
	ReasoningEffort string `yaml:"reasoning_effort,omitempty"`
	// Policy defines optional execution constraints for sessions using this Agent.
	Policy Policy `yaml:"policy,omitempty"`
	// Source indicates where this agent was created from.
	// For locally created agents, Type is "local".
	// For agentteam-synced agents, Type is "agentteam" and ID is the channel ID that
	// owns this agent, scoping sync-based deletion to the originating channel.
	Source AgentSource `yaml:"source,omitempty"`
}

type configAlias Config

// MarshalYAML normalizes inline LLM fields and encrypts the inline API key when
// local crypto keys are available. It keeps the on-disk agents.yaml shape stable
// while avoiding plaintext secret writes for newly-saved agent records.
func (c Config) MarshalYAML() (interface{}, error) {
	alias := configAlias(c)
	alias.LLMMode, alias.BaseURL, alias.APIKey, alias.Headers, alias.WireAPI = normalizeLLMFields(alias.LLMMode, alias.BaseURL, alias.APIKey, alias.Headers, alias.WireAPI)
	alias.CredentialKind = strings.TrimSpace(alias.CredentialKind)
	if alias.APIKey != "" && crypto.KeysSet() {
		encrypted, err := crypto.Encrypt(alias.APIKey)
		if err != nil {
			log.Printf("agent: encrypt llm api key: %v", err)
			return alias, nil
		}
		alias.APIKey = encrypted
	}
	return alias, nil
}

// UnmarshalYAML decodes both the current inline LLM fields and historical
// prefixed/nested LLM shapes, then decrypts an encrypted inline API key when
// local crypto keys are available.
func (c *Config) UnmarshalYAML(value *yaml.Node) error {
	type configYAML struct {
		configAlias          `yaml:",inline"`
		LegacyPrefixedLLM    legacyPrefixedLLMConfig `yaml:",inline"`
		LegacyNestedLLMBlock *legacyLLMConfig        `yaml:"llm,omitempty"`
	}

	var decoded configYAML
	if err := value.Decode(&decoded); err != nil {
		return err
	}

	alias := decoded.configAlias
	if strings.TrimSpace(alias.LLMMode) == "" &&
		strings.TrimSpace(alias.BaseURL) == "" &&
		strings.TrimSpace(alias.APIKey) == "" &&
		len(alias.Headers) == 0 &&
		strings.TrimSpace(alias.WireAPI) == "" {
		alias.LLMMode = decoded.LegacyPrefixedLLM.Mode
		alias.BaseURL = decoded.LegacyPrefixedLLM.BaseURL
		alias.APIKey = decoded.LegacyPrefixedLLM.APIKey
		alias.Headers = decoded.LegacyPrefixedLLM.Headers
		alias.WireAPI = decoded.LegacyPrefixedLLM.WireAPI
	}
	if decoded.LegacyNestedLLMBlock != nil &&
		strings.TrimSpace(alias.LLMMode) == "" &&
		strings.TrimSpace(alias.BaseURL) == "" &&
		strings.TrimSpace(alias.APIKey) == "" &&
		len(alias.Headers) == 0 &&
		strings.TrimSpace(alias.WireAPI) == "" {
		alias.LLMMode = decoded.LegacyNestedLLMBlock.Mode
		alias.BaseURL = decoded.LegacyNestedLLMBlock.BaseURL
		alias.APIKey = decoded.LegacyNestedLLMBlock.APIKey
		alias.Headers = decoded.LegacyNestedLLMBlock.Headers
		alias.WireAPI = decoded.LegacyNestedLLMBlock.WireAPI
	}

	alias.LLMMode, alias.BaseURL, alias.APIKey, alias.Headers, alias.WireAPI = normalizeLLMFields(alias.LLMMode, alias.BaseURL, alias.APIKey, alias.Headers, alias.WireAPI)
	alias.CredentialKind = strings.TrimSpace(alias.CredentialKind)
	if alias.APIKey != "" && crypto.KeysSet() && crypto.IsEncrypted(alias.APIKey) {
		decrypted, err := crypto.Decrypt(alias.APIKey)
		if err != nil {
			log.Printf("agent: decrypt llm api key: %v", err)
			return err
		}
		alias.APIKey = decrypted
	}

	*c = Config(alias)
	return nil
}

// AgentSource identifies the origin of an agent config entry.
// Type is the source kind (e.g., "local", "agentteam").
// ID is the source identifier (the server connection ID for agentteam-sourced agents).
type AgentSource struct {
	Type string `yaml:"type"`
	ID   string `yaml:"id,omitempty"`
}

// String returns a human-readable representation of the source.
func (s AgentSource) String() string {
	if s.ID != "" {
		return s.Type + ":" + s.ID
	}
	return s.Type
}

// IsZero reports whether s is the zero value.
func (s AgentSource) IsZero() bool {
	return s.Type == "" && s.ID == ""
}

// MarshalYAML implements yaml.Marshaler. A plain source (no ID) is written
// as a bare string for backward compatibility with agents.yaml.
func (s AgentSource) MarshalYAML() (interface{}, error) {
	if s.ID == "" {
		return s.Type, nil
	}
	return map[string]string{"type": s.Type, "id": s.ID}, nil
}

// UnmarshalYAML implements yaml.Unmarshaler. Accepts a bare string or a map.
func (s *AgentSource) UnmarshalYAML(unmarshal func(interface{}) error) error {
	// Try bare string first.
	var plain string
	if err := unmarshal(&plain); err == nil {
		s.Type = plain
		s.ID = ""
		return nil
	}
	var m map[string]string
	if err := unmarshal(&m); err != nil {
		return err
	}
	s.Type = m["type"]
	s.ID = m["id"]
	return nil
}

// NewLocalSource returns an AgentSource for a locally created agent.
func NewLocalSource() AgentSource {
	return AgentSource{Type: "local"}
}

// NewAgentTeamSource returns an AgentSource for a agentteam-synced agent with the given
// server connection ID.
func NewAgentTeamSource(serverID string) AgentSource {
	return AgentSource{Type: "agentteam", ID: serverID}
}

// Policy defines execution constraints for an Agent.
type Policy struct {
	// MaxConcurrentSessions limits how many sessions this Agent can run simultaneously.
	// Zero means no limit.
	MaxConcurrentSessions int `yaml:"max_concurrent_sessions,omitempty"`
	// Timeout is the default session execution timeout. Empty string means no timeout.
	Timeout string `yaml:"timeout,omitempty"`
	// AllowedPaths restricts which project paths this Agent is allowed to access.
	// Empty means no restriction.
	AllowedPaths []string `yaml:"allowed_paths,omitempty"`
}

// ValidateAgent checks an agent config for correctness and returns any validation errors.
// A valid agent config must have a non-empty ID and reference a non-empty runtime.
func ValidateAgent(a Config) error {
	var errs []string

	if strings.TrimSpace(a.ID) == "" {
		errs = append(errs, "agent ID must not be empty")
	}

	if strings.TrimSpace(a.Runtime) == "" {
		errs = append(errs, fmt.Sprintf("agent %q must reference a runtime", a.ID))
	}

	llmMode, _, _, _, wireAPI := normalizeLLMFields(a.LLMMode, a.BaseURL, a.APIKey, a.Headers, a.WireAPI)
	switch resolveLLMMode(llmMode) {
	case LLMModeInline:
	case LLMModeRuntimeDefault:
	default:
		errs = append(errs, fmt.Sprintf("agent %q has unsupported llm mode %q", a.ID, llmMode))
	}
	if wireAPI != "" {
		switch wireAPI {
		case "openai", "responses", "anthropic":
		default:
			errs = append(errs, fmt.Sprintf("agent %q has unsupported llm wire_api %q", a.ID, wireAPI))
		}
	}
	if credentialKind := strings.TrimSpace(a.CredentialKind); credentialKind != "" && credentialKind != LLMCredentialKindMaaSCapability {
		errs = append(errs, fmt.Sprintf("agent %q has unsupported llm credential_kind %q", a.ID, credentialKind))
	}

	if strings.TrimSpace(a.Policy.Timeout) != "" {
		if _, err := parseDuration(a.Policy.Timeout); err != nil {
			errs = append(errs, fmt.Sprintf("agent %q has invalid policy timeout: %s", a.ID, a.Policy.Timeout))
		}
	}

	if a.Policy.MaxConcurrentSessions < 0 {
		errs = append(errs, fmt.Sprintf("agent %q has invalid max_concurrent_sessions: must be >= 0", a.ID))
	}

	if len(errs) > 0 {
		return fmt.Errorf("validation failed: %s", strings.Join(errs, "; "))
	}

	return nil
}

// parseDuration is a simple duration parser that accepts Go duration strings
// and common human-friendly formats like "30s", "5m", "1h".
func parseDuration(s string) (string, error) {
	trimmed := strings.TrimSpace(s)
	if trimmed == "" {
		return "", nil
	}
	if len(trimmed) < 2 {
		return "", fmt.Errorf("invalid duration: %q", trimmed)
	}
	return trimmed, nil
}
