package profile

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/pelletier/go-toml/v2"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/provider"
)

const defaultCodexEnvKey = "AGENTTEAM_CODEX_API_KEY"

// ErrRuntimeConfigReadOnly is returned when a runtime-native config file is
// intentionally read-only from AgentTeam's runtime settings surface.
var ErrRuntimeConfigReadOnly = errors.New("runtime config is read-only")

// RuntimeConfigEditable is the dashboard-editable subset of a runtime-native
// upstream provider configuration.
type RuntimeConfigEditable struct {
	BaseURL          string `json:"baseUrl,omitempty"`
	APIKey           string `json:"apiKey,omitempty"`
	APIKeyConfigured bool   `json:"apiKeyConfigured,omitempty"`
	APIKeyEnvKey     string `json:"apiKeyEnvKey,omitempty"`
	Model            string `json:"model,omitempty"`
	ReasoningEffort  string `json:"reasoningEffort,omitempty"`
	WireAPI          string `json:"wireApi,omitempty"`
}

// RuntimeConfigOfflineResult reports local validation without contacting the
// upstream provider.
type RuntimeConfigOfflineResult struct {
	OK      bool     `json:"ok"`
	Message string   `json:"message,omitempty"`
	Missing []string `json:"missing,omitempty"`
}

// RuntimeConfigOnlineResult reports optional upstream probing results such as
// wire API support and available models.
type RuntimeConfigOnlineResult struct {
	OK         bool     `json:"ok"`
	Message    string   `json:"message,omitempty"`
	ReasonCode string   `json:"reasonCode,omitempty"`
	WireAPIs   []string `json:"wireApis,omitempty"`
	Models     []string `json:"models,omitempty"`
}

// RuntimeConfigState is the runtime settings API payload for one runtime,
// including the resolved config path, editable fields, and local validation.
type RuntimeConfigState struct {
	Runtime    string                     `json:"runtime"`
	ConfigPath string                     `json:"configPath"`
	Exists     bool                       `json:"exists"`
	Editable   RuntimeConfigEditable      `json:"editable"`
	Offline    RuntimeConfigOfflineResult `json:"offline"`
}

// ReadRuntimeConfig reads and normalizes one runtime's editable upstream
// provider settings without mutating the runtime-native config file.
func ReadRuntimeConfig(runtimeName string) (RuntimeConfigState, error) {
	normalized := normalizeRuntimeName(runtimeName)
	if normalized == "" {
		return RuntimeConfigState{}, fmt.Errorf("unsupported runtime %q", runtimeName)
	}

	path := runtimeConfigPath(normalized)
	state := RuntimeConfigState{
		Runtime:    normalized,
		ConfigPath: path,
		Editable:   defaultRuntimeConfigEditable(normalized),
		Offline: RuntimeConfigOfflineResult{
			OK:      false,
			Message: "runtime config not found",
		},
	}

	switch normalized {
	case "opencode":
		editable, exists, err := readOpenCodeRuntimeConfig(path)
		if err != nil {
			state.Exists = exists
			state.Editable = editable
			state.Offline = RuntimeConfigOfflineResult{OK: false, Message: err.Error()}
			return state, nil
		}
		state.Exists = exists
		state.Editable = editable
	case "claude":
		editable, exists, err := readClaudeRuntimeConfig(path)
		if err != nil {
			state.Exists = exists
			state.Editable = editable
			state.Offline = RuntimeConfigOfflineResult{OK: false, Message: err.Error()}
			return state, nil
		}
		state.Exists = exists
		state.Editable = editable
	case "codex":
		editable, exists, err := readCodexRuntimeConfig(path)
		if err != nil {
			state.Exists = exists
			state.Editable = editable
			state.Offline = RuntimeConfigOfflineResult{OK: false, Message: err.Error()}
			return state, nil
		}
		state.Exists = exists
		state.Editable = editable
	}

	offline, _ := offlineValidateRuntimeConfig(normalized, state.Editable)
	state.Offline = offline
	return state, nil
}

