// Package domain — configuration types.
//
// S3Config and SSHConfig are intentionally split into their own structs so
// that future providers can introduce their own configuration types side-
// by-side without polluting the core domain types file.
package domain

// S3Config describes the connection parameters for any S3-compatible
// endpoint (AWS S3, MinIO, Cloudflare R2, Backblaze B2, Aliyun OSS S3
// endpoint, etc.).
//
// Field design notes:
//   - PathPrefix supports object name templating so users can group screenshots
//     by date.
//   - PublicURLBase is optional to support CDN-fronted buckets; otherwise the
//     adapter falls back to "{Endpoint}/{Bucket}/{Key}" (path-style).
//   - UsePathStyle defaults to true because many self-hosted MinIO / R2
//     deployments do not support virtual-hosted-style addressing.
type S3Config struct {
	Endpoint        string `json:"endpoint"`
	Region          string `json:"region"`
	Bucket          string `json:"bucket"`
	AccessKeyID     string `json:"accessKeyId"`
	SecretAccessKey string `json:"secretAccessKey"`
	PathPrefix      string `json:"pathPrefix"`
	PublicURLBase   string `json:"publicUrlBase"`
	UsePathStyle    bool   `json:"usePathStyle"`
}

// SSHConfig describes the parameters required to upload a screenshot via
// SSH/SCP to a remote host.
//
// Field design notes:
//   - Host / Port form the destination endpoint. Port defaults to 22 when 0.
//   - User is the SSH login name.
//   - AuthMethod selects how the connection authenticates:
//     "password"     → password only, via the in-process Go SSH client.
//     "key"          → ssh-agent + ~/.ssh/id_* key files (no password),
//     via the in-process Go SSH client.
//     "kerberos"     → delegate to the system /usr/bin/ssh binary so the
//     existing Kerberos (GSSAPI) credential cache from
//     `kinit` is reused. Native GSSAPI is required here
//     because macOS stores tickets in an API: ccache
//     that pure-Go SSH libraries cannot read.
//     "" / "builtin" → legacy combined behaviour (password → agent → key
//     files) kept only for backward compatibility with
//     configs written before the method was split.
//   - Password is optional; when empty the implementation falls back to the
//     local SSH agent or ~/.ssh/id_* keys (i.e. the user is responsible for
//     having password-less access already configured on this machine).
//     It is ignored entirely when AuthMethod is "kerberos".
//   - PathPrefix is interpreted *relative to the remote user's $HOME*. The
//     UI presents it as "~/<PathPrefix>" so the user cannot escape the home
//     directory by writing absolute paths. Leading "/" or "~" markers are
//     stripped before the path is used.
//   - StrictHostKey toggles host key verification. When false the client
//     uses ssh.InsecureIgnoreHostKey() to mimic `scp -o StrictHostKeyChecking=no`,
//     trading some security for first-launch usability on personal LANs.
//   - KnownHostsPath defaults to ~/.ssh/known_hosts when empty and is only
//     used while StrictHostKey is true.
//   - ConnectTimeoutSecs caps the network handshake duration so a wrong
//     endpoint does not hang the capture flow indefinitely.
type SSHConfig struct {
	Host               string `json:"host"`
	Port               int    `json:"port"`
	User               string `json:"user"`
	AuthMethod         string `json:"authMethod"`
	Password           string `json:"password"`
	PathPrefix         string `json:"pathPrefix"`
	StrictHostKey      bool   `json:"strictHostKey"`
	KnownHostsPath     string `json:"knownHostsPath"`
	ConnectTimeoutSecs int    `json:"connectTimeoutSecs"`
}

// LLMProviderConfig stores one OpenAI-compatible multimodal chat endpoint.
//
// The three built-in providers (Qwen, Doubao/Ark, OpenAI-compatible) all use
// the same request shape: chat/completions with a user content array that
// contains text plus an image_url item. Keeping them as provider records lets
// users switch between credentials/models without retyping every field.
type LLMProviderConfig struct {
	Label       string  `json:"label"`
	BaseURL     string  `json:"baseUrl"`
	APIKey      string  `json:"apiKey"`
	Model       string  `json:"model"`
	MaxTokens   int     `json:"maxTokens"`
	Temperature float64 `json:"temperature"`
	TimeoutSecs int     `json:"timeoutSecs"`
	// MaxInlineBytes caps the screenshot size (in bytes of the PNG payload)
	// that may be sent inline as a base64 data URL. Above this threshold the
	// summary pipeline falls back to uploading the image to S3 and passing a
	// public URL instead, because most providers reject oversized inline
	// images. A value <= 0 means "use the built-in default".
	MaxInlineBytes int `json:"maxInlineBytes"`
}

