// Package config loads and validates all application configuration from the
// environment (optionally seeded from a .env file). The app fails fast at
// startup on invalid configuration rather than misbehaving later.
package config

import (
	"encoding/base64"
	"errors"
	"fmt"
	"net/netip"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

type SMTP struct {
	Host     string
	Port     int
	User     string
	Password string
	From     string
}

func (s SMTP) Enabled() bool { return s.Host != "" }

type OIDC struct {
	IssuerURL    string
	ClientID     string
	ClientSecret string
}

func (o OIDC) Enabled() bool {
	return o.IssuerURL != "" && o.ClientID != "" && o.ClientSecret != ""
}

type Config struct {
	AppBaseURL string
	// AppInternalURL is how the external services reach the app from inside
	// the compose network (callback URLs, signed attachment URLs). Defaults
	// to AppBaseURL.
	AppInternalURL    string
	TrustedProxyCIDRs []netip.Prefix
	AppPort           int
	MongoURI          string
	MongoDB           string
	MongoUsername     string
	MongoPassword     string
	SessionSecret     string
	CredentialsEncKey []byte // 32 bytes, decoded
	MaxUploadMB       int

	AdminEmail           string
	AdminInitialPassword string

	AtomizerBaseURL          string
	AtomizerToken            string
	AtomizerTimeout          time.Duration
	WorkPerformerBaseURL     string
	WorkPerformerToken       string
	WorkPerformerHTTPTimeout time.Duration

	SMTP SMTP
	OIDC OIDC

	CookieSecureMode      string // auto | true | false
	AtomizeMaxConcurrency int
}

// CookieSecure resolves the Secure cookie flag: explicit true/false, or in
// auto mode derived from the APP_BASE_URL scheme.
func (c *Config) CookieSecure() bool {
	switch c.CookieSecureMode {
	case "true":
		return true
	case "false":
		return false
	default:
		return strings.HasPrefix(c.AppBaseURL, "https://")
	}
}

func getenv(key, def string) string {
	if v, ok := os.LookupEnv(key); ok {
		return v
	}
	return def
}

// Load reads configuration from the environment and validates it. All
// problems are reported together so a broken deployment is fixed in one pass.
func Load() (*Config, error) {
	var errs []error
	fail := func(format string, a ...any) { errs = append(errs, fmt.Errorf(format, a...)) }

	atoi := func(key, def string) int {
		raw := getenv(key, def)
		n, err := strconv.Atoi(raw)
		if err != nil {
			fail("%s: %q is not an integer", key, raw)
		}
		return n
	}

	c := &Config{
		AppBaseURL:           strings.TrimRight(getenv("APP_BASE_URL", "http://localhost:8787"), "/"),
		AppPort:              atoi("APP_PORT", "8787"),
		MongoURI:             getenv("MONGO_URI", "mongodb://localhost:27017"),
		MongoDB:              getenv("MONGO_DB", "bountyboard"),
		MongoUsername:        os.Getenv("MONGO_USERNAME"),
		MongoPassword:        os.Getenv("MONGO_PASSWORD"),
		SessionSecret:        os.Getenv("SESSION_SECRET"),
		MaxUploadMB:          atoi("MAX_UPLOAD_MB", "25"),
		AdminEmail:           strings.ToLower(strings.TrimSpace(getenv("ADMIN_EMAIL", ""))),
		AdminInitialPassword: os.Getenv("ADMIN_INITIAL_PASSWORD"),

		AtomizerBaseURL:      strings.TrimRight(getenv("ATOMIZER_BASE_URL", "http://atomizer-mock:8090"), "/"),
		AtomizerToken:        os.Getenv("ATOMIZER_TOKEN"),
		WorkPerformerBaseURL: strings.TrimRight(getenv("WORK_PERFORMER_BASE_URL", "http://work-performer:8091"), "/"),
		WorkPerformerToken:   os.Getenv("WORK_PERFORMER_TOKEN"),

		SMTP: SMTP{
			Host:     os.Getenv("SMTP_HOST"),
			Port:     atoi("SMTP_PORT", "587"),
			User:     os.Getenv("SMTP_USER"),
			Password: os.Getenv("SMTP_PASSWORD"),
			From:     getenv("SMTP_FROM", "bountyboard@example.com"),
		},
		OIDC: OIDC{
			IssuerURL:    os.Getenv("OIDC_ISSUER_URL"),
			ClientID:     os.Getenv("OIDC_CLIENT_ID"),
			ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
		},

		CookieSecureMode:      getenv("COOKIE_SECURE", "auto"),
		AtomizeMaxConcurrency: atoi("ATOMIZE_MAX_CONCURRENCY", "3"),
	}
	c.AppInternalURL = strings.TrimRight(getenv("APP_INTERNAL_URL", c.AppBaseURL), "/")
	c.AtomizerTimeout = time.Duration(atoi("ATOMIZER_TIMEOUT_SEC", "120")) * time.Second
	c.WorkPerformerHTTPTimeout = time.Duration(atoi("WORK_PERFORMER_HTTP_TIMEOUT_SEC", "30")) * time.Second

	if c.AppPort < 1 || c.AppPort > 65535 {
		fail("APP_PORT: %d out of range", c.AppPort)
	}
	if u, err := url.Parse(c.AppBaseURL); err != nil || u.Scheme == "" || u.Host == "" {
		fail("APP_BASE_URL: %q is not an absolute URL", c.AppBaseURL)
	}
	for _, name := range []string{"ATOMIZER_BASE_URL", "WORK_PERFORMER_BASE_URL"} {
		v := c.AtomizerBaseURL
		if name == "WORK_PERFORMER_BASE_URL" {
			v = c.WorkPerformerBaseURL
		}
		if u, err := url.Parse(v); err != nil || u.Scheme == "" || u.Host == "" {
			fail("%s: %q is not an absolute URL", name, v)
		}
	}
	if len(c.SessionSecret) < 16 {
		fail("SESSION_SECRET: must be at least 16 characters of random data")
	}
	switch raw := os.Getenv("CREDENTIALS_ENC_KEY"); raw {
	case "":
		fail("CREDENTIALS_ENC_KEY: required; generate with `openssl rand -base64 32`")
	default:
		key, err := base64.StdEncoding.DecodeString(raw)
		if err != nil || len(key) != 32 {
			fail("CREDENTIALS_ENC_KEY: must be base64 of exactly 32 bytes")
		}
		c.CredentialsEncKey = key
	}
	if c.MaxUploadMB < 1 {
		fail("MAX_UPLOAD_MB: must be >= 1")
	}
	if c.AtomizeMaxConcurrency < 1 {
		fail("ATOMIZE_MAX_CONCURRENCY: must be >= 1")
	}
	switch c.CookieSecureMode {
	case "auto", "true", "false":
	default:
		fail("COOKIE_SECURE: %q must be auto, true or false", c.CookieSecureMode)
	}
	if raw := strings.TrimSpace(os.Getenv("TRUSTED_PROXY_CIDRS")); raw != "" {
		for cidr := range strings.SplitSeq(raw, ",") {
			cidr = strings.TrimSpace(cidr)
			if cidr == "" {
				continue
			}
			p, err := netip.ParsePrefix(cidr)
			if err != nil {
				fail("TRUSTED_PROXY_CIDRS: %q is not a CIDR", cidr)
				continue
			}
			c.TrustedProxyCIDRs = append(c.TrustedProxyCIDRs, p.Masked())
		}
	}

	if len(errs) > 0 {
		return nil, fmt.Errorf("invalid configuration:\n%w", errors.Join(errs...))
	}
	return c, nil
}