// UpdateRuntimeConfig patches runtime config with the non-empty incoming values
// and writes the merged result back to the runtime-native config file. Existing
// config fields that are not represented by the runtime settings UI are kept.
func UpdateRuntimeConfig(runtimeName string, incoming RuntimeConfigEditable) (RuntimeConfigState, error) {
	normalized := normalizeRuntimeName(runtimeName)
	if normalized == "" {
		return RuntimeConfigState{}, fmt.Errorf("unsupported runtime %q", runtimeName)
	}
	if normalized == "codex" {
		return RuntimeConfigState{}, fmt.Errorf("%w: codex native config must not be modified by AgentTeam", ErrRuntimeConfigReadOnly)
	}

	current, err := ReadRuntimeConfig(normalized)
	if err != nil {
		return RuntimeConfigState{}, err
	}
	merged := mergeRuntimeConfigEditable(normalized, current.Editable, incoming)

	switch normalized {
	case "opencode":
		err = writeOpenCodeRuntimeConfig(current.ConfigPath, merged)
	case "claude":
		err = writeClaudeRuntimeConfig(current.ConfigPath, merged)
	default:
		err = fmt.Errorf("unsupported runtime %q", runtimeName)
	}
	if err != nil {
		return RuntimeConfigState{}, err
	}
	return ReadRuntimeConfig(normalized)
}

// RuntimeConfigValidationState combines local shape validation with optional
// online provider probing.
type RuntimeConfigValidationState struct {
	Offline RuntimeConfigOfflineResult `json:"offline"`
	Online  RuntimeConfigOnlineResult  `json:"online"`
}

// ValidateRuntimeConfig validates the submitted values without writing them.
// Incoming values are first merged with existing config so blank form fields keep
// the currently configured value.
func ValidateRuntimeConfig(runtimeName string, incoming RuntimeConfigEditable) (RuntimeConfigValidationState, error) {
	normalized := normalizeRuntimeName(runtimeName)
	if normalized == "" {
		return RuntimeConfigValidationState{}, fmt.Errorf("unsupported runtime %q", runtimeName)
	}

	current, err := ReadRuntimeConfig(normalized)
	if err != nil {
		return RuntimeConfigValidationState{}, err
	}

	merged := mergeRuntimeConfigEditable(normalized, current.Editable, incoming)
	offline, prov := offlineValidateRuntimeConfig(normalized, merged)
	result := RuntimeConfigValidationState{Offline: offline}

	if prov == nil {
		prov = runtimeConfigProviderForModelDetection(normalized, merged)
	}
	if prov == nil {
		result.Online = RuntimeConfigOnlineResult{OK: false, Message: offline.Message}
		return result, nil
	}

	resolved := resolveProviderEnvKeyForRuntime(provider.Normalize(*prov))
	models, err := provider.FetchModels(resolved)
	if err != nil {
		result.Online = RuntimeConfigOnlineResult{
			OK:         false,
			Message:    err.Error(),
			ReasonCode: provider.ModelFetchReasonCode(err),
		}
		return result, nil
	}
	result.Online = RuntimeConfigOnlineResult{
		OK:       true,
		Message:  "online validation passed",
		WireAPIs: append([]string(nil), prov.WireAPIs...),
		Models:   models,
	}
	if len(models) == 0 {
		result.Online.Message = "model listing is not available"
		result.Online.ReasonCode = "models_unavailable"
	}
	return result, nil
}

func runtimeConfigProviderForModelDetection(runtimeName string, editable RuntimeConfigEditable) *provider.Provider {
	editable.BaseURL = strings.TrimSpace(editable.BaseURL)
	editable.APIKey = strings.TrimSpace(editable.APIKey)
	editable.APIKeyEnvKey = strings.TrimSpace(editable.APIKeyEnvKey)
	if editable.APIKey == "" && editable.APIKeyEnvKey != "" {
		editable.APIKey = runtimeEnvValue(editable.APIKeyEnvKey)
	}
	if editable.BaseURL == "" || editable.APIKey == "" {
		return nil
	}
	prov := buildRuntimeProviderForValidation(runtimeName, editable)
	return &prov
}

func normalizeRuntimeName(runtimeName string) string {
	rt, err := runtimeconfig.ValidateType(strings.TrimSpace(runtimeName))
	if err != nil {
		return ""
	}
	return string(runtimeconfig.EffectiveRuntimeType(rt))
}

func defaultRuntimeConfigEditable(runtimeName string) RuntimeConfigEditable {
	switch runtimeName {
	case "opencode":
		return RuntimeConfigEditable{
			WireAPI: provider.WireAIOpenAI,
		}
	case "codex":
		return RuntimeConfigEditable{
			WireAPI:      provider.WireAPIResponses,
			APIKeyEnvKey: defaultCodexEnvKey,
		}
	default:
		return RuntimeConfigEditable{}
	}
}

func runtimeConfigPath(runtimeName string) string {
	return getRuntimeConfigPathsFromRuntimes(nil)[runtimeName]
}

