package config

import (
	"fmt"
	"net/url"
	"os"
	"strings"

	"apps/backend/internal/lib/secretbox"

	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/rest"
)

// Config — go-zero 風格：RestConf + CacheRedis + Mongo URI.
type Config struct {
	rest.RestConf

	// CacheRedis is go-zero cache.CacheConf (feeds monc Redis 快取).
	CacheRedis cache.CacheConf
	Redis      struct {
		Namespace                     string `json:",optional,default=haixun:dev:v1"`
		AIModelCacheFingerprintSecret string `json:",optional"`
	}

	Mongo struct {
		URI      string
		Database string
	}

	Auth struct {
		AccessSecret  string
		AccessExpire  int64
		RefreshSecret string
		RefreshExpire int64
	}
	// MemberSettingsEncryption is dedicated to member BYOK secrets. Never reuse
	// Auth or Scout secrets here.
	MemberSettingsEncryption struct {
		CurrentKeyID string `json:",optional"`
		Key          string `json:",optional"`
	}
	// CORSAllowedOrigins is an explicit browser-origin allowlist.
	CORSAllowedOrigins []string `json:",optional"`
	Platform           struct {
		// AIKey — legacy alias for XAIKey
		AIKey string `json:",optional"`
		// XAIKey — platform BYOK for xAI (api.x.ai)
		XAIKey string `json:",optional"`
		// OpenCodeKey — platform BYOK for OpenCode Go (opencode.ai/zen/go)
		OpenCodeKey      string `json:",optional"`
		ExaKey           string `json:",optional"`
		ThreadsAppId     string `json:",optional"`
		ThreadsAppSecret string `json:",optional"`
	}
	// Scout keeps browser-session credentials separate from JWT and public APIs.
	Scout struct {
		SessionSecret   string `json:",optional"`
		CrawlerEndpoint string `json:",optional"`
		CrawlerToken    string `json:",optional"`
	}
	Worker struct {
		ID             string `json:",default=worker-1"`
		PollIntervalMs int    `json:",default=2000"`
	}
	// Seed is used only by cmd/seeder; gateway and worker never read or log it.
	Seed struct {
		AdminPassword string `json:",optional"`
	}
	// Bcrypt cost (stand-alone Bcrypt.Cost)
	Bcrypt struct {
		Cost int `json:",default=10"`
	}
	// OAuth third-party. Empty provider config disables that provider.
	OAuth struct {
		GoogleClientID   string `json:",optional"`
		LineClientID     string `json:",optional"`
		LineClientSecret string `json:",optional"`
		LineRedirectURI  string `json:",optional"`
	}
	Stripe struct {
		Enabled         bool   `json:",optional"`
		SecretKey       string `json:",optional"`
		WebhookSecret   string `json:",optional"`
		StarterPriceID  string `json:",optional"`
		ProPriceID      string `json:",optional"`
		SuccessURL      string `json:",optional"`
		CancelURL       string `json:",optional"`
		PortalReturnURL string `json:",optional"`
	}
	// Mail — 驗證碼／重設密碼寄信（SMTP；空 Host 則只打 log + 可回 mock_code）
	Mail struct {
		Sender string `json:",optional"` // e.g. "Harbor Desk <noreply@example.com>"
		SMTP   struct {
			Host     string `json:",optional"`
			Port     int    `json:",default=587"`
			User     string `json:",optional"`
			Password string `json:",optional"`
		} `json:",optional"`
		// DevExposeCode: API 回 mock_code（本機沒信箱時用）。正式請 false。
		DevExposeCode bool `json:",default=true"`
	} `json:",optional"`
	// PublicWebBase — 前端 origin，用於重設密碼信內連結／logo（勿尾斜線）
	// 例：http://127.0.0.1:5173 或 https://threads-tool.30cm.net
	PublicWebBase string `json:",optional,default=http://127.0.0.1:5173"`
	// PublicAPIBase — 對外 API origin（Threads OAuth redirect_uri 必須 https 公開網址）
	// 空則：若 PublicWebBase 為 https 則同 host；否則回落 http://127.0.0.1:Port
	// 例：https://threads-tool.30cm.net
	PublicAPIBase string `json:",optional"`
	// Brand — 郵件 chrome（hermes）；LogoURL 空則用 PublicWebBase + /brand-mark.svg
	Brand struct {
		Name      string `json:",optional"`
		LogoURL   string `json:",optional"` // absolute URL
		Copyright string `json:",optional"`
	} `json:",optional"`
	// ObjectStorage — 頭像等二進位；**僅 MinIO / S3**（不落本機磁碟）
	ObjectStorage struct {
		Endpoint      string `json:",optional"` // 必填，e.g. http://127.0.0.1:9000
		Region        string `json:",optional,default=us-east-1"`
		Bucket        string `json:",optional,default=haixun-assets"`
		AccessKey     string `json:",optional"`
		SecretKey     string `json:",optional"`
		UsePathStyle  bool   `json:",optional,default=true"`
		PublicBaseURL string `json:",optional"` // 公開讀取前綴，勿尾斜線
	} `json:",optional"`
}

