package agent

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

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

// openCodeServeConfig is the minimal configuration passed to opencode via the
// OPENCODE_CONFIG_CONTENT environment variable. Field names must match the
// opencode config schema (https://opencode.ai/config.json) exactly.
type openCodeServeConfig struct {
	Model      string                           `json:"model,omitempty"`
	SmallModel string                           `json:"small_model,omitempty"`
	Snapshot   *bool                            `json:"snapshot,omitempty"`
	MCP        map[string]openCodeServeMCP      `json:"mcp,omitempty"`
	Permission *openCodeServePermission         `json:"permission,omitempty"`
	Provider   map[string]openCodeServeProvider `json:"provider,omitempty"`
}

type openCodeServeMCP struct {
	Type        string            `json:"type"`
	URL         string            `json:"url,omitempty"`
	Headers     map[string]string `json:"headers,omitempty"`
	Command     []string          `json:"command,omitempty"`
	Environment map[string]string `json:"environment,omitempty"`
	CWD         string            `json:"cwd,omitempty"`
	Enabled     bool              `json:"enabled"`
}

type openCodeServePermission struct {
	All       string `json:"*,omitempty"`
	Knowledge string `json:"knowledge_*,omitempty"`
}

type openCodeServeProvider struct {
	NPM     string                        `json:"npm,omitempty"`
	Name    string                        `json:"name,omitempty"`
	Options *openCodeServeProviderOptions `json:"options,omitempty"`
	Models  map[string]openCodeServeModel `json:"models,omitempty"`
}

type openCodeServeProviderOptions struct {
	BaseURL string            `json:"baseURL,omitempty"`
	APIKey  string            `json:"apiKey,omitempty"`
	Headers map[string]string `json:"headers,omitempty"`
}

type openCodeServeModel struct {
	Name string `json:"name,omitempty"`
}

const (
	openCodeDatabaseEnv = "OPENCODE_DB"
	openCodeTestHomeEnv = "OPENCODE_TEST_HOME"
)

func isOpenCodeRestrictedLaunch(cfg drivercontract.LaunchConfig) (bool, error) {
	if _, err := cfg.EffectivePermissionMode(); err != nil {
		return false, err
	}
	return cfg.DisableNativeTools, nil
}

func openCodePermissionReplyForLaunch(cfg drivercontract.LaunchConfig) (string, error) {
	restricted, err := isOpenCodeRestrictedLaunch(cfg)
	if err != nil {
		return "", err
	}
	if restricted {
		return openCodePermissionReplyReject, nil
	}
	return openCodePermissionReplyOnce, nil
}

func openCodeMCPServerConfig(m types.NamedMCPConfig) (openCodeServeMCP, bool) {
	switch m.EffectiveTransport() {
	case types.MCPTransportStdio:
		command := strings.TrimSpace(m.Command)
		if command == "" {
			return openCodeServeMCP{}, false
		}
		commandParts := append([]string{command}, m.Args...)
		return openCodeServeMCP{
			Type:        "local",
			Command:     commandParts,
			Environment: runtimeconfig.CloneStringMap(m.Env),
			CWD:         strings.TrimSpace(m.CWD),
			Enabled:     true,
		}, true
	default:
		url := strings.TrimSpace(m.URL)
		if url == "" {
			return openCodeServeMCP{}, false
		}
		return openCodeServeMCP{
			Type:    "remote",
			URL:     url,
			Headers: runtimeconfig.CloneStringMap(m.Headers),
			Enabled: true,
		}, true
	}
}

// buildOpenCodeServeConfigContent generates the minimal opencode config JSON
// for injection via OPENCODE_CONFIG_CONTENT. The generated config follows the
// opencode schema:
//   - model / small_model use "providerID/modelID" format
//   - provider.<id>.models registers the model(s) used
//   - mcp + permission configure non-interactive runtime tool access
func buildOpenCodeServeConfigContent(cfg drivercontract.LaunchConfig) (string, error) {
	config := openCodeServeConfig{}
	if !runtimeconfig.IsBuiltinRuntime(cfg.RuntimeType) {
		config = loadNativeOpenCodeServeConfig()
	}
	// AgentTeam rewind is context-only in this iteration. OpenCode's native revert
	// API uses the snapshot subsystem to roll files back when snapshots are
	// enabled, so AgentTeam-managed OpenCode launches disable snapshot capture. This
	// keeps /session/:id/revert a native runtime transcript rewind without
	// hidden workspace file side effects.
	snapshotEnabled := false
	config.Snapshot = &snapshotEnabled

	// MCP servers.
	if len(cfg.MCPs) > 0 {
		mcpMap := make(map[string]openCodeServeMCP, len(cfg.MCPs))
		for _, m := range cfg.MCPs {
			if server, ok := openCodeMCPServerConfig(m); ok {
				mcpMap[m.Name] = server
			}
		}
		if len(mcpMap) > 0 {
			config.MCP = mcpMap
		}
	}

	restricted, err := isOpenCodeRestrictedLaunch(cfg)
	if err != nil {
		return "", err
	}
	if restricted {
		// OpenCode permission patterns cover both built-in and MCP tools. Deny
		// everything, then open only this session's Knowledge MCP namespace.
		config.Permission = &openCodeServePermission{
			All:       "deny",
			Knowledge: "allow",
		}
	} else {
		// AgentTeam's full-access launch policy is non-interactive. Configure the
		// upstream runtime to allow all permissions so the normal path never
		// pauses on a headless permission prompt.
		config.Permission = &openCodeServePermission{
			All: "allow",
		}
	}

	// Provider and model configuration.
	providerID := currentOpenCodeProviderID(config)
	if cfg.Provider != nil && strings.TrimSpace(cfg.Provider.BaseURL) != "" {
		providerID = cfg.Provider.ID

		// Determine the npm package based on the wire API. OpenCode routes
		// @ai-sdk/openai through its Responses adapter and
		// @ai-sdk/openai-compatible through Chat Completions.
		npm := openCodeProviderNPMForWireAPIs(cfg.Provider.WireAPIs)

		models := make(map[string]openCodeServeModel)

		// Register the primary model.
		model := normalizeOpenCodeModelForProvider(cfg.Model, providerID)
		if model != "" {
			models[model] = openCodeServeModel{Name: model}
			config.Model = providerID + "/" + model
		}

		// Register the small model.
		smallModel := normalizeOpenCodeModelForProvider(cfg.SmallModel, providerID)
		if smallModel != "" {
			models[smallModel] = openCodeServeModel{Name: smallModel}
			config.SmallModel = providerID + "/" + smallModel
		}

		if config.Provider == nil {
			config.Provider = map[string]openCodeServeProvider{}
		}
		config.Provider[providerID] = openCodeServeProvider{
			NPM:  npm,
			Name: cfg.Provider.Name,
			Options: &openCodeServeProviderOptions{
				BaseURL: cfg.Provider.BaseURL,
				APIKey:  cfg.Provider.APIKey,
				Headers: cfg.Provider.Headers,
			},
			Models: models,
		}
	} else if strings.TrimSpace(providerID) != "" {
		if model := normalizeOpenCodeModelForProvider(cfg.Model, providerID); model != "" {
			config.Model = providerID + "/" + model
			config = ensureOpenCodeServeModel(config, providerID, model)
		}
		if smallModel := normalizeOpenCodeModelForProvider(cfg.SmallModel, providerID); smallModel != "" {
			config.SmallModel = providerID + "/" + smallModel
			config = ensureOpenCodeServeModel(config, providerID, smallModel)
		}
	}

	data, _ := json.Marshal(config)
	return string(data), nil
}

func openCodeProviderNPMForWireAPIs(wireAPIs []string) string {
	if len(wireAPIs) == 0 {
		return "@ai-sdk/openai-compatible"
	}
	switch strings.TrimSpace(wireAPIs[0]) {
	case provider.WireAPIAnthropic:
		return "@ai-sdk/anthropic"
	case provider.WireAPIResponses:
		return "@ai-sdk/openai"
	case provider.WireAIOpenAI:
		return "@ai-sdk/openai-compatible"
	default:
		return "@ai-sdk/openai-compatible"
	}
}

func loadNativeOpenCodeServeConfig() openCodeServeConfig {
	homeDir, err := os.UserHomeDir()
	if err != nil || strings.TrimSpace(homeDir) == "" {
		return openCodeServeConfig{}
	}

	configDir := filepath.Join(homeDir, ".config", "opencode")
	for _, name := range []string{"opencode.json", "opencode.jsonc"} {
		data, err := os.ReadFile(filepath.Join(configDir, name))
		if err != nil {
			continue
		}
		var config openCodeServeConfig
		if err := json.Unmarshal([]byte(stripOpenCodeJSONComments(string(data))), &config); err == nil {
			return config
		}
	}

	return openCodeServeConfig{}
}

func currentOpenCodeProviderID(config openCodeServeConfig) string {
	if parsed := parseOpenCodeModel(strings.TrimSpace(config.Model), ""); parsed != nil {
		return strings.TrimSpace(parsed["providerID"])
	}
	if len(config.Provider) == 1 {
		for providerID := range config.Provider {
			return strings.TrimSpace(providerID)
		}
	}
	return ""
}

func ensureOpenCodeServeModel(config openCodeServeConfig, providerID, model string) openCodeServeConfig {
	providerID = strings.TrimSpace(providerID)
	model = strings.TrimSpace(model)
	if providerID == "" || model == "" {
		return config
	}
	if config.Provider == nil {
		return config
	}
	prov, ok := config.Provider[providerID]
	if !ok {
		return config
	}
	if prov.Models == nil {
		prov.Models = map[string]openCodeServeModel{}
	}
	prov.Models[model] = openCodeServeModel{Name: model}
	config.Provider[providerID] = prov
	return config
}

func stripOpenCodeJSONComments(s string) string {
	var result strings.Builder
	result.Grow(len(s))

	i := 0
	inString := false
	escape := false

	for i < len(s) {
		ch := s[i]

		if escape {
			result.WriteByte(ch)
			escape = false
			i++
			continue
		}
		if ch == '\\' && inString {
			result.WriteByte(ch)
			escape = true
			i++
			continue
		}
		if ch == '"' {
			inString = !inString
			result.WriteByte(ch)
			i++
			continue
		}
		if inString {
			result.WriteByte(ch)
			i++
			continue
		}
		if ch == '/' && i+1 < len(s) {
			if s[i+1] == '/' {
				for i < len(s) && s[i] != '\n' {
					i++
				}
				continue
			}
			if s[i+1] == '*' {
				i += 2
				for i+1 < len(s) && !(s[i] == '*' && s[i+1] == '/') {
					i++
				}
				if i+1 < len(s) {
					i += 2
				}
				continue
			}
		}
		result.WriteByte(ch)
		i++
	}

	return result.String()
}

func buildOpenCodeServeEnv(cfg drivercontract.LaunchConfig, password string) ([]string, error) {
	modelsPath := openCodeManagedModelsPath(cfg)
	isBuiltin := runtimeconfig.IsBuiltinRuntime(cfg.RuntimeType)
	var managedEnv map[string]string
	if isBuiltin {
		var err error
		managedEnv, err = runtimeconfig.PrepareManagedOpenCodeEnv(modelsPath, cfg.RuntimeStateDir)
		if err != nil {
			return nil, err
		}
	}

	configDir, err := ensureOpenCodeConfigDir(cfg.RuntimeType, cfg.WorkDir)
	if err != nil {
		return nil, err
	}
	runtimeEnv := runtimeconfig.RuntimeEnvOverrides(cfg.Env, cfg.MCPs)
	env := buildRuntimeEnvWithProvider(runtimeEnv, cfg.Provider)
	configContent, err := buildOpenCodeServeConfigContent(cfg)
	if err != nil {
		return nil, err
	}
	if isBuiltin {
		managedEnv["OPENCODE_CONFIG_DIR"] = configDir
		managedEnv["OPENCODE_SERVER_USERNAME"] = openCodeServerUsername
		managedEnv["OPENCODE_SERVER_PASSWORD"] = password
		managedEnv["OPENCODE_CONFIG_CONTENT"] = configContent
		return applyManagedOpenCodeEnv(env, managedEnv), nil
	}

	env = append(env,
		"OPENCODE_CONFIG_DIR="+configDir,
		"OPENCODE_SERVER_USERNAME="+openCodeServerUsername,
		"OPENCODE_SERVER_PASSWORD="+password,
		"OPENCODE_CONFIG_CONTENT="+configContent,
		"OPENCODE_DISABLE_AUTOUPDATE=1",
		"OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1",
		"OPENCODE_DISABLE_LSP_DOWNLOAD=1",
		"OPENCODE_DISABLE_MODELS_FETCH=1",
	)
	return env, nil
}