func mergeRuntimeConfigEditable(runtimeName string, existing RuntimeConfigEditable, incoming RuntimeConfigEditable) RuntimeConfigEditable {
	merged := existing
	if strings.TrimSpace(incoming.BaseURL) != "" {
		merged.BaseURL = strings.TrimSpace(incoming.BaseURL)
	}
	if strings.TrimSpace(incoming.APIKey) != "" {
		merged.APIKey = strings.TrimSpace(incoming.APIKey)
		merged.APIKeyConfigured = true
	}
	if strings.TrimSpace(incoming.APIKeyEnvKey) != "" {
		merged.APIKeyEnvKey = strings.TrimSpace(incoming.APIKeyEnvKey)
	}
	if strings.TrimSpace(incoming.Model) != "" {
		merged.Model = strings.TrimSpace(incoming.Model)
	}
	if strings.TrimSpace(incoming.ReasoningEffort) != "" {
		merged.ReasoningEffort = strings.TrimSpace(incoming.ReasoningEffort)
	}
	if strings.TrimSpace(incoming.WireAPI) != "" {
		merged.WireAPI = strings.TrimSpace(incoming.WireAPI)
	}

	if runtimeName == "codex" && merged.APIKeyEnvKey == "" {
		merged.APIKeyEnvKey = defaultCodexEnvKey
	}
	return merged
}

func offlineValidateRuntimeConfig(runtimeName string, editable RuntimeConfigEditable) (RuntimeConfigOfflineResult, *provider.Provider) {
	var missing []string
	editable.BaseURL = strings.TrimSpace(editable.BaseURL)
	editable.APIKey = strings.TrimSpace(editable.APIKey)
	editable.APIKeyEnvKey = strings.TrimSpace(editable.APIKeyEnvKey)
	editable.Model = strings.TrimSpace(editable.Model)
	editable.ReasoningEffort = strings.TrimSpace(editable.ReasoningEffort)
	editable.WireAPI = strings.TrimSpace(editable.WireAPI)
	if editable.APIKey == "" && editable.APIKeyEnvKey != "" {
		editable.APIKey = runtimeEnvValue(editable.APIKeyEnvKey)
		editable.APIKeyConfigured = editable.APIKey != ""
	}

	switch runtimeName {
	case "opencode":
		if !editable.APIKeyConfigured && editable.APIKey == "" {
			missing = append(missing, "apiKey")
		}
		if editable.Model == "" {
			missing = append(missing, "model")
		}
	case "claude":
		if !editable.APIKeyConfigured && editable.APIKey == "" {
			missing = append(missing, "apiKey")
		}
	case "codex":
		if !editable.APIKeyConfigured && editable.APIKey == "" {
			missing = append(missing, "apiKey")
		}
		if editable.Model == "" {
			missing = append(missing, "model")
		}
	}

	if len(missing) > 0 {
		return RuntimeConfigOfflineResult{
			OK:      false,
			Message: "missing required runtime config fields",
			Missing: missing,
		}, nil
	}

	prov := buildRuntimeProviderForValidation(runtimeName, editable)
	return RuntimeConfigOfflineResult{
		OK:      true,
		Message: "offline validation passed",
	}, &prov
}

func buildRuntimeProviderForValidation(runtimeName string, editable RuntimeConfigEditable) provider.Provider {
	switch runtimeName {
	case "opencode":
		wireAPI := editable.WireAPI
		if wireAPI == "" {
			wireAPI = provider.WireAIOpenAI
		}
		return provider.Provider{
			ID:       internalRuntimeProviderID(runtimeName),
			Name:     internalRuntimeProviderName(runtimeName),
			Source:   "opencode",
			BaseURL:  editable.BaseURL,
			APIKey:   editable.APIKey,
			WireAPIs: []string{wireAPI},
			Models:   singleModelList(editable.Model),
		}
	case "claude":
		return provider.Provider{
			ID:       "claude/runtime",
			Name:     "Claude Code",
			Source:   "claude",
			BaseURL:  editable.BaseURL,
			APIKey:   editable.APIKey,
			EnvKey:   editable.APIKeyEnvKey,
			WireAPIs: []string{provider.WireAPIAnthropic},
			Models:   singleModelList(editable.Model),
		}
	case "codex":
		wireAPI := editable.WireAPI
		if wireAPI == "" {
			wireAPI = provider.WireAPIResponses
		}
		return provider.Provider{
			ID:       internalRuntimeProviderID(runtimeName),
			Name:     internalRuntimeProviderName(runtimeName),
			Source:   "codex",
			BaseURL:  editable.BaseURL,
			APIKey:   editable.APIKey,
			EnvKey:   editable.APIKeyEnvKey,
			WireAPIs: []string{wireAPI},
			Models:   singleModelList(editable.Model),
		}
	default:
		return provider.Provider{}
	}
}