// ApplyEnv overlays secrets from environment.
func (c *Config) ApplyEnv() {
	if strings.TrimSpace(c.Redis.Namespace) == "" {
		c.Redis.Namespace = "haixun:dev:v1"
	}
	if c.Brand.Name == "" {
		c.Brand.Name = "Harbor Desk"
	}
	if c.Brand.Copyright == "" {
		c.Brand.Copyright = "© Harbor Desk"
	}
	if v := os.Getenv("MONGO_URI"); v != "" {
		c.Mongo.URI = v
	}
	if v := os.Getenv("MONGO_DATABASE"); v != "" {
		c.Mongo.Database = v
	}
	if v := os.Getenv("REDIS_HOST"); v != "" && len(c.CacheRedis) > 0 {
		c.CacheRedis[0].RedisConf.Host = v
	}
	if len(c.CacheRedis) > 0 {
		// monc 快取 Redis 帳密（本機 infra 常開 requirepass）
		if v := os.Getenv("REDIS_PASSWORD"); v != "" {
			c.CacheRedis[0].RedisConf.Pass = v
		} else if v := os.Getenv("HAIXUN_REDIS_PASSWORD"); v != "" {
			c.CacheRedis[0].RedisConf.Pass = v
		}
		if v := os.Getenv("HAIXUN_REDIS_ADDR"); v != "" {
			c.CacheRedis[0].RedisConf.Host = v
		}
	}
	if v := firstEnv("REDIS_NAMESPACE", "HAIXUN_REDIS_NAMESPACE"); v != "" {
		c.Redis.Namespace = strings.Trim(v, ":")
	}
	if v := firstEnv("AI_MODEL_CACHE_FINGERPRINT_SECRET", "HAIXUN_AI_MODEL_CACHE_FINGERPRINT_SECRET"); v != "" {
		c.Redis.AIModelCacheFingerprintSecret = v
	}
	if v := os.Getenv("AUTH_ACCESS_SECRET"); v != "" {
		c.Auth.AccessSecret = v
	}
	if v := os.Getenv("AUTH_REFRESH_SECRET"); v != "" {
		c.Auth.RefreshSecret = v
	}
	if v := os.Getenv("STRIPE_ENABLED"); v != "" {
		c.Stripe.Enabled = v == "1" || strings.EqualFold(v, "true")
	}
	if v := os.Getenv("STRIPE_SECRET_KEY"); v != "" {
		c.Stripe.SecretKey = v
	}
	if v := os.Getenv("STRIPE_WEBHOOK_SECRET"); v != "" {
		c.Stripe.WebhookSecret = v
	}
	if v := os.Getenv("STRIPE_STARTER_PRICE_ID"); v != "" {
		c.Stripe.StarterPriceID = v
	}
	if v := os.Getenv("STRIPE_PRO_PRICE_ID"); v != "" {
		c.Stripe.ProPriceID = v
	}
	if v := os.Getenv("STRIPE_SUCCESS_URL"); v != "" {
		c.Stripe.SuccessURL = v
	}
	if v := os.Getenv("STRIPE_CANCEL_URL"); v != "" {
		c.Stripe.CancelURL = v
	}
	if v := os.Getenv("STRIPE_PORTAL_RETURN_URL"); v != "" {
		c.Stripe.PortalReturnURL = v
	}
	if v := os.Getenv("MEMBER_SETTINGS_ENCRYPTION_KEY_ID"); v != "" {
		c.MemberSettingsEncryption.CurrentKeyID = v
	}
	if v := os.Getenv("MEMBER_SETTINGS_ENCRYPTION_KEY"); v != "" {
		c.MemberSettingsEncryption.Key = v
	}
	if v := os.Getenv("PLATFORM_AI_KEY"); v != "" {
		c.Platform.AIKey = v
	}
	if v := firstEnv("PLATFORM_XAI_KEY", "XAI_API_KEY"); v != "" {
		c.Platform.XAIKey = v
	}
	if v := firstEnv("PLATFORM_OPENCODE_KEY", "OPENCODE_API_KEY", "OPENCODE_GO_API_KEY"); v != "" {
		c.Platform.OpenCodeKey = v
	}
	// legacy AIKey → XAI when XAI empty
	if c.Platform.XAIKey == "" && c.Platform.AIKey != "" {
		c.Platform.XAIKey = c.Platform.AIKey
	}
	if v := os.Getenv("PLATFORM_EXA_KEY"); v != "" {
		c.Platform.ExaKey = v
	}
	if v := firstEnv("THREADS_APP_ID", "HAIXUN_THREADS_APP_ID"); v != "" {
		c.Platform.ThreadsAppId = v
	}
	if v := firstEnv("THREADS_APP_SECRET", "HAIXUN_THREADS_APP_SECRET"); v != "" {
		c.Platform.ThreadsAppSecret = v
	}
	if v := os.Getenv("SCOUT_SESSION_SECRET"); v != "" {
		c.Scout.SessionSecret = v
	}
	if v := os.Getenv("SCOUT_CRAWLER_ENDPOINT"); v != "" {
		c.Scout.CrawlerEndpoint = v
	}
	if v := os.Getenv("SCOUT_CRAWLER_TOKEN"); v != "" {
		c.Scout.CrawlerToken = v
	}
	if v := os.Getenv("SEED_ADMIN_PASSWORD"); v != "" {
		c.Seed.AdminPassword = v
	}
	if v := os.Getenv("WORKER_ID"); v != "" {
		c.Worker.ID = v
	}
	if v := os.Getenv("PORT"); v != "" {
		var p int
		valid := true
		for _, ch := range v {
			if ch < '0' || ch > '9' {
				valid = false
				break
			}
			p = p*10 + int(ch-'0')
		}
		if valid && p > 0 {
			c.Port = p
		}
	}
	// Mail / SMTP（MAIL_* 優先；HAIXUN_SMTP_* 與 old/infra 相容）
	if v := firstEnv("MAIL_SENDER", "HAIXUN_SMTP_FROM"); v != "" {
		c.Mail.Sender = v
	}
	if v := firstEnv("MAIL_SMTP_HOST", "HAIXUN_SMTP_HOST"); v != "" {
		c.Mail.SMTP.Host = v
	}
	if v := firstEnv("MAIL_SMTP_USER", "HAIXUN_SMTP_USERNAME"); v != "" {
		c.Mail.SMTP.User = v
	}
	if v := firstEnv("MAIL_SMTP_PASSWORD", "HAIXUN_SMTP_PASSWORD"); v != "" {
		c.Mail.SMTP.Password = v
	}
	if v := firstEnv("MAIL_SMTP_PORT", "HAIXUN_SMTP_PORT"); v != "" {
		if p := parsePort(v); p > 0 {
			c.Mail.SMTP.Port = p
		}
	}
	if v := os.Getenv("MAIL_DEV_EXPOSE_CODE"); v == "0" || v == "false" || v == "FALSE" {
		c.Mail.DevExposeCode = false
	} else if v == "1" || v == "true" || v == "TRUE" {
		c.Mail.DevExposeCode = true
	}
	if v := firstEnv("PUBLIC_WEB_BASE", "HAIXUN_PUBLIC_WEB_BASE"); v != "" {
		c.PublicWebBase = strings.TrimRight(v, "/")
	}
	if v := firstEnv("PUBLIC_API_BASE", "HAIXUN_PUBLIC_API_BASE"); v != "" {
		c.PublicAPIBase = strings.TrimRight(v, "/")
	}
	if v := os.Getenv("CORS_ALLOWED_ORIGINS"); v != "" {
		c.CORSAllowedOrigins = appendUnique(c.CORSAllowedOrigins, splitCSV(v))
	}
	if v := os.Getenv("MAIL_BRAND_NAME"); v != "" {
		c.Brand.Name = v
	}
	if v := firstEnv("MAIL_LOGO_URL", "HAIXUN_MAIL_LOGO_URL"); v != "" {
		c.Brand.LogoURL = v
	}
	if v := os.Getenv("MAIL_COPYRIGHT"); v != "" {
		c.Brand.Copyright = v
	}
	// Object storage (HAIXUN_STORAGE_S3_* 與 old/infra 相容)
	if v := firstEnv("OBJECT_STORAGE_ENDPOINT", "HAIXUN_STORAGE_S3_ENDPOINT"); v != "" {
		c.ObjectStorage.Endpoint = v
	}
	if v := firstEnv("OBJECT_STORAGE_REGION", "HAIXUN_STORAGE_S3_REGION"); v != "" {
		c.ObjectStorage.Region = v
	}
	if v := firstEnv("OBJECT_STORAGE_BUCKET", "HAIXUN_STORAGE_S3_BUCKET"); v != "" {
		c.ObjectStorage.Bucket = v
	}
	if v := firstEnv("OBJECT_STORAGE_ACCESS_KEY", "HAIXUN_STORAGE_S3_ACCESS_KEY", "MINIO_ROOT_USER"); v != "" {
		c.ObjectStorage.AccessKey = v
	}
	if v := firstEnv("OBJECT_STORAGE_SECRET_KEY", "HAIXUN_STORAGE_S3_SECRET_KEY", "MINIO_ROOT_PASSWORD"); v != "" {
		c.ObjectStorage.SecretKey = v
	}
	if v := firstEnv("OBJECT_STORAGE_PUBLIC_BASE_URL", "HAIXUN_STORAGE_S3_PUBLIC_BASE_URL"); v != "" {
		c.ObjectStorage.PublicBaseURL = strings.TrimRight(v, "/")
	}
	if v := os.Getenv("HAIXUN_STORAGE_S3_USE_PATH_STYLE"); v == "0" || v == "false" {
		c.ObjectStorage.UsePathStyle = false
	} else if v == "1" || v == "true" {
		c.ObjectStorage.UsePathStyle = true
	}
}

