// Package config parses and validates admin's environment.
//
// Everything required is checked at boot and the process refuses to start
// without it. A licensing service that cannot sign is worse than one that is
// down, because it looks healthy.
package config

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

type Config struct {
	AdminMongoURI   string
	AdminDBName     string
	ControlMongoURI string
	ControlDBName   string
	RedisAddr       string
	RedisUsername   string
	RedisPassword   string
	SigningKey      string
	PublicURL       string
	AppLoginURL     string
	AllowedOrigins  []string
	TrustProxy      bool
	Addr            string
	ReapAfter       time.Duration

	PaddleEnv           string // "sandbox" or "production"
	PaddleAPIKey        string
	PaddleWebhookSecret string

	SMTPHost     string
	SMTPPort     string
	SMTPFrom     string
	SMTPUsername string
	SMTPPassword string
}

// dbNameFromURI reads the database from a Mongo URI path.
//
// Both URIs must name their database inline rather than through a separate
// variable. Admin talks to two databases; a bare MONGO_DB would be ambiguous
// about which, and guessing wrong means writing licence fields into the wrong
// place.
func dbNameFromURI(raw, which string) (string, error) {
	u, err := url.Parse(raw)
	if err != nil {
		return "", fmt.Errorf("%s is not a valid URI: %w", which, err)
	}
	name := strings.TrimPrefix(u.Path, "/")
	if name == "" {
		return "", fmt.Errorf("%s must name a database in its path, e.g. mongodb://host:27017/vantage_admin", which)
	}
	return name, nil
}

func Load() (Config, error) {
	c := Config{
		AdminMongoURI:   os.Getenv("ADMIN_MONGO_URI"),
		ControlMongoURI: os.Getenv("CONTROL_MONGO_URI"),
		RedisAddr:       os.Getenv("REDIS_ADDR"),
		// Optional: an unauthenticated Redis needs neither. Redis 6+ ACL auth
		// takes both; a legacy `requirepass` instance takes the password alone
		// and must leave the username empty.
		RedisUsername: os.Getenv("REDIS_USERNAME"),
		RedisPassword: os.Getenv("REDIS_PASSWORD"),
		SigningKey:    os.Getenv("LICENSE_SIGNING_KEY"),
		PublicURL:     strings.TrimSuffix(os.Getenv("PUBLIC_URL"), "/"),
		AppLoginURL:   os.Getenv("APP_LOGIN_URL"),
		TrustProxy:    strings.EqualFold(os.Getenv("TRUST_PROXY"), "true"),
		Addr:          ":" + envOr("PORT", "8083"),

		SMTPHost:     os.Getenv("SMTP_HOST"),
		SMTPPort:     envOr("SMTP_PORT", "587"),
		SMTPFrom:     os.Getenv("SMTP_FROM"),
		SMTPUsername: os.Getenv("SMTP_USERNAME"),
		SMTPPassword: os.Getenv("SMTP_PASSWORD"),

		PaddleEnv:           envOr("PADDLE_ENV", "sandbox"),
		PaddleAPIKey:        os.Getenv("PADDLE_API_KEY"),
		PaddleWebhookSecret: os.Getenv("PADDLE_WEBHOOK_SECRET"),
	}

	var missing []string
	for name, v := range map[string]string{
		"ADMIN_MONGO_URI":     c.AdminMongoURI,
		"CONTROL_MONGO_URI":   c.ControlMongoURI,
		"REDIS_ADDR":          c.RedisAddr,
		"LICENSE_SIGNING_KEY": c.SigningKey,
		"PUBLIC_URL":          c.PublicURL,
		"ADMIN_ORIGIN":        os.Getenv("ADMIN_ORIGIN"),
		// An unverified webhook endpoint is one anyone can issue licences
		// through, so the secret and API key are boot-required.
		"PADDLE_API_KEY":        c.PaddleAPIKey,
		"PADDLE_WEBHOOK_SECRET": c.PaddleWebhookSecret,
	} {
		if v == "" {
			missing = append(missing, name)
		}
	}
	if len(missing) > 0 {
		return Config{}, fmt.Errorf("missing required environment: %s", strings.Join(missing, ", "))
	}

	var err error
	if c.AdminDBName, err = dbNameFromURI(c.AdminMongoURI, "ADMIN_MONGO_URI"); err != nil {
		return Config{}, err
	}
	if c.ControlDBName, err = dbNameFromURI(c.ControlMongoURI, "CONTROL_MONGO_URI"); err != nil {
		return Config{}, err
	}
	if c.AdminMongoURI == c.ControlMongoURI {
		return Config{}, fmt.Errorf("ADMIN_MONGO_URI and CONTROL_MONGO_URI must not be the same database")
	}

	for _, o := range strings.Split(os.Getenv("ADMIN_ORIGIN"), ",") {
		if o = strings.TrimSpace(o); o != "" {
			c.AllowedOrigins = append(c.AllowedOrigins, o)
		}
	}

	// Mirrors the control plane's FREE_INSTANCE_REAP_AFTER so notice emails can
	// name the real deletion date. An unparseable value is refused rather than
	// silently treated as "off": a typo here would quietly stop every deletion
	// warning while the control plane still deletes.
	if v := os.Getenv("FREE_INSTANCE_REAP_AFTER"); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil {
			return Config{}, fmt.Errorf("FREE_INSTANCE_REAP_AFTER %q: %w", v, err)
		}
		c.ReapAfter = d
	}
	return c, nil
}

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