// Package config provides integration configuration for external coding tools
// (Claude Code, Codex, Droid, OpenCode) to use Ollama models.
package config

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

	"github.com/ollama/ollama/cmd/internal/fileutil"
)

type integration struct {
	Models    []string          `json:"models"`
	Aliases   map[string]string `json:"aliases,omitempty"`
	Onboarded bool              `json:"onboarded,omitempty"`
}

// IntegrationConfig is the persisted config for one integration.
type IntegrationConfig = integration

type config struct {
	Integrations  map[string]*integration `json:"integrations"`
	LastModel     string                  `json:"last_model,omitempty"`
	LastSelection string                  `json:"last_selection,omitempty"` // "run" or integration name
}

func configPath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".ollama", "config.json"), nil
}

func legacyConfigPath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".ollama", "config", "config.json"), nil
}

// migrateConfig moves the config from the legacy path to ~/.ollama/config.json
func migrateConfig() (bool, error) {
	oldPath, err := legacyConfigPath()
	if err != nil {
		return false, err
	}

	oldData, err := os.ReadFile(oldPath)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, err
	}

	// Ignore legacy files with invalid JSON and continue startup.
	if !json.Valid(oldData) {
		return false, nil
	}

	newPath, err := configPath()
	if err != nil {
		return false, err
	}

	if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
		return false, err
	}
	if err := os.WriteFile(newPath, oldData, 0o644); err != nil {
		return false, fmt.Errorf("write new config: %w", err)
	}

	_ = os.Remove(oldPath)
	_ = os.Remove(filepath.Dir(oldPath)) // clean up empty directory

	return true, nil
}

func load() (*config, error) {
	path, err := configPath()
	if err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil && os.IsNotExist(err) {
		if migrated, merr := migrateConfig(); merr == nil && migrated {
			data, err = os.ReadFile(path)
		}
	}
	if err != nil {
		if os.IsNotExist(err) {
			return &config{Integrations: make(map[string]*integration)}, nil
		}
		return nil, err
	}

	var cfg config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("failed to parse config: %w, at: %s", err, path)
	}
	if cfg.Integrations == nil {
		cfg.Integrations = make(map[string]*integration)
	}
	return &cfg, nil
}

func save(cfg *config) error {
	path, err := configPath()
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}

	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}

	return fileutil.WriteWithBackup(path, data)
}

func SaveIntegration(appName string, models []string) error {
	if appName == "" {
		return errors.New("app name cannot be empty")
	}

	cfg, err := load()
	if err != nil {
		return err
	}

	key := strings.ToLower(appName)
	existing := cfg.Integrations[key]
	var aliases map[string]string
	var onboarded bool
	if existing != nil {
		aliases = existing.Aliases
		onboarded = existing.Onboarded
	}

	cfg.Integrations[key] = &integration{
		Models:    models,
		Aliases:   aliases,
		Onboarded: onboarded,
	}

	return save(cfg)
}

// MarkIntegrationOnboarded marks an integration as onboarded in Ollama's config.
func MarkIntegrationOnboarded(appName string) error {
	cfg, err := load()
	if err != nil {
		return err
	}

	key := strings.ToLower(appName)
	existing := cfg.Integrations[key]
	if existing == nil {
		existing = &integration{}
	}
	existing.Onboarded = true
	cfg.Integrations[key] = existing
	return save(cfg)
}

// IntegrationModel returns the first configured model for an integration, or empty string if not configured.
func IntegrationModel(appName string) string {
	integrationConfig, err := LoadIntegration(appName)
	if err != nil || len(integrationConfig.Models) == 0 {
		return ""
	}
	return integrationConfig.Models[0]
}

// IntegrationModels returns all configured models for an integration, or nil.
func IntegrationModels(appName string) []string {
	integrationConfig, err := LoadIntegration(appName)
	if err != nil || len(integrationConfig.Models) == 0 {
		return nil
	}
	return integrationConfig.Models
}

// LastModel returns the last model that was run, or empty string if none.
func LastModel() string {
	cfg, err := load()
	if err != nil {
		return ""
	}
	return cfg.LastModel
}

// SetLastModel saves the last model that was run.
func SetLastModel(model string) error {
	cfg, err := load()
	if err != nil {
		return err
	}
	cfg.LastModel = model
	return save(cfg)
}

// LastSelection returns the last menu selection ("run" or integration name), or empty string if none.
func LastSelection() string {
	cfg, err := load()
	if err != nil {
		return ""
	}
	return cfg.LastSelection
}

// SetLastSelection saves the last menu selection ("run" or integration name).
func SetLastSelection(selection string) error {
	cfg, err := load()
	if err != nil {
		return err
	}
	cfg.LastSelection = selection
	return save(cfg)
}

// LoadIntegration returns the saved config for one integration.
func LoadIntegration(appName string) (*integration, error) {
	cfg, err := load()
	if err != nil {
		return nil, err
	}

	integrationConfig, ok := cfg.Integrations[strings.ToLower(appName)]
	if !ok {
		return nil, os.ErrNotExist
	}

	return integrationConfig, nil
}

// SaveAliases replaces the saved aliases for one integration.
func SaveAliases(appName string, aliases map[string]string) error {
	if appName == "" {
		return errors.New("app name cannot be empty")
	}

	cfg, err := load()
	if err != nil {
		return err
	}

	key := strings.ToLower(appName)
	existing := cfg.Integrations[key]
	if existing == nil {
		existing = &integration{}
	}

	// Replace aliases entirely (not merge) so deletions are persisted
	existing.Aliases = aliases

	cfg.Integrations[key] = existing
	return save(cfg)
}

func listIntegrations() ([]integration, error) {
	cfg, err := load()
	if err != nil {
		return nil, err
	}

	result := make([]integration, 0, len(cfg.Integrations))
	for _, integrationConfig := range cfg.Integrations {
		result = append(result, *integrationConfig)
	}

	return result, nil
}