// DefaultMaxInlineBytes is the fallback inline-image cap (~4 MiB) used when a
// provider config does not specify MaxInlineBytes. It is a conservative bound
// that keeps a single base64 data URL within the request-size limits accepted
// by the common OpenAI-compatible multimodal endpoints.
const DefaultMaxInlineBytes = 4 << 20

// LLMConfig controls screenshot summarisation.
type LLMConfig struct {
	ActiveProvider string                       `json:"activeProvider"`
	Prompt         string                       `json:"prompt"`
	Providers      map[string]LLMProviderConfig `json:"providers"`
}

// OCRProviderConfig stores one cloud OCR endpoint that accepts a screenshot
// image and returns extracted text. Aliyun and Volcengine both authenticate
// with an AccessKey pair, but their signing schemes use slightly different
// endpoint metadata; keeping Region/Service/Action/Version configurable lets
// the presets track official API changes without a schema rewrite.
type OCRProviderConfig struct {
	Label           string `json:"label"`
	Endpoint        string `json:"endpoint"`
	AccessKeyID     string `json:"accessKeyId"`
	AccessKeySecret string `json:"accessKeySecret"`
	Region          string `json:"region"`
	Service         string `json:"service"`
	Action          string `json:"action"`
	Version         string `json:"version"`
	TimeoutSecs     int    `json:"timeoutSecs"`
}

// OCRConfig controls screenshot text extraction.
type OCRConfig struct {
	ActiveProvider string                       `json:"activeProvider"`
	Providers      map[string]OCRProviderConfig `json:"providers"`
}

// SSH authentication method identifiers stored in SSHConfig.AuthMethod.
const (
	// SSHAuthBuiltin is the legacy combined method (password → agent → key
	// files). Retained for backward compatibility with older config files;
	// new configs use the explicit methods below.
	SSHAuthBuiltin = "builtin"
	// SSHAuthPassword authenticates with the password field only.
	SSHAuthPassword = "password"
	// SSHAuthKey authenticates with ssh-agent / ~/.ssh/id_* key files only.
	SSHAuthKey = "key"
	// SSHAuthKerberos delegates to the system ssh binary so an existing
	// Kerberos credential cache (populated by `kinit`) is reused via GSSAPI.
	SSHAuthKerberos = "kerberos"
)

// Built-in LLM provider identifiers.
const (
	LLMProviderQwen   = "qwen"
	LLMProviderDoubao = "doubao"
	LLMProviderOpenAI = "openai"
)

// Built-in OCR provider identifiers.
const (
	OCRProviderAliyun     = "aliyun"
	OCRProviderVolcengine = "volcengine"
)

// Theme preference identifiers stored in AppConfig.Theme.
const (
	ThemeAuto  = "auto"
	ThemeLight = "light"
	ThemeDark  = "dark"
)

const DefaultSummaryPrompt = `你是一个截图内容总结助手。请阅读截图，并用中文输出一段适合直接粘贴到聊天、工单、Issue 或 PR 中的说明。

要求：
- 先说明截图中最重要的信息和用户可能想表达的意图。
- 如果截图包含报错、异常状态、表格、代码、界面控件或关键数字，请准确提取。
- 如果截图内容不足以判断，不要编造；直接说明不确定点。
- 输出尽量简洁，默认 3 到 6 条要点。
- 不要输出 Markdown 标题，不要寒暄。`

// IsKerberos reports whether this config requests Kerberos/GSSAPI auth.
func (c SSHConfig) IsKerberos() bool {
	return c.AuthMethod == SSHAuthKerberos
}

// AppConfig is the top-level on-disk configuration document.
//
// We keep S3 / SSH nested so that adding more providers later (e.g. AliyunOSS,
// COS, FTP) only requires a new sibling field rather than a schema rewrite.
type AppConfig struct {
	// Hotkey describes the global shortcut that triggers a capture.
	// Stored as a human-readable string like "cmd+shift+a"; parsing happens
	// in the infrastructure hotkey adapter.
	Hotkey string `json:"hotkey"`

	// Theme controls the settings UI appearance: "auto", "light", or "dark".
	Theme string `json:"theme"`

	// S3 holds the active S3-compatible storage configuration.
	S3 S3Config `json:"s3"`

	// SSH holds the configuration for the optional "save to remote via scp"
	// destination triggered by the save-remote toolbar button.
	SSH SSHConfig `json:"ssh"`

	// LLM holds provider settings for the "copy summary" screenshot action.
	LLM LLMConfig `json:"llm"`

	// OCR holds provider settings for the "extract text" screenshot action.
	OCR OCRConfig `json:"ocr"`
}