func singleModelList(model string) []string {
	model = strings.TrimSpace(model)
	if model == "" {
		return nil
	}
	return []string{model}
}

func readOpenCodeRuntimeConfig(path string) (RuntimeConfigEditable, bool, error) {
	editable := defaultRuntimeConfigEditable("opencode")
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return editable, false, nil
		}
		return editable, false, err
	}

	var config openCodeConfig
	if err := json.Unmarshal([]byte(stripJSONComments(string(data))), &config); err != nil {
		return editable, true, fmt.Errorf("parse opencode config: %w", err)
	}

	providerID := internalRuntimeProviderID("opencode")
	model := strings.TrimSpace(config.Model)
	if idx := strings.Index(model, "/"); idx > 0 {
		providerID = model[:idx]
		model = model[idx+1:]
	}

	editable.Model = model
	if providerID == "" && len(config.Provider) == 1 {
		for key := range config.Provider {
			providerID = key
		}
	}
	if prov, ok := config.Provider[providerID]; ok {
		editable.WireAPI = openCodeWireAPIFromProvider(prov)
		if prov.Options != nil {
			editable.BaseURL = strings.TrimSpace(prov.Options.BaseURL)
			editable.APIKey = strings.TrimSpace(prov.Options.APIKey)
			editable.APIKeyConfigured = editable.APIKey != ""
		}
		if editable.Model == "" && len(prov.Models) == 1 {
			for modelID := range prov.Models {
				editable.Model = strings.TrimSpace(modelID)
			}
		}
	}
	return editable, true, nil
}

func openCodeWireAPIFromProvider(prov openCodeProvider) string {
	if prov.Options != nil {
		if wireAPI := normalizeRuntimeWireAPI(prov.Options.WireAPI); wireAPI != "" {
			return wireAPI
		}
	}
	switch strings.TrimSpace(prov.NPM) {
	case "@ai-sdk/anthropic":
		return provider.WireAPIAnthropic
	case "@ai-sdk/openai", "@ai-sdk/azure":
		return provider.WireAPIResponses
	case "@ai-sdk/openai-compatible":
		return provider.WireAIOpenAI
	default:
		if strings.Contains(prov.NPM, "anthropic") {
			return provider.WireAPIAnthropic
		}
		return provider.WireAIOpenAI
	}
}

func normalizeRuntimeWireAPI(wireAPI string) string {
	switch strings.TrimSpace(wireAPI) {
	case provider.WireAIOpenAI:
		return provider.WireAIOpenAI
	case provider.WireAPIResponses:
		return provider.WireAPIResponses
	case provider.WireAPIAnthropic:
		return provider.WireAPIAnthropic
	default:
		return ""
	}
}

func readClaudeRuntimeConfig(path string) (RuntimeConfigEditable, bool, error) {
	editable := defaultRuntimeConfigEditable("claude")
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return editable, false, nil
		}
		return editable, false, err
	}

	var settings claudeSettings
	if err := json.Unmarshal(data, &settings); err != nil {
		return editable, true, fmt.Errorf("parse claude settings: %w", err)
	}

	editable.BaseURL = strings.TrimSpace(rawEnvValue(settings.Env, "ANTHROPIC_BASE_URL"))
	if authToken := strings.TrimSpace(rawEnvValue(settings.Env, "ANTHROPIC_AUTH_TOKEN")); authToken != "" {
		editable.APIKey = authToken
		editable.APIKeyEnvKey = "ANTHROPIC_AUTH_TOKEN"
	} else if apiKey := strings.TrimSpace(rawEnvValue(settings.Env, "ANTHROPIC_API_KEY")); apiKey != "" {
		editable.APIKey = apiKey
		editable.APIKeyEnvKey = "ANTHROPIC_API_KEY"
	}
	editable.APIKeyConfigured = editable.APIKey != ""
	editable.Model = strings.TrimSpace(rawEnvValue(settings.Env, "ANTHROPIC_MODEL"))
	return editable, true, nil
}

