package config

import (
	"encoding/hex"
	"fmt"
	"net/url"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/spf13/viper"
)

// Config holds all configuration for the application
type Config struct {
	Server     ServerConfig
	Database   DatabaseConfig
	Redis      RedisConfig
	Email      EmailConfig
	Push       PushConfig
	Worker     WorkerConfig
	Security   SecurityConfig
	Storage    StorageConfig
	AppleAuth  AppleAuthConfig
	GoogleAuth GoogleAuthConfig
	AppleIAP   AppleIAPConfig
	GoogleIAP  GoogleIAPConfig
	Stripe     StripeConfig
	Features   FeatureFlags
}

type ServerConfig struct {
	Port               int
	Debug              bool
	DebugFixedCodes    bool     // Separate from Debug: enables fixed confirmation codes for local testing
	AllowedHosts       []string
	CorsAllowedOrigins []string // Comma-separated origins for CORS (production only; debug uses wildcard)
	Timezone           string
	StaticDir          string // Directory for static landing page files
	BaseURL            string // Public base URL for email tracking links (e.g., https://honeyDue.treytartt.com)
}

type DatabaseConfig struct {
	Host         string
	Port         int
	User         string
	Password     string
	Database     string
	SSLMode      string
	MaxOpenConns int
	MaxIdleConns int
	MaxLifetime  time.Duration
}

type RedisConfig struct {
	URL      string
	Password string
	DB       int
}

type EmailConfig struct {
	Host     string
	Port     int
	User     string
	Password string
	From     string
	UseTLS   bool
}

type PushConfig struct {
	// APNs (iOS) - uses github.com/sideshow/apns2 for direct APNs communication
	APNSKeyPath    string
	APNSKeyID      string
	APNSTeamID     string
	APNSTopic      string
	APNSSandbox    bool
	APNSProduction bool // If true, use production APNs; if false, use sandbox

	// FCM (Android) - uses FCM HTTP v1 API with OAuth 2.0
	FCMProjectID          string // Firebase project ID (required for v1 API)
	FCMServiceAccountPath string // Path to Google service account JSON file
	FCMServiceAccountJSON string // Raw service account JSON (alternative to path, e.g. for env var injection)

	// Deprecated: FCMServerKey is for the legacy HTTP API. Use FCMProjectID + service account instead.
	FCMServerKey string
}

type AppleAuthConfig struct {
	ClientID string // Bundle ID (e.g., com.tt.honeyDue.honeyDueDev)
	TeamID   string // Apple Developer Team ID
}

type GoogleAuthConfig struct {
	ClientID        string // Web client ID for token verification
	AndroidClientID string // Android client ID (optional, for audience verification)
	IOSClientID     string // iOS client ID (optional, for audience verification)
}

// AppleIAPConfig holds Apple App Store Server API configuration
type AppleIAPConfig struct {
	KeyPath   string // Path to .p8 private key file
	KeyID     string // Key ID from App Store Connect
	IssuerID  string // Issuer ID from App Store Connect
	BundleID  string // App bundle ID (e.g., com.tt.honeyDue)
	Sandbox   bool   // Use sandbox environment for testing
}

// GoogleIAPConfig holds Google Play Developer API configuration
type GoogleIAPConfig struct {
	ServiceAccountPath string // Path to service account JSON file
	PackageName        string // Android package name (e.g., com.tt.honeyDue)
}

// StripeConfig holds Stripe payment configuration
type StripeConfig struct {
	SecretKey      string // Stripe secret API key
	WebhookSecret  string // Stripe webhook endpoint signing secret
	PriceMonthly   string // Stripe Price ID for monthly Pro subscription
	PriceYearly    string // Stripe Price ID for yearly Pro subscription
}

type WorkerConfig struct {
	// Scheduled job times (UTC)
	TaskReminderHour    int
	OverdueReminderHour int
	DailyNotifHour      int
}

type SecurityConfig struct {
	SecretKey            string
	TokenCacheTTL        time.Duration
	PasswordResetExpiry  time.Duration
	ConfirmationExpiry   time.Duration
	MaxPasswordResetRate int // per hour
	TokenExpiryDays      int // Number of days before auth tokens expire (default 90)
	TokenRefreshDays     int // Token must be at least this many days old before refresh (default 60)
}

// StorageConfig holds file storage settings.
// When S3Endpoint is set, files are stored in S3-compatible storage (B2, MinIO).
// When S3Endpoint is empty, files are stored on the local filesystem using UploadDir.
type StorageConfig struct {
	// Local filesystem settings
	UploadDir string // Directory to store uploaded files (local mode)
	BaseURL   string // Public URL prefix for serving files (e.g., "/uploads")

	// S3-compatible storage settings (B2, MinIO)
	S3Endpoint  string // S3 endpoint (e.g., "s3.us-west-004.backblazeb2.com" or "minio:9000")
	S3KeyID     string // Access key ID
	S3AppKey    string // Secret access key
	S3Bucket    string // Bucket name
	S3UseSSL    bool   // Use HTTPS (true for B2, false for in-cluster MinIO)
	S3Region    string // Region (optional, defaults to "us-east-1")

	// Shared settings
	MaxFileSize   int64  // Max file size in bytes (default 10MB)
	AllowedTypes  string // Comma-separated MIME types
	EncryptionKey string // 64-char hex key for file encryption at rest (optional)
}

// IsS3 returns true if S3-compatible storage is configured
func (c *StorageConfig) IsS3() bool {
	return c.S3Endpoint != "" && c.S3KeyID != "" && c.S3AppKey != "" && c.S3Bucket != ""
}

// FeatureFlags holds kill switches for major subsystems.
// All default to true (enabled). Set to false via env vars to disable.
type FeatureFlags struct {
	PushEnabled             bool // FEATURE_PUSH_ENABLED (default: true)
	EmailEnabled            bool // FEATURE_EMAIL_ENABLED (default: true)
	WebhooksEnabled         bool // FEATURE_WEBHOOKS_ENABLED (default: true)
	OnboardingEmailsEnabled bool // FEATURE_ONBOARDING_EMAILS_ENABLED (default: true)
	PDFReportsEnabled       bool // FEATURE_PDF_REPORTS_ENABLED (default: true)
	WorkerEnabled           bool // FEATURE_WORKER_ENABLED (default: true)
}

var (
	cfg     *Config
	cfgOnce sync.Once
)

// knownWeakSecretKeys contains well-known default or placeholder secret keys
// that must not be used in production.
var knownWeakSecretKeys = map[string]bool{
	"secret":                                true,
	"changeme":                              true,
	"change-me":                             true,
	"password":                              true,
	"change-me-in-production-secret-key-12345": true,
}

