package main

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

type MCPServer struct {
	Name               string   `json:"name"`
	URL                string   `json:"url"`
	AuthorizationToken string   `json:"authorization_token,omitempty"`
	AllowedTools       []string `json:"allowed_tools,omitempty"`
}

type WebSearchConfig struct {
	AllowedDomains      []string `json:"allowed_domains,omitempty"`
	BlockedDomains      []string `json:"blocked_domains,omitempty"`
	FetchAllowedDomains []string `json:"fetch_allowed_domains,omitempty"`
	MaxUses             int      `json:"max_uses,omitempty"`
	Fetch               bool     `json:"fetch,omitempty"`
	MaxContentTokens    int      `json:"max_content_tokens,omitempty"`
}

const (
	ThinkingModeAdaptive      = "adaptive"
	ThinkingModeDisabled      = "disabled"
	ThinkingDisplaySummarized = "summarized"
	ThinkingDisplayOmitted    = "omitted"
)

type BotConfig struct {
	ID                string            `json:"id"`
	TelegramToken     string            `json:"telegram_token"`
	MemorySize        int               `json:"memory_size"`
	MessagePerHour    int               `json:"messages_per_hour"`
	MessagePerDay     int               `json:"messages_per_day"`
	TempBanDuration   string            `json:"temp_ban_duration"`
	Model             string            `json:"model"`
	Temperature       *float32          `json:"temperature,omitempty"`
	MaxTokens         int               `json:"max_tokens,omitempty"`
	Thinking          string            `json:"thinking,omitempty"`
	ThinkingDisplay   string            `json:"thinking_display,omitempty"`
	SystemPrompts     map[string]string `json:"system_prompts"`
	Active            bool              `json:"active"`
	OwnerTelegramID   int64             `json:"owner_telegram_id"`
	AnthropicAPIKey   string            `json:"anthropic_api_key"`
	ElevenLabsAPIKey  string            `json:"elevenlabs_api_key"`
	ElevenLabsVoiceID string            `json:"elevenlabs_voice_id"`
	ElevenLabsModel   string            `json:"elevenlabs_model"`
	DebugScreening    bool              `json:"debug_screening"`
	MCPServers        []MCPServer       `json:"mcp_servers,omitempty"`
	WebSearch         *WebSearchConfig  `json:"web_search,omitempty"`
	ConfigFilePath    string            `json:"-"`
}

func validateConfigPath(configDir, filename string) (string, error) {
	configDir = filepath.Clean(configDir)
	filename = filepath.Clean(filename)

	absConfigDir, err := filepath.Abs(configDir)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute path for config directory: %w", err)
	}

	fullPath := filepath.Join(absConfigDir, filename)
	absPath, err := filepath.Abs(fullPath)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute path for config file: %w", err)
	}

	rel, err := filepath.Rel(absConfigDir, absPath)
	if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, "..") {
		return "", fmt.Errorf("invalid config path: file must be within the config directory")
	}

	if filepath.Ext(absPath) != ".json" {
		return "", fmt.Errorf("invalid file extension: must be .json")
	}

	return absPath, nil
}

func loadAllConfigs(dir string) ([]BotConfig, error) {
	var configs []BotConfig
	ids := make(map[string]bool)
	tokens := make(map[string]bool)

	files, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("failed to read config directory: %w", err)
	}

	for _, file := range files {
		if filepath.Ext(file.Name()) == ".json" {
			validPath, err := validateConfigPath(dir, file.Name())
			if err != nil {
				InfoLogger.Printf("Invalid config path for %s: %v", file.Name(), err)
				continue
			}

			config, err := loadConfig(validPath)
			if err != nil {
				InfoLogger.Printf("Failed to load config %s: %v", validPath, err)
				continue
			}

			if !config.Active {
				InfoLogger.Printf("Skipping inactive bot: %s", config.ID)
				continue
			}

			if err := validateConfig(&config, ids, tokens); err != nil {
				InfoLogger.Printf("Config validation failed for %s: %v", validPath, err)
				continue
			}

			if config.Thinking == ThinkingModeAdaptive && config.MaxTokens > 0 && config.MaxTokens < 4000 {
				InfoLogger.Printf("[%s] thinking=adaptive with max_tokens=%d: thinking tokens count toward max_tokens; consider >= 4000",
					config.ID, config.MaxTokens)
			}

			if ws := config.WebSearch; ws != nil && len(ws.AllowedDomains) == 0 && len(ws.BlockedDomains) == 0 {
				InfoLogger.Printf("[%s] web_search enabled with no allowed_domains/blocked_domains: the model may search the open web",
					config.ID)
			}

			if ws := config.WebSearch; ws != nil && len(ws.FetchAllowedDomains) > 0 {
				if !ws.Fetch {
					InfoLogger.Printf("[%s] web_search.fetch_allowed_domains is set but fetch is disabled: it has no effect",
						config.ID)
				}
				for _, d := range ws.FetchAllowedDomains {
					if strings.Contains(d, "/") {
						InfoLogger.Printf("[%s] web_search.fetch_allowed_domains entry %q includes a path: web_fetch matches host-only, so the whole host is fetchable",
							config.ID, d)
					}
				}
			}

			config.ConfigFilePath = validPath
			configs = append(configs, config)
		}
	}

	if len(configs) == 0 {
		return nil, fmt.Errorf("no valid configs found")
	}

	return configs, nil
}

