// Package provider manages external provider configurations used by the
// dashboard for upstream service connectivity.
package provider

import (
	"fmt"
	"log"
	"os"
	"strings"

	"go.yaml.in/yaml/v3"

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

// Wire protocol constants supported by providers.
const (
	WireAIOpenAI     = "openai"    // OpenAI Compatible (chat completions API)
	WireAPIResponses = "responses" // OpenAI Responses API
	WireAPIAnthropic = "anthropic" // Anthropic Compatible
)

// AllWireAPIs contains the supported wire protocols.
var AllWireAPIs = []string{
	WireAIOpenAI,
	WireAPIResponses,
	WireAPIAnthropic,
}

// DefaultWireAPIs is the default wire protocol list when none is specified.
var DefaultWireAPIs = []string{WireAIOpenAI}

// Provider defines a dashboard-managed upstream provider configuration.
type Provider struct {
	// ID is the unique identifier for the provider entry.
	ID string `yaml:"id" toml:"id"`
	// Name is the human-readable display name.
	Name string `yaml:"name,omitempty" toml:"name,omitempty"`
	// BaseURL is the provider API base URL.
	BaseURL string `yaml:"base_url,omitempty" toml:"base_url,omitempty"`
	// APIKey stores the provider credential used by clients.
	APIKey string `yaml:"api_key,omitempty" toml:"api_key,omitempty"`
	// Headers stores extra HTTP headers to include when talking to the provider.
	Headers map[string]string `yaml:"headers,omitempty" toml:"headers,omitempty"`
	// Source indicates where this provider was detected from.
	// Empty for manually created providers; set to runtime name (e.g. "opencode",
	// "claude", "codex") for auto-detected providers.
	Source string `yaml:"source,omitempty" toml:"source,omitempty"`
	// WireAPIs lists the wire protocols supported by this provider.
	// Values are WireAIOpenAI, WireAPIResponses, WireAPIAnthropic.
	// When empty, defaults to [WireAIOpenAI].
	WireAPIs []string `yaml:"wire_api,omitempty" toml:"wire_api,omitempty"`
	// EnvKey is the environment variable name used to obtain the API key.
	// When set, the API key is read from this environment variable at detection time.
	EnvKey string `yaml:"env_key,omitempty" toml:"env_key,omitempty"`
	// Models is the list of model IDs selected by the user from SupportedModels.
	// This is the subset that is actively used and displayed in the dashboard.
	Models []string `yaml:"models,omitempty" toml:"models,omitempty"`
	// SupportedModels is the full list of model IDs available from this provider.
	// Populated by calling the provider's /models endpoint or from runtime config.
	SupportedModels []string `yaml:"supported_models,omitempty" toml:"supported_models,omitempty"`
}

type providerAlias Provider

// MarshalYAML encrypts an inline API key when local crypto keys are available.
func (p Provider) MarshalYAML() (interface{}, error) {
	alias := providerAlias(p)

	if alias.APIKey != "" && crypto.KeysSet() {
		encrypted, err := crypto.Encrypt(alias.APIKey)
		if err != nil {
			log.Printf("provider: encrypt api key for %q: %v", p.ID, err)
			return alias, nil
		}
		alias.APIKey = encrypted
	}

	return alias, nil
}

// UnmarshalYAML accepts current and historical provider config shapes and
// decrypts encrypted API keys when local crypto keys are available.
func (p *Provider) UnmarshalYAML(value *yaml.Node) error {
	var raw struct {
		ID               string            `yaml:"id"`
		Name             string            `yaml:"name,omitempty"`
		Type             string            `yaml:"type,omitempty"`
		OpenAIBaseURL    string            `yaml:"openai_base_url,omitempty"`
		AnthropicBaseURL string            `yaml:"anthropic_base_url,omitempty"`
		AnthropicAPIKey  string            `yaml:"anthropic_api_key,omitempty"`
		APIKey           string            `yaml:"api_key,omitempty"`
		AnthropicAuthKey string            `yaml:"anthropic_auth_token,omitempty"`
		BaseURL          string            `yaml:"base_url,omitempty"`
		WireAPI          interface{}       `yaml:"wire_api,omitempty"`
		EnvKey           string            `yaml:"env_key,omitempty"`
		Models           []string          `yaml:"models,omitempty"`
		SupportedModels  []string          `yaml:"supported_models,omitempty"`
		Source           string            `yaml:"source,omitempty"`
		Headers          map[string]string `yaml:"headers,omitempty"`
	}
	if err := value.Decode(&raw); err != nil {
		return err
	}

	alias := providerAlias{
		ID:      raw.ID,
		Name:    raw.Name,
		BaseURL: raw.BaseURL,
		APIKey:  raw.APIKey,
		EnvKey:  raw.EnvKey,
		Models:  raw.Models,
		Source:  raw.Source,
		Headers: raw.Headers,
	}

	if alias.APIKey == "" {
		alias.APIKey = raw.AnthropicAPIKey
	}
	if alias.APIKey == "" {
		alias.APIKey = raw.AnthropicAuthKey
	}

	// Migrate old fields to new unified base_url.
	if alias.BaseURL == "" {
		alias.BaseURL = raw.OpenAIBaseURL
	}
	if alias.BaseURL == "" {
		alias.BaseURL = raw.AnthropicBaseURL
	}

	// Migrate legacy wire_api → WireAPIs.
	if raw.WireAPI != nil {
		switch v := raw.WireAPI.(type) {
		case string:
			if v != "" {
				alias.WireAPIs = []string{v}
			}
		case []interface{}:
			arr := make([]string, 0)
			for _, item := range v {
				if s, ok := item.(string); ok && s != "" {
					arr = append(arr, s)
				}
			}
			if len(arr) > 0 {
				alias.WireAPIs = arr
			}
		}
	}

	// Legacy Type-based wire_api inference.
	if len(alias.WireAPIs) == 0 && raw.Type != "" {
		switch strings.TrimSpace(raw.Type) {
		case "anthropic":
			alias.WireAPIs = []string{WireAPIAnthropic}
		default:
			alias.WireAPIs = []string{WireAIOpenAI}
		}
	}

	if len(alias.WireAPIs) == 0 {
		alias.WireAPIs = DefaultWireAPIs
	}

	*p = Provider(alias)
	return p.decryptAPIKey()
}

func (p *Provider) decryptAPIKey() error {
	if p.APIKey != "" && crypto.KeysSet() && crypto.IsEncrypted(p.APIKey) {
		decrypted, err := crypto.Decrypt(p.APIKey)
		if err != nil {
			log.Printf("provider: decrypt api key for %q: %v", p.ID, err)
			return err
		}
		p.APIKey = decrypted
	}
	return nil
}

// Validate checks a provider for correctness.
func Validate(p Provider) error {
	var errs []string

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

	for _, w := range p.WireAPIs {
		switch strings.TrimSpace(w) {
		case WireAIOpenAI, WireAPIResponses, WireAPIAnthropic:
		default:
			errs = append(errs, fmt.Sprintf("unsupported wire_api %q", w))
		}
	}

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

	return nil
}

// Normalize trims whitespace and ensures Headers and WireAPIs are non-nil.
func Normalize(p Provider) Provider {
	p.ID = strings.TrimSpace(p.ID)
	p.Name = strings.TrimSpace(p.Name)
	p.BaseURL = strings.TrimSpace(p.BaseURL)
	p.APIKey = strings.TrimSpace(p.APIKey)
	p.Source = strings.TrimSpace(p.Source)
	p.EnvKey = strings.TrimSpace(p.EnvKey)
	if p.WireAPIs == nil {
		p.WireAPIs = DefaultWireAPIs
	}
	for i, w := range p.WireAPIs {
		p.WireAPIs[i] = strings.TrimSpace(w)
	}
	if p.Headers == nil {
		p.Headers = map[string]string{}
	}
	return p
}

// ResolveEnvKey returns a copy of p with APIKey populated from EnvKey when the
// provider does not already carry an explicit API key. It does not mutate p so
// callers can use env-backed providers without writing secrets back to disk.
func ResolveEnvKey(p Provider) Provider {
	if strings.TrimSpace(p.APIKey) != "" || strings.TrimSpace(p.EnvKey) == "" {
		return p
	}
	p.APIKey = strings.TrimSpace(os.Getenv(strings.TrimSpace(p.EnvKey)))
	return p
}

// Default base URLs for known wire protocols.
const (
	DefaultAnthropicBaseURL = "https://api.anthropic.com"
	DefaultOpenAIBaseURL    = "https://api.openai.com/v1"
)

// EffectiveBaseURL returns the canonical base URL used for deduplication.
func (p Provider) EffectiveBaseURL() string {
	return strings.TrimRight(p.BaseURL, "/")
}

// ResolvedBaseURL returns the base URL to use for API calls. If BaseURL is
// empty, a sensible default is inferred from WireAPIs and Source.
func (p Provider) ResolvedBaseURL() string {
	if u := p.EffectiveBaseURL(); u != "" {
		return u
	}
	// Infer from wire protocol.
	for _, w := range p.WireAPIs {
		switch w {
		case WireAPIAnthropic:
			return DefaultAnthropicBaseURL
		case WireAIOpenAI, WireAPIResponses:
			return DefaultOpenAIBaseURL
		}
	}
	// Fallback based on source.
	switch p.Source {
	case "claude":
		return DefaultAnthropicBaseURL
	}
	return ""
}

// DedupKey returns a deduplication key based on (EffectiveBaseURL, APIKey).
// Returns an empty string if both are empty, meaning the provider should not
// participate in deduplication.
func (p Provider) DedupKey() string {
	url := p.EffectiveBaseURL()
	if url == "" && p.APIKey == "" {
		return ""
	}
	return url + "\x00" + p.APIKey
}
