package config

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

type Config struct {
	Port           string
	DatabaseURL    string
	JWTPrivateKey  *rsa.PrivateKey
	JWTPublicKey   *rsa.PublicKey
	JWTExpirySec   int
	OwnerEmail     string
	APISecret      string
	SteamAPIKey    string
	PublicURL      string
	FrontendURL    string
	PublicHost     string
	PortRangeStart int
	PortRangeEnd   int
	GSLTTokens     []string
	Environment    string
	DemoMode       bool
}

func Load() (*Config, error) {
	cfg := &Config{
		Port:           getEnv("PORT", "3000"),
		DatabaseURL:    getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/tapper?sslmode=disable"),
		JWTExpirySec:   getEnvInt("JWT_EXPIRY_SEC", 900),
		OwnerEmail:     getEnv("OWNER_EMAIL", ""),
		APISecret:      getEnv("API_SECRET", "change-me-in-production"),
		SteamAPIKey:    getEnv("STEAM_API_KEY", ""),
		PublicURL:      getEnv("PUBLIC_URL", "http://localhost:3000"),
		FrontendURL:    getEnv("FRONTEND_URL", "http://localhost:5173"),
		PublicHost:     getEnv("PUBLIC_HOST", "localhost"),
		PortRangeStart: getEnvInt("PORT_RANGE_START", 27015),
		PortRangeEnd:   getEnvInt("PORT_RANGE_END", 27064),
		Environment:    getEnv("ENVIRONMENT", "development"),
		DemoMode:       strings.EqualFold(getEnv("DEMO_MODE", "false"), "true"),
	}

	gsltRaw := getEnv("GSLT_TOKENS", "")
	if gsltRaw != "" {
		cfg.GSLTTokens = strings.Split(gsltRaw, ",")
	}

	privKeyPath := getEnv("JWT_PRIVATE_KEY_PATH", "keys/private.pem")
	pubKeyPath := getEnv("JWT_PUBLIC_KEY_PATH", "keys/public.pem")

	privKeyData, err := os.ReadFile(privKeyPath)
	if err != nil {
		privKey, pubKey, genErr := generateRSAKeys(privKeyPath, pubKeyPath)
		if genErr != nil {
			return nil, fmt.Errorf("failed to generate RSA keys: %w", genErr)
		}
		cfg.JWTPrivateKey = privKey
		cfg.JWTPublicKey = pubKey
	} else {
		block, _ := pem.Decode(privKeyData)
		if block == nil {
			return nil, fmt.Errorf("failed to decode private key PEM")
		}
		// openssl genrsa outputs PKCS#8, but some older keys are PKCS#1
		privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
		if err != nil {
			key, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
			if err2 != nil {
				return nil, fmt.Errorf("failed to parse private key (tried PKCS1 and PKCS8): %w", err2)
			}
			rsaKey, ok := key.(*rsa.PrivateKey)
			if !ok {
				return nil, fmt.Errorf("private key is not RSA")
			}
			privKey = rsaKey
		}
		cfg.JWTPrivateKey = privKey

		pubKeyData, err := os.ReadFile(pubKeyPath)
		if err != nil {
			return nil, fmt.Errorf("failed to read public key: %w", err)
		}
		pubBlock, _ := pem.Decode(pubKeyData)
		if pubBlock == nil {
			return nil, fmt.Errorf("failed to decode public key PEM")
		}
		pubKeyParsed, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
		if err != nil {
			return nil, fmt.Errorf("failed to parse public key: %w", err)
		}
		rsaPubKey, ok := pubKeyParsed.(*rsa.PublicKey)
		if !ok {
			return nil, fmt.Errorf("public key is not RSA")
		}
		cfg.JWTPublicKey = rsaPubKey
	}

	return cfg, nil
}

func generateRSAKeys(privPath, pubPath string) (*rsa.PrivateKey, *rsa.PublicKey, error) {
	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, nil, fmt.Errorf("rsa.GenerateKey: %w", err)
	}

	dir := filepath.Dir(privPath)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return nil, nil, err
	}

	privPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privKey),
	})
	if err := os.WriteFile(privPath, privPEM, 0600); err != nil {
		return nil, nil, err
	}

	pubBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
	if err != nil {
		return nil, nil, err
	}
	pubPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pubBytes,
	})
	if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil {
		return nil, nil, err
	}

	fmt.Println("[config] Generated new RSA key pair")
	return privKey, &privKey.PublicKey, nil
}

func getEnv(key, fallback string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return fallback
}

func getEnvInt(key string, fallback int) int {
	if val := os.Getenv(key); val != "" {
		if i, err := strconv.Atoi(val); err == nil {
			return i
		}
	}
	return fallback
}