func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
	if config.ID == "" {
		return fmt.Errorf("missing 'id' field")
	}
	if _, exists := ids[config.ID]; exists {
		return fmt.Errorf("duplicate bot id '%s'", config.ID)
	}
	ids[config.ID] = true

	if config.TelegramToken == "" {
		return fmt.Errorf("missing 'telegram_token' field")
	}
	if _, exists := tokens[config.TelegramToken]; exists {
		return fmt.Errorf("duplicate telegram_token")
	}
	tokens[config.TelegramToken] = true

	if config.Model == "" {
		return fmt.Errorf("missing 'model' field")
	}

	switch config.Thinking {
	case "", ThinkingModeAdaptive, ThinkingModeDisabled:
	default:
		return fmt.Errorf("invalid 'thinking' value %q: must be %q or %q (or omitted)",
			config.Thinking, ThinkingModeAdaptive, ThinkingModeDisabled)
	}

	switch config.ThinkingDisplay {
	case "":
	case ThinkingDisplaySummarized, ThinkingDisplayOmitted:
		if config.Thinking != ThinkingModeAdaptive {
			return fmt.Errorf("'thinking_display' requires 'thinking': %q (the API rejects display with thinking disabled)",
				ThinkingModeAdaptive)
		}
	default:
		return fmt.Errorf("invalid 'thinking_display' value %q: must be %q or %q (or omitted)",
			config.ThinkingDisplay, ThinkingDisplaySummarized, ThinkingDisplayOmitted)
	}

	if config.MaxTokens < 0 {
		return fmt.Errorf("'max_tokens' must be greater than 0 when set")
	}

	if ws := config.WebSearch; ws != nil {
		if len(ws.AllowedDomains) > 0 && len(ws.BlockedDomains) > 0 {
			return fmt.Errorf("'web_search' cannot set both allowed_domains and blocked_domains (the API rejects that)")
		}
		if ws.MaxUses < 0 {
			return fmt.Errorf("'web_search.max_uses' must be greater than 0 when set")
		}
		if ws.MaxContentTokens < 0 {
			return fmt.Errorf("'web_search.max_content_tokens' must be greater than 0 when set")
		}
	}

	if config.MessagePerHour <= 0 {
		return fmt.Errorf("'messages_per_hour' must be greater than 0")
	}

	if config.MessagePerDay <= 0 {
		return fmt.Errorf("'messages_per_day' must be greater than 0")
	}

	return nil
}

func loadConfig(filename string) (BotConfig, error) {
	var config BotConfig
	file, err := os.OpenFile(filepath.Clean(filename), os.O_RDONLY, 0)
	if err != nil {
		return config, fmt.Errorf("failed to open config file %s: %w", filename, err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			InfoLogger.Printf("Failed to close config file: %v", err)
		}
	}()

	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&config); err != nil {
		return config, fmt.Errorf("failed to decode JSON from %s: %w", filename, err)
	}

	return config, nil
}

func (c *BotConfig) Reload(configDir, filename string) error {
	validPath, err := validateConfigPath(configDir, filename)
	if err != nil {
		return fmt.Errorf("invalid config path: %w", err)
	}

	cleanPath := filepath.Clean(validPath)
	file, err := os.OpenFile(cleanPath, os.O_RDONLY, 0)
	if err != nil {
		return fmt.Errorf("failed to open config file %s: %w", cleanPath, err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			InfoLogger.Printf("Failed to close config file: %v", err)
		}
	}()

	decoder := json.NewDecoder(file)
	if err := decoder.Decode(c); err != nil {
		return fmt.Errorf("failed to decode JSON from %s: %w", validPath, err)
	}

	return nil
}

func (c *BotConfig) PersistModel(newModel string) error {
	if c.ConfigFilePath == "" {
		return fmt.Errorf("config file path not set; cannot persist model")
	}

	data, err := os.ReadFile(c.ConfigFilePath)
	if err != nil {
		return fmt.Errorf("failed to read config for update: %w", err)
	}

	var raw map[string]any
	if err := json.Unmarshal(data, &raw); err != nil {
		return fmt.Errorf("failed to parse config for update: %w", err)
	}

	raw["model"] = newModel

	updated, err := json.MarshalIndent(raw, "", "\t")
	if err != nil {
		return fmt.Errorf("failed to re-encode config: %w", err)
	}

	if err := os.WriteFile(c.ConfigFilePath, updated, 0600); err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}

	c.Model = newModel
	return nil
}