// Load reads configuration from environment variables
func Load() (*Config, error) {
	var loadErr error

	cfgOnce.Do(func() {
		viper.SetEnvPrefix("")
		viper.AutomaticEnv()
		viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

		// Set defaults
		setDefaults()

		// Parse DATABASE_URL if set (Dokku-style)
		dbConfig := DatabaseConfig{
			Host:         viper.GetString("DB_HOST"),
			Port:         viper.GetInt("DB_PORT"),
			User:         viper.GetString("POSTGRES_USER"),
			Password:     viper.GetString("POSTGRES_PASSWORD"),
			Database:     viper.GetString("POSTGRES_DB"),
			SSLMode:      viper.GetString("DB_SSLMODE"),
			MaxOpenConns: viper.GetInt("DB_MAX_OPEN_CONNS"),
			MaxIdleConns: viper.GetInt("DB_MAX_IDLE_CONNS"),
			MaxLifetime:  viper.GetDuration("DB_MAX_LIFETIME"),
		}

		// Override with DATABASE_URL if present (F-16: log warning on parse failure)
		if databaseURL := viper.GetString("DATABASE_URL"); databaseURL != "" {
			parsed, err := parseDatabaseURL(databaseURL)
			if err != nil {
				maskedURL := MaskURLCredentials(databaseURL)
				fmt.Printf("WARNING: Failed to parse DATABASE_URL (%s): %v — falling back to individual DB_* env vars\n", maskedURL, err)
			} else {
				dbConfig.Host = parsed.Host
				dbConfig.Port = parsed.Port
				dbConfig.User = parsed.User
				dbConfig.Password = parsed.Password
				dbConfig.Database = parsed.Database
				if parsed.SSLMode != "" {
					dbConfig.SSLMode = parsed.SSLMode
				}
			}
		}

		cfg = &Config{
			Server: ServerConfig{
				Port:               viper.GetInt("PORT"),
				Debug:              viper.GetBool("DEBUG"),
				DebugFixedCodes:    viper.GetBool("DEBUG_FIXED_CODES"),
				AllowedHosts:       strings.Split(viper.GetString("ALLOWED_HOSTS"), ","),
				CorsAllowedOrigins: parseCorsOrigins(viper.GetString("CORS_ALLOWED_ORIGINS")),
				Timezone:           viper.GetString("TIMEZONE"),
				StaticDir:          viper.GetString("STATIC_DIR"),
				BaseURL:            viper.GetString("BASE_URL"),
			},
			Database: dbConfig,
			Redis: RedisConfig{
				URL:      viper.GetString("REDIS_URL"),
				Password: viper.GetString("REDIS_PASSWORD"),
				DB:       viper.GetInt("REDIS_DB"),
			},
			Email: EmailConfig{
				Host:     viper.GetString("EMAIL_HOST"),
				Port:     viper.GetInt("EMAIL_PORT"),
				User:     viper.GetString("EMAIL_HOST_USER"),
				Password: viper.GetString("EMAIL_HOST_PASSWORD"),
				From:     viper.GetString("DEFAULT_FROM_EMAIL"),
				UseTLS:   viper.GetBool("EMAIL_USE_TLS"),
			},
			Push: PushConfig{
				APNSKeyPath:           viper.GetString("APNS_AUTH_KEY_PATH"),
				APNSKeyID:             viper.GetString("APNS_AUTH_KEY_ID"),
				APNSTeamID:            viper.GetString("APNS_TEAM_ID"),
				APNSTopic:             viper.GetString("APNS_TOPIC"),
				APNSSandbox:           viper.GetBool("APNS_USE_SANDBOX"),
				APNSProduction:        viper.GetBool("APNS_PRODUCTION"),
				FCMProjectID:          viper.GetString("FCM_PROJECT_ID"),
				FCMServiceAccountPath: viper.GetString("FCM_SERVICE_ACCOUNT_PATH"),
				FCMServiceAccountJSON: viper.GetString("FCM_SERVICE_ACCOUNT_JSON"),
				FCMServerKey:          viper.GetString("FCM_SERVER_KEY"),
			},
			Worker: WorkerConfig{
				TaskReminderHour:    viper.GetInt("TASK_REMINDER_HOUR"),
				OverdueReminderHour: viper.GetInt("OVERDUE_REMINDER_HOUR"),
				DailyNotifHour:      viper.GetInt("DAILY_DIGEST_HOUR"),
			},
			Security: SecurityConfig{
				SecretKey:            viper.GetString("SECRET_KEY"),
				TokenCacheTTL:        5 * time.Minute,
				PasswordResetExpiry:  15 * time.Minute,
				ConfirmationExpiry:   24 * time.Hour,
				MaxPasswordResetRate: 3,
				TokenExpiryDays:      viper.GetInt("TOKEN_EXPIRY_DAYS"),
				TokenRefreshDays:     viper.GetInt("TOKEN_REFRESH_DAYS"),
			},
			Storage: StorageConfig{
				UploadDir:     viper.GetString("STORAGE_UPLOAD_DIR"),
				BaseURL:       viper.GetString("STORAGE_BASE_URL"),
				S3Endpoint:    viper.GetString("B2_ENDPOINT"),
				S3KeyID:       viper.GetString("B2_KEY_ID"),
				S3AppKey:      viper.GetString("B2_APP_KEY"),
				S3Bucket:      viper.GetString("B2_BUCKET_NAME"),
				S3UseSSL:      viper.GetString("STORAGE_USE_SSL") == "" || viper.GetBool("STORAGE_USE_SSL"),
				S3Region:      viper.GetString("B2_REGION"),
				MaxFileSize:   viper.GetInt64("STORAGE_MAX_FILE_SIZE"),
				AllowedTypes:  viper.GetString("STORAGE_ALLOWED_TYPES"),
				EncryptionKey: viper.GetString("STORAGE_ENCRYPTION_KEY"),
			},
			AppleAuth: AppleAuthConfig{
				ClientID: viper.GetString("APPLE_CLIENT_ID"),
				TeamID:   viper.GetString("APPLE_TEAM_ID"),
			},
			GoogleAuth: GoogleAuthConfig{
				ClientID:        viper.GetString("GOOGLE_CLIENT_ID"),
				AndroidClientID: viper.GetString("GOOGLE_ANDROID_CLIENT_ID"),
				IOSClientID:     viper.GetString("GOOGLE_IOS_CLIENT_ID"),
			},
			AppleIAP: AppleIAPConfig{
				KeyPath:  viper.GetString("APPLE_IAP_KEY_PATH"),
				KeyID:    viper.GetString("APPLE_IAP_KEY_ID"),
				IssuerID: viper.GetString("APPLE_IAP_ISSUER_ID"),
				BundleID: viper.GetString("APPLE_IAP_BUNDLE_ID"),
				Sandbox:  viper.GetBool("APPLE_IAP_SANDBOX"),
			},
			GoogleIAP: GoogleIAPConfig{
				ServiceAccountPath: viper.GetString("GOOGLE_IAP_SERVICE_ACCOUNT_PATH"),
				PackageName:        viper.GetString("GOOGLE_IAP_PACKAGE_NAME"),
			},
			Stripe: StripeConfig{
				SecretKey:     viper.GetString("STRIPE_SECRET_KEY"),
				WebhookSecret: viper.GetString("STRIPE_WEBHOOK_SECRET"),
				PriceMonthly:  viper.GetString("STRIPE_PRICE_MONTHLY"),
				PriceYearly:   viper.GetString("STRIPE_PRICE_YEARLY"),
			},
			Features: FeatureFlags{
				PushEnabled:             viper.GetBool("FEATURE_PUSH_ENABLED"),
				EmailEnabled:            viper.GetBool("FEATURE_EMAIL_ENABLED"),
				WebhooksEnabled:         viper.GetBool("FEATURE_WEBHOOKS_ENABLED"),
				OnboardingEmailsEnabled: viper.GetBool("FEATURE_ONBOARDING_EMAILS_ENABLED"),
				PDFReportsEnabled:       viper.GetBool("FEATURE_PDF_REPORTS_ENABLED"),
				WorkerEnabled:           viper.GetBool("FEATURE_WORKER_ENABLED"),
			},
		}

		// Validate required fields
		if err := validate(cfg); err != nil {
			loadErr = err
			// Reset so a subsequent call can retry after env is fixed
			cfg = nil
			cfgOnce = sync.Once{}
		}
	})

	if loadErr != nil {
		return nil, loadErr
	}

	return cfg, nil
}