// ValidateProductionDependencies prevents a successful-looking deployment that
// silently falls back to test credentials or fake external integrations.
func (c Config) ValidateProductionDependencies() error {
	if invalidSecret(c.Auth.AccessSecret) {
		return fmt.Errorf("AUTH_ACCESS_SECRET must be at least 32 bytes")
	}
	if invalidSecret(c.Auth.RefreshSecret) {
		return fmt.Errorf("AUTH_REFRESH_SECRET must be at least 32 bytes")
	}
	if c.Auth.AccessSecret == c.Auth.RefreshSecret {
		return fmt.Errorf("AUTH_ACCESS_SECRET and AUTH_REFRESH_SECRET must differ")
	}
	fingerprintSecret := strings.TrimSpace(c.Redis.AIModelCacheFingerprintSecret)
	if invalidSecret(fingerprintSecret) {
		return fmt.Errorf("AI_MODEL_CACHE_FINGERPRINT_SECRET must be at least 32 bytes")
	}
	if fingerprintSecret == strings.TrimSpace(c.Auth.AccessSecret) ||
		fingerprintSecret == strings.TrimSpace(c.Auth.RefreshSecret) ||
		fingerprintSecret == strings.TrimSpace(c.Scout.SessionSecret) ||
		fingerprintSecret == strings.TrimSpace(c.MemberSettingsEncryption.Key) {
		return fmt.Errorf("AI model cache fingerprint secret must be dedicated")
	}
	if namespace := strings.TrimSpace(c.Redis.Namespace); namespace == "" || strings.ContainsAny(namespace, "{} \t\r\n") {
		return fmt.Errorf("Redis.Namespace must be non-empty and must not contain whitespace or braces")
	}
	if _, err := c.NewMemberSettingsCodec(); err != nil {
		return fmt.Errorf("member settings encryption: %w", err)
	}
	encryptionKey := strings.TrimSpace(c.MemberSettingsEncryption.Key)
	if encryptionKey == strings.TrimSpace(c.Auth.AccessSecret) ||
		encryptionKey == strings.TrimSpace(c.Auth.RefreshSecret) ||
		encryptionKey == strings.TrimSpace(c.Scout.SessionSecret) {
		return fmt.Errorf("member settings encryption key must not reuse Auth or Scout secrets")
	}
	if len(c.CacheRedis) == 0 || strings.TrimSpace(c.CacheRedis[0].Host) == "" {
		return fmt.Errorf("CacheRedis is required")
	}
	if strings.TrimSpace(c.Mongo.URI) == "" || strings.TrimSpace(c.Mongo.Database) == "" {
		return fmt.Errorf("Mongo.URI and Mongo.Database are required")
	}
	if len(c.CORSAllowedOrigins) == 0 {
		return fmt.Errorf("CORSAllowedOrigins is required")
	}
	if err := validateStripe(c.Stripe.Enabled, c.Stripe.SecretKey, c.Stripe.WebhookSecret, c.Stripe.StarterPriceID, c.Stripe.ProPriceID, c.Stripe.SuccessURL, c.Stripe.CancelURL, c.Stripe.PortalReturnURL); err != nil {
		return err
	}
	return nil
}