func applyManagedOpenCodeEnv(env []string, mandatory map[string]string) []string {
	managedKeys := make(map[string]struct{}, len(mandatory)+2)
	managedKeys[openCodeDatabaseEnv] = struct{}{}
	managedKeys[openCodeTestHomeEnv] = struct{}{}
	sortedKeys := make([]string, 0, len(mandatory))
	for key := range mandatory {
		sortedKeys = append(sortedKeys, key)
		managedKeys[strings.ToUpper(key)] = struct{}{}
	}
	sort.Strings(sortedKeys)

	filtered := make([]string, 0, len(env)+len(mandatory))
	for _, entry := range env {
		key, _, ok := strings.Cut(entry, "=")
		if ok {
			if _, managed := managedKeys[strings.ToUpper(strings.TrimSpace(key))]; managed {
				continue
			}
		}
		filtered = append(filtered, entry)
	}
	for _, key := range sortedKeys {
		filtered = append(filtered, key+"="+mandatory[key])
	}
	return filtered
}

func ensureOpenCodeConfigDir(rt runtimeconfig.RuntimeType, workDir string) (string, error) {
	trimmedWorkDir := strings.TrimSpace(workDir)
	if trimmedWorkDir == "" {
		return "", fmt.Errorf("empty workdir")
	}
	configDir := filepath.Join(trimmedWorkDir, ".opencode")
	if runtimeconfig.IsBuiltinRuntime(rt) {
		configDir = runtimeconfig.ManagedOpenCodeConfigDir(trimmedWorkDir)
	}
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		return "", fmt.Errorf("create opencode config dir %s: %w", configDir, err)
	}
	return configDir, nil
}

func openCodeManagedModelsPath(cfg drivercontract.LaunchConfig) string {
	if !runtimeconfig.IsBuiltinRuntime(cfg.RuntimeType) {
		return ""
	}
	binary := strings.TrimSpace(cfg.Binary)
	if binary == "" {
		return ""
	}
	return filepath.Join(filepath.Dir(binary), "models.json")
}

// parseOpenCodeModel parses a model string into the opencode PromptInput model
// format: { "providerID": "...", "modelID": "..." }.
//
// Both providerID and modelID are required by the opencode schema when the
// model object is present. Returns nil if providerID cannot be determined.
//
// Resolution order:
//  1. If providerID is provided (non-empty), use it directly with model as modelID.
//  2. If the model string contains "/", split as "providerID/modelID".
//  3. If the model string contains ":", split as "providerID:modelID".
//  4. Otherwise, return nil (caller should omit the model field).
func normalizeOpenCodeModelForProvider(model, providerID string) string {
	trimmedModel := strings.TrimSpace(model)
	trimmedProviderID := strings.TrimSpace(providerID)
	if trimmedModel == "" || trimmedProviderID == "" {
		return trimmedModel
	}
	parsed := parseOpenCodeModel(trimmedModel, "")
	if parsed == nil {
		return trimmedModel
	}
	parsedProviderID := strings.TrimSpace(parsed["providerID"])
	parsedModelID := strings.TrimSpace(parsed["modelID"])
	if parsedProviderID == "" || parsedModelID == "" {
		return trimmedModel
	}
	if parsedProviderID == trimmedProviderID {
		return parsedModelID
	}
	if strings.Contains(parsedModelID, "/") || strings.Contains(parsedModelID, ":") {
		return parsedModelID
	}
	return trimmedModel
}

func parseOpenCodeModel(model, providerID string) map[string]string {
	if strings.TrimSpace(providerID) != "" {
		return map[string]string{
			"providerID": providerID,
			"modelID":    model,
		}
	}
	parts := strings.SplitN(model, "/", 2)
	if len(parts) == 2 {
		return map[string]string{
			"providerID": parts[0],
			"modelID":    parts[1],
		}
	}
	parts = strings.SplitN(model, ":", 2)
	if len(parts) == 2 {
		return map[string]string{
			"providerID": parts[0],
			"modelID":    parts[1],
		}
	}
	return nil
}