// Get returns the current configuration
func Get() *Config {
	return cfg
}

func setDefaults() {
	// Server defaults
	viper.SetDefault("PORT", 8000)
	viper.SetDefault("DEBUG", false)
	viper.SetDefault("DEBUG_FIXED_CODES", false) // Separate flag for fixed confirmation codes
	viper.SetDefault("ALLOWED_HOSTS", "localhost,127.0.0.1")
	viper.SetDefault("TIMEZONE", "UTC")
	viper.SetDefault("STATIC_DIR", "/app/static")
	viper.SetDefault("BASE_URL", "https://api.myhoneydue.com")

	// Database defaults
	viper.SetDefault("DB_HOST", "localhost")
	viper.SetDefault("DB_PORT", 5432)
	viper.SetDefault("POSTGRES_USER", "postgres")
	viper.SetDefault("POSTGRES_DB", "honeydue")
	viper.SetDefault("DB_SSLMODE", "disable")
	viper.SetDefault("DB_MAX_OPEN_CONNS", 25)
	viper.SetDefault("DB_MAX_IDLE_CONNS", 10)
	viper.SetDefault("DB_MAX_LIFETIME", 600*time.Second)

	// Redis defaults
	viper.SetDefault("REDIS_URL", "redis://localhost:6379/0")
	viper.SetDefault("REDIS_DB", 0)

	// Email defaults
	viper.SetDefault("EMAIL_HOST", "smtp.gmail.com")
	viper.SetDefault("EMAIL_PORT", 587)
	viper.SetDefault("EMAIL_USE_TLS", true)
	viper.SetDefault("EMAIL_HOST_USER", "")
	viper.SetDefault("EMAIL_HOST_PASSWORD", "")
	viper.SetDefault("DEFAULT_FROM_EMAIL", "honeyDue <noreply@myhoneydue.com>")

	// Push notification defaults
	viper.SetDefault("APNS_TOPIC", "com.tt.honeyDue")
	viper.SetDefault("APNS_USE_SANDBOX", true)
	viper.SetDefault("APNS_PRODUCTION", false)

	// Worker defaults (all times in UTC, jobs run at minute 0)
	viper.SetDefault("TASK_REMINDER_HOUR", 14)    // 8:00 PM UTC
	viper.SetDefault("OVERDUE_REMINDER_HOUR", 15) // 9:00 AM UTC
	viper.SetDefault("DAILY_DIGEST_HOUR", 3)      // 3:00 AM UTC

	// Token expiry defaults
	viper.SetDefault("TOKEN_EXPIRY_DAYS", 90)  // Tokens expire after 90 days
	viper.SetDefault("TOKEN_REFRESH_DAYS", 60) // Tokens can be refreshed after 60 days

	// Storage defaults
	viper.SetDefault("STORAGE_UPLOAD_DIR", "./uploads")
	viper.SetDefault("STORAGE_BASE_URL", "/uploads")
	viper.SetDefault("STORAGE_MAX_FILE_SIZE", 10*1024*1024) // 10MB
	viper.SetDefault("STORAGE_ALLOWED_TYPES", "image/jpeg,image/png,image/gif,image/webp,application/pdf")

	// Apple IAP defaults
	viper.SetDefault("APPLE_IAP_SANDBOX", true) // Default to sandbox for safety

	// Google IAP defaults - no defaults needed, will fail gracefully if not configured

	// Feature flags (all enabled by default)
	viper.SetDefault("FEATURE_PUSH_ENABLED", true)
	viper.SetDefault("FEATURE_EMAIL_ENABLED", true)
	viper.SetDefault("FEATURE_WEBHOOKS_ENABLED", true)
	viper.SetDefault("FEATURE_ONBOARDING_EMAILS_ENABLED", true)
	viper.SetDefault("FEATURE_PDF_REPORTS_ENABLED", true)
	viper.SetDefault("FEATURE_WORKER_ENABLED", true)
}