// NewMemberSettingsCodec constructs the dedicated BYOK-at-rest codec.
func (c Config) NewMemberSettingsCodec() (*secretbox.Codec, error) {
	return secretbox.New(c.MemberSettingsEncryption.CurrentKeyID, c.MemberSettingsEncryption.Key)
}

func invalidSecret(value string) bool {
	value = strings.TrimSpace(value)
	return len(value) < 32 || strings.Contains(value, "REPLACE_WITH") || strings.Contains(value, "CHANGE_ME")
}

func validateStripe(enabled bool, secretKey, webhookSecret, starterPriceID, proPriceID, successURL, cancelURL, portalReturnURL string) error {
	if !enabled {
		return nil
	}
	if !strings.HasPrefix(strings.TrimSpace(secretKey), "sk_") {
		return fmt.Errorf("STRIPE_SECRET_KEY is required when Stripe is enabled")
	}
	if !strings.HasPrefix(strings.TrimSpace(webhookSecret), "whsec_") {
		return fmt.Errorf("STRIPE_WEBHOOK_SECRET is required when Stripe is enabled")
	}
	starterPriceID = strings.TrimSpace(starterPriceID)
	proPriceID = strings.TrimSpace(proPriceID)
	if !strings.HasPrefix(starterPriceID, "price_") || !strings.HasPrefix(proPriceID, "price_") {
		return fmt.Errorf("STRIPE_STARTER_PRICE_ID and STRIPE_PRO_PRICE_ID are required when Stripe is enabled")
	}
	if starterPriceID == proPriceID {
		return fmt.Errorf("Stripe Starter and Pro Price IDs must differ")
	}
	if !strings.Contains(successURL, "{CHECKOUT_ID}") {
		return fmt.Errorf("STRIPE_SUCCESS_URL must contain {CHECKOUT_ID}")
	}
	for name, value := range map[string]string{
		"STRIPE_SUCCESS_URL":       successURL,
		"STRIPE_CANCEL_URL":        cancelURL,
		"STRIPE_PORTAL_RETURN_URL": portalReturnURL,
	} {
		parsed, err := url.ParseRequestURI(strings.TrimSpace(value))
		if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil {
			return fmt.Errorf("%s must be an absolute HTTPS URL", name)
		}
	}
	return nil
}

func firstEnv(keys ...string) string {
	for _, k := range keys {
		if v := os.Getenv(k); v != "" {
			return v
		}
	}
	return ""
}

func appendUnique(existing, values []string) []string {
	seen := make(map[string]struct{}, len(existing)+len(values))
	out := make([]string, 0, len(existing)+len(values))
	for _, value := range append(existing, values...) {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func splitCSV(v string) []string {
	parts := strings.Split(v, ",")
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		if part = strings.TrimSpace(part); part != "" {
			out = append(out, strings.TrimRight(part, "/"))
		}
	}
	return out
}

func parsePort(v string) int {
	var p int
	for _, ch := range v {
		if ch < '0' || ch > '9' {
			return 0
		}
		p = p*10 + int(ch-'0')
	}
	return p
}