func readCodexRuntimeConfig(path string) (RuntimeConfigEditable, bool, error) {
	editable := defaultRuntimeConfigEditable("codex")
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return editable, false, nil
		}
		return editable, false, err
	}

	var config codexConfig
	if err := toml.Unmarshal(data, &config); err != nil {
		return editable, true, fmt.Errorf("parse codex config: %w", err)
	}

	providerID := strings.TrimSpace(config.ModelProvider)
	if providerID == "" && len(config.ModelProviders) == 1 {
		for key := range config.ModelProviders {
			providerID = key
		}
	}
	prov, ok := config.ModelProviders[providerID]
	if ok {
		editable.BaseURL = strings.TrimSpace(prov.BaseURL)
		editable.APIKeyEnvKey = strings.TrimSpace(prov.EnvKey)
		if editable.APIKeyEnvKey != "" {
			editable.APIKey = runtimeEnvValue(editable.APIKeyEnvKey)
			editable.APIKeyConfigured = editable.APIKey != ""
		}
		editable.WireAPI = strings.TrimSpace(prov.WireAPI)
	}
	editable.Model = strings.TrimSpace(config.Model)
	editable.ReasoningEffort = strings.TrimSpace(config.ModelReasoningEffort)
	return editable, true, nil
}

func writeOpenCodeRuntimeConfig(path string, editable RuntimeConfigEditable) error {
	raw := make(map[string]interface{})
	if data, err := os.ReadFile(path); err == nil {
		if err := json.Unmarshal([]byte(stripJSONComments(string(data))), &raw); err != nil {
			return fmt.Errorf("parse opencode config: %w", err)
		}
	} else if !os.IsNotExist(err) {
		return err
	}

	providerID := internalRuntimeProviderID("opencode")
	providers := mapValue(raw["provider"])
	prov := mapValue(providers[providerID])
	options := mapValue(prov["options"])

	if editable.BaseURL != "" {
		options["baseURL"] = editable.BaseURL
	}
	if editable.APIKey != "" {
		options["apiKey"] = editable.APIKey
	}
	if editable.WireAPI != "" {
		options["wireAPI"] = editable.WireAPI
		if npm := openCodeProviderNPMForWireAPI(editable.WireAPI); npm != "" {
			prov["npm"] = npm
		}
	}
	prov["options"] = options
	if _, ok := prov["name"]; !ok {
		prov["name"] = internalRuntimeProviderName("opencode")
	}
	if editable.Model != "" {
		raw["model"] = providerID + "/" + editable.Model
		models := mapValue(prov["models"])
		if _, ok := models[editable.Model]; !ok {
			models[editable.Model] = map[string]interface{}{}
		}
		prov["models"] = models
	}
	providers[providerID] = prov
	raw["provider"] = providers

	data, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}
	return writeRuntimeConfigFile(path, append(data, '\n'))
}

func openCodeProviderNPMForWireAPI(wireAPI string) string {
	switch strings.TrimSpace(wireAPI) {
	case provider.WireAPIAnthropic:
		return "@ai-sdk/anthropic"
	case provider.WireAPIResponses:
		return "@ai-sdk/openai"
	case provider.WireAIOpenAI:
		return "@ai-sdk/openai-compatible"
	default:
		return ""
	}
}

func writeClaudeRuntimeConfig(path string, editable RuntimeConfigEditable) error {
	raw := make(map[string]interface{})
	if data, err := os.ReadFile(path); err == nil {
		if err := json.Unmarshal(data, &raw); err != nil {
			return fmt.Errorf("parse claude settings: %w", err)
		}
	} else if !os.IsNotExist(err) {
		return err
	}

	env := mapValue(raw["env"])
	if editable.BaseURL != "" {
		env["ANTHROPIC_BASE_URL"] = editable.BaseURL
	}
	if editable.APIKey != "" {
		switch strings.TrimSpace(editable.APIKeyEnvKey) {
		case "ANTHROPIC_API_KEY":
			env["ANTHROPIC_API_KEY"] = editable.APIKey
		default:
			env["ANTHROPIC_AUTH_TOKEN"] = editable.APIKey
		}
	}
	if editable.Model != "" {
		env["ANTHROPIC_MODEL"] = editable.Model
	}
	raw["env"] = env

	data, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}
	return writeRuntimeConfigFile(path, append(data, '\n'))
}

func mapValue(value interface{}) map[string]interface{} {
	if typed, ok := value.(map[string]interface{}); ok && typed != nil {
		return typed
	}
	return make(map[string]interface{})
}

func writeRuntimeConfigFile(path string, data []byte) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o600)
}

func internalRuntimeProviderID(runtimeName string) string {
	switch runtimeName {
	case "opencode":
		return "agentteam"
	case "codex":
		return "agentteam"
	default:
		return runtimeName
	}
}

func internalRuntimeProviderName(runtimeName string) string {
	switch runtimeName {
	case "opencode", "codex":
		return "AgentTeam"
	default:
		return runtimeName
	}
}