// isWeakSecretKey checks if the provided key is a known weak/default value.
func isWeakSecretKey(key string) bool {
	return knownWeakSecretKeys[strings.ToLower(strings.TrimSpace(key))]
}

func validate(cfg *Config) error {
	// S-08: Validate SECRET_KEY against known weak defaults
	if cfg.Security.SecretKey == "" {
		if cfg.Server.Debug {
			// In debug mode, use a default key with a warning for local development
			cfg.Security.SecretKey = "change-me-in-production-secret-key-12345"
			fmt.Println("WARNING: SECRET_KEY not set, using default (debug mode only)")
			fmt.Println("WARNING: *** DO NOT USE THIS DEFAULT KEY IN PRODUCTION ***")
		} else {
			// In production, refuse to start without a proper secret key
			return fmt.Errorf("FATAL: SECRET_KEY environment variable is required in production (DEBUG=false)")
		}
	} else if isWeakSecretKey(cfg.Security.SecretKey) {
		if cfg.Server.Debug {
			fmt.Printf("WARNING: SECRET_KEY is set to a well-known weak value (%q). Change it for production use.\n", cfg.Security.SecretKey)
		} else {
			return fmt.Errorf("FATAL: SECRET_KEY is set to a well-known weak value (%q). Use a strong, unique secret in production", cfg.Security.SecretKey)
		}
	}

	// Database password might come from DATABASE_URL, don't require it separately
	// The actual connection will fail if credentials are wrong

	// Validate STORAGE_ENCRYPTION_KEY if set: must be exactly 64 hex characters
	if cfg.Storage.EncryptionKey != "" {
		if len(cfg.Storage.EncryptionKey) != 64 {
			return fmt.Errorf("STORAGE_ENCRYPTION_KEY must be exactly 64 hex characters (got %d)", len(cfg.Storage.EncryptionKey))
		}
		if _, err := hex.DecodeString(cfg.Storage.EncryptionKey); err != nil {
			return fmt.Errorf("STORAGE_ENCRYPTION_KEY contains invalid hex: %w", err)
		}
	}

	return nil
}