// DefaultAppConfig returns sane zero-value defaults used on first launch.
func DefaultAppConfig() AppConfig {
	cfg := AppConfig{
		Hotkey: "cmd+shift+a",
		Theme:  ThemeAuto,
		S3: S3Config{
			PathPrefix:   "snapgo/",
			UsePathStyle: true,
		},
		SSH: SSHConfig{
			Port:               22,
			PathPrefix:         "snapgo/",
			ConnectTimeoutSecs: 10,
			StrictHostKey:      false,
		},
		LLM: DefaultLLMConfig(),
		OCR: DefaultOCRConfig(),
	}
	cfg.Normalize()
	return cfg
}

// DefaultLLMConfig returns the built-in provider presets. API keys and exact
// models remain user-editable because each provider account may expose
// different model names / endpoint IDs.
func DefaultLLMConfig() LLMConfig {
	return LLMConfig{
		ActiveProvider: LLMProviderOpenAI,
		Prompt:         DefaultSummaryPrompt,
		Providers: map[string]LLMProviderConfig{
			LLMProviderQwen: {
				Label:          "阿里千问多模态",
				BaseURL:        "https://dashscope.aliyuncs.com/compatible-mode/v1",
				Model:          "qwen-vl-plus",
				MaxTokens:      600,
				Temperature:    0.2,
				TimeoutSecs:    60,
				MaxInlineBytes: DefaultMaxInlineBytes,
			},
			LLMProviderDoubao: {
				Label:          "火山方舟豆包多模态",
				BaseURL:        "https://ark.cn-beijing.volces.com/api/v3",
				Model:          "",
				MaxTokens:      600,
				Temperature:    0.2,
				TimeoutSecs:    60,
				MaxInlineBytes: DefaultMaxInlineBytes,
			},
			LLMProviderOpenAI: {
				Label:          "ChatGPT / OpenAI-compatible",
				BaseURL:        "https://api.openai.com/v1",
				Model:          "gpt-5.5",
				MaxTokens:      600,
				Temperature:    0.2,
				TimeoutSecs:    60,
				MaxInlineBytes: DefaultMaxInlineBytes,
			},
		},
	}
}

// DefaultOCRConfig returns the built-in OCR provider presets. Users only need
// to fill AccessKey credentials for the common regions/actions, while advanced
// accounts can still override endpoint metadata from the settings UI.
func DefaultOCRConfig() OCRConfig {
	return OCRConfig{
		ActiveProvider: OCRProviderAliyun,
		Providers: map[string]OCRProviderConfig{
			OCRProviderAliyun: {
				Label:       "阿里云文字识别",
				Endpoint:    "https://ocr-api.cn-hangzhou.aliyuncs.com",
				Action:      "RecognizeGeneral",
				Version:     "2021-07-07",
				TimeoutSecs: 30,
			},
			OCRProviderVolcengine: {
				Label:       "火山引擎文字识别",
				Endpoint:    "https://visual.volcengineapi.com",
				Region:      "cn-north-1",
				Service:     "cv",
				Action:      "OCRNormal",
				Version:     "2020-08-26",
				TimeoutSecs: 30,
			},
		},
	}
}