// MaskURLCredentials parses a URL and replaces any password with "***".
// If parsing fails, it returns the string "<unparseable-url>" to avoid leaking credentials.
func MaskURLCredentials(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return "<unparseable-url>"
	}
	if u.User != nil {
		if _, hasPassword := u.User.Password(); hasPassword {
			u.User = url.UserPassword(u.User.Username(), "***")
		}
	}
	return u.Redacted()
}

// DSN returns the database connection string
func (d *DatabaseConfig) DSN() string {
	return fmt.Sprintf(
		"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
		d.Host, d.Port, d.User, d.Password, d.Database, d.SSLMode,
	)
}

// ReadAPNSKey reads the APNs key from file if path is provided
func (p *PushConfig) ReadAPNSKey() (string, error) {
	if p.APNSKeyPath == "" {
		return "", nil
	}

	content, err := os.ReadFile(p.APNSKeyPath)
	if err != nil {
		return "", fmt.Errorf("failed to read APNs key: %w", err)
	}

	return string(content), nil
}

// parseCorsOrigins splits a comma-separated CORS_ALLOWED_ORIGINS string
// into a slice, trimming whitespace. Returns nil if the input is empty.
func parseCorsOrigins(raw string) []string {
	if raw == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	var origins []string
	for _, p := range parts {
		trimmed := strings.TrimSpace(p)
		if trimmed != "" {
			origins = append(origins, trimmed)
		}
	}
	return origins
}

// parseDatabaseURL parses a PostgreSQL URL into DatabaseConfig
// Format: postgres://user:password@host:port/database?sslmode=disable
func parseDatabaseURL(databaseURL string) (*DatabaseConfig, error) {
	u, err := url.Parse(databaseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse DATABASE_URL: %w", err)
	}

	// Default port
	port := 5432
	if u.Port() != "" {
		port, err = strconv.Atoi(u.Port())
		if err != nil {
			return nil, fmt.Errorf("invalid port in DATABASE_URL: %w", err)
		}
	}

	// Get password
	password, _ := u.User.Password()

	// Get database name (remove leading slash)
	database := strings.TrimPrefix(u.Path, "/")

	// Get sslmode from query params
	sslMode := u.Query().Get("sslmode")

	return &DatabaseConfig{
		Host:     u.Hostname(),
		Port:     port,
		User:     u.User.Username(),
		Password: password,
		Database: database,
		SSLMode:  sslMode,
	}, nil
}