// Normalize fills defaults into configs written by older app versions while
// preserving any user-provided provider fields.
func (c *AppConfig) Normalize() {
	if c.Hotkey == "" {
		c.Hotkey = "cmd+shift+a"
	}
	if c.Theme != ThemeLight && c.Theme != ThemeDark && c.Theme != ThemeAuto {
		c.Theme = ThemeAuto
	}
	if c.S3.PathPrefix == "" {
		c.S3.PathPrefix = "snapgo/"
	}
	if c.SSH.Port == 0 {
		c.SSH.Port = 22
	}
	if c.SSH.PathPrefix == "" {
		c.SSH.PathPrefix = "snapgo/"
	}
	if c.SSH.ConnectTimeoutSecs == 0 {
		c.SSH.ConnectTimeoutSecs = 10
	}

	defaultLLM := DefaultLLMConfig()
	if c.LLM.ActiveProvider == "" {
		c.LLM.ActiveProvider = defaultLLM.ActiveProvider
	}
	if c.LLM.Prompt == "" {
		c.LLM.Prompt = defaultLLM.Prompt
	}
	if c.LLM.Providers == nil {
		c.LLM.Providers = map[string]LLMProviderConfig{}
	}
	for id, def := range defaultLLM.Providers {
		current, ok := c.LLM.Providers[id]
		if !ok {
			c.LLM.Providers[id] = def
			continue
		}
		if current.Label == "" {
			current.Label = def.Label
		}
		if current.BaseURL == "" {
			current.BaseURL = def.BaseURL
		}
		if current.Model == "" && id != LLMProviderDoubao {
			current.Model = def.Model
		}
		if current.MaxTokens == 0 {
			current.MaxTokens = def.MaxTokens
		}
		if current.Temperature < 0 {
			current.Temperature = def.Temperature
		}
		if current.TimeoutSecs == 0 {
			current.TimeoutSecs = def.TimeoutSecs
		}
		if current.MaxInlineBytes <= 0 {
			current.MaxInlineBytes = DefaultMaxInlineBytes
		}
		c.LLM.Providers[id] = current
	}

	defaultOCR := DefaultOCRConfig()
	if c.OCR.ActiveProvider == "" {
		c.OCR.ActiveProvider = defaultOCR.ActiveProvider
	}
	if c.OCR.Providers == nil {
		c.OCR.Providers = map[string]OCRProviderConfig{}
	}
	for id, def := range defaultOCR.Providers {
		current, ok := c.OCR.Providers[id]
		if !ok {
			c.OCR.Providers[id] = def
			continue
		}
		if current.Label == "" {
			current.Label = def.Label
		}
		if current.Endpoint == "" {
			current.Endpoint = def.Endpoint
		}
		if current.Region == "" {
			current.Region = def.Region
		}
		if current.Service == "" {
			current.Service = def.Service
		}
		if current.Action == "" {
			current.Action = def.Action
		}
		if current.Version == "" {
			current.Version = def.Version
		}
		if current.TimeoutSecs == 0 {
			current.TimeoutSecs = def.TimeoutSecs
		}
		c.OCR.Providers[id] = current
	}
}

// IsS3Configured reports whether the user has filled the mandatory S3 fields.
func (c AppConfig) IsS3Configured() bool {
	return c.S3.Endpoint != "" &&
		c.S3.Bucket != "" &&
		c.S3.AccessKeyID != "" &&
		c.S3.SecretAccessKey != ""
}

// IsSSHConfigured reports whether the SSH destination has the minimum
// fields required to attempt a connection. Password is intentionally NOT
// checked because empty password means "use agent / key auth".
func (c AppConfig) IsSSHConfigured() bool {
	return c.SSH.Host != "" && c.SSH.User != ""
}

// ActiveLLMProvider returns the selected provider config plus a boolean
// indicating whether the selection exists.
func (c AppConfig) ActiveLLMProvider() (string, LLMProviderConfig, bool) {
	id := c.LLM.ActiveProvider
	if id == "" {
		id = LLMProviderOpenAI
	}
	provider, ok := c.LLM.Providers[id]
	return id, provider, ok
}

// IsLLMConfigured reports whether the selected multimodal provider has the
// fields required to make a request.
func (c AppConfig) IsLLMConfigured() bool {
	_, provider, ok := c.ActiveLLMProvider()
	return ok && provider.BaseURL != "" && provider.APIKey != "" && provider.Model != ""
}

// ActiveOCRProvider returns the selected OCR provider config plus a boolean
// indicating whether the selection exists.
func (c AppConfig) ActiveOCRProvider() (string, OCRProviderConfig, bool) {
	id := c.OCR.ActiveProvider
	if id == "" {
		id = OCRProviderAliyun
	}
	provider, ok := c.OCR.Providers[id]
	return id, provider, ok
}

// IsOCRConfigured reports whether the selected OCR provider has the fields
// required to sign and send a request.
func (c AppConfig) IsOCRConfigured() bool {
	id, provider, ok := c.ActiveOCRProvider()
	if !ok ||
		provider.Endpoint == "" ||
		provider.AccessKeyID == "" ||
		provider.AccessKeySecret == "" ||
		provider.Action == "" ||
		provider.Version == "" {
		return false
	}
	if id == OCRProviderVolcengine && (provider.Region == "" || provider.Service == "") {
		return false
	}
	return true
}
