// Package config loads the environment configuration for the two Telegram
// platform binaries: the home validator (HMAC of Mini App / Login Widget data, no
// Telegram egress) and the remote bot (Bot API long-poll + sendMessage, dialing
// the gateway over the reverse mTLS bot-link). The bot token lives in both
// processes — the validator needs it only as the HMAC secret (ARCHITECTURE.md §12).
package config

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	pkgtel "scrabble/pkg/telemetry"
)

// ValidatorConfig is the home validator's runtime configuration.
type ValidatorConfig struct {
	// Token is the Telegram Bot API token (TELEGRAM_BOT_TOKEN, required). The
	// validator uses it only as the HMAC secret for Mini App initData and Login
	// Widget validation; it never calls the Bot API.
	Token string
	// GRPCAddr is the listen address of the validator gRPC server the gateway calls
	// (TELEGRAM_VALIDATOR_GRPC_ADDR, default :9091).
	GRPCAddr string
	// LogLevel is the zap log level (TELEGRAM_LOG_LEVEL, default info).
	LogLevel string
	// Telemetry configures the OpenTelemetry providers (shared bootstrap).
	Telemetry pkgtel.Config
}

// BotConfig is the remote bot's runtime configuration.
type BotConfig struct {
	// Token is the Telegram Bot API token (TELEGRAM_BOT_TOKEN, required).
	Token string
	// GameChannelID is the chat id of the bot's game channel for the admin channel
	// post (TELEGRAM_GAME_CHANNEL_ID, optional; 0 disables channel posts).
	GameChannelID int64
	// ChatID is the chat id of the moderated discussion supergroup (a channel's linked
	// chat) whose write access the bot gates by registration and moderation
	// (TELEGRAM_CHAT_ID, optional; 0 disables chat gating). The bot must be an admin
	// there with the "Ban users" right, and "chat_member" in its allowed updates.
	ChatID int64
	// SupportChatID is the chat id of the private forum supergroup the bot relays
	// direct user messages into — one forum topic per user — and reads operator
	// replies from (TELEGRAM_SUPPORT_CHAT_ID, optional; 0 disables the support relay).
	// The bot must be an administrator there with the manage-topics and
	// delete-messages rights. Distinct from ChatID (the moderated discussion chat).
	SupportChatID int64
	// SupportStateDir is the directory holding the support relay's JSON state file
	// (TELEGRAM_SUPPORT_STATE_DIR, default /data). It must be writable by the
	// container user (UID 65532) and backed by a persistent volume.
	SupportStateDir string
	// StarsOutboxDir is the directory holding the Telegram Stars payment outbox SQLite file
	// (TELEGRAM_STARS_OUTBOX_DIR, optional; empty disables the Stars rail). It must be writable by
	// the container user (UID 65532) and backed by a persistent volume — a lost outbox loses any
	// payment not yet forwarded to the gateway.
	StarsOutboxDir string
	// PromoBotToken is the API token of the optional standalone promo bot run in this
	// container — a second bot whose only job is to answer /start with a button that
	// opens the main bot's Mini App (TELEGRAM_PROMO_BOT_TOKEN, optional; empty disables
	// the promo bot).
	PromoBotToken string
	// BotUsername is the main bot's @username without the leading @, used in the promo
	// bot's message text (TELEGRAM_BOT_USERNAME; required when the promo bot runs).
	BotUsername string
	// BotLinkURL is the main bot's Mini App direct link — the same value the UI builds
	// share links from (VITE_TELEGRAM_LINK), e.g. https://telegram.me/<bot>/<app>. The promo
	// button appends ?startapp=<payload> to it (TELEGRAM_BOT_LINK; required when the
	// promo bot runs). It is distinct from the BotLink mTLS dial config below.
	BotLinkURL string
	// PromoStartParam is the promo button's launch payload, appended as
	// ?startapp=<PromoStartParam> (TELEGRAM_PROMO_START_PARAM, default
	// "verudit_ru-scrabble_en"). It is a variant-seed deep link the backend decodes to
	// seed a brand-new user's variant preferences; its labels must match the backend's
	// known variants. Empty falls back to forwarding the user's own /start payload.
	PromoStartParam string
	// MiniAppURL is the HTTPS origin of the Mini App registered with BotFather; it is
	// the base of every launch button (TELEGRAM_MINIAPP_URL, required).
	MiniAppURL string
	// APIBaseURL overrides the Bot API host (TELEGRAM_API_BASE_URL, optional;
	// default https://api.telegram.org).
	APIBaseURL string
	// TestEnv routes the Bot API client to Telegram's test environment
	// (TELEGRAM_TEST_ENV=true, default false).
	TestEnv bool
	// OwnsUpdates reports whether this bot runs the exclusive getUpdates long-poll
	// (TELEGRAM_OWNS_UPDATES, default true). Exactly one bot per token must own it.
	OwnsUpdates bool
	// SendRatePerSecond caps outbound Bot API sends to respect Telegram flood limits
	// (TELEGRAM_SEND_RATE_PER_SECOND, default 25; 0 disables the limiter).
	SendRatePerSecond int
	// BotLink configures the reverse mTLS channel the bot dials.
	BotLink BotLinkClientConfig
	// LogLevel is the zap log level (TELEGRAM_LOG_LEVEL, default info).
	LogLevel string
	// Telemetry configures the OpenTelemetry providers (shared bootstrap).
	Telemetry pkgtel.Config
}

// BotLinkClientConfig is the bot's dial side of the reverse bot-link.
type BotLinkClientConfig struct {
	// GatewayAddr is the gateway bot-link endpoint to dial (TELEGRAM_GATEWAY_ADDR,
	// required), e.g. "gateway.example.com:9443".
	GatewayAddr string
	// ServerName is the gateway certificate's expected SNI / CN
	// (TELEGRAM_BOTLINK_SERVER_NAME, required).
	ServerName string
	// InstanceID identifies this bot to the gateway (TELEGRAM_INSTANCE_ID, default
	// the hostname).
	InstanceID string
	// CertFile, KeyFile and CAFile are the bot client certificate, its key and the
	// CA bundle that signs the gateway server certificate (required).
	CertFile string
	KeyFile  string
	CAFile   string
	// ReconnectDelay is the pause before re-dialing after the stream ends
	// (TELEGRAM_BOTLINK_RECONNECT_DELAY, default 2s).
	ReconnectDelay time.Duration
}

const (
	defaultValidatorGRPCAddr = ":9091"
	defaultBotReconnectDelay = 2 * time.Second
	defaultSendRatePerSecond = 25
	// defaultPromoStartParam is the promo button's default launch payload: a variant-seed
	// deep link the backend decodes to add English Scrabble to a brand-new user's variant
	// preferences alongside the default Erudit. Override with TELEGRAM_PROMO_START_PARAM.
	defaultPromoStartParam = "verudit_ru-scrabble_en"
)

// LoadValidator reads the validator configuration from the environment.
func LoadValidator() (ValidatorConfig, error) {
	cfg := ValidatorConfig{
		Token:    os.Getenv("TELEGRAM_BOT_TOKEN"),
		GRPCAddr: envOr("TELEGRAM_VALIDATOR_GRPC_ADDR", defaultValidatorGRPCAddr),
		LogLevel: envOr("TELEGRAM_LOG_LEVEL", "info"),
	}
	tel, err := loadTelemetry("scrabble-telegram-validator")
	if err != nil {
		return ValidatorConfig{}, err
	}
	cfg.Telemetry = tel
	if cfg.Token == "" {
		return ValidatorConfig{}, fmt.Errorf("config: TELEGRAM_BOT_TOKEN is required")
	}
	return cfg, nil
}

// LoadBot reads the bot configuration from the environment.
func LoadBot() (BotConfig, error) {
	cfg := BotConfig{
		Token:             os.Getenv("TELEGRAM_BOT_TOKEN"),
		MiniAppURL:        os.Getenv("TELEGRAM_MINIAPP_URL"),
		APIBaseURL:        os.Getenv("TELEGRAM_API_BASE_URL"),
		TestEnv:           os.Getenv("TELEGRAM_TEST_ENV") == "true",
		OwnsUpdates:       os.Getenv("TELEGRAM_OWNS_UPDATES") != "false",
		SendRatePerSecond: defaultSendRatePerSecond,
		PromoBotToken:     os.Getenv("TELEGRAM_PROMO_BOT_TOKEN"),
		BotUsername:       strings.TrimPrefix(os.Getenv("TELEGRAM_BOT_USERNAME"), "@"),
		BotLinkURL:        os.Getenv("TELEGRAM_BOT_LINK"),
		PromoStartParam:   envOr("TELEGRAM_PROMO_START_PARAM", defaultPromoStartParam),
		SupportStateDir:   envOr("TELEGRAM_SUPPORT_STATE_DIR", "/data"),
		StarsOutboxDir:    os.Getenv("TELEGRAM_STARS_OUTBOX_DIR"),
		LogLevel:          envOr("TELEGRAM_LOG_LEVEL", "info"),
		BotLink: BotLinkClientConfig{
			GatewayAddr: os.Getenv("TELEGRAM_GATEWAY_ADDR"),
			ServerName:  os.Getenv("TELEGRAM_BOTLINK_SERVER_NAME"),
			InstanceID:  envOr("TELEGRAM_INSTANCE_ID", hostname()),
			CertFile:    os.Getenv("TELEGRAM_BOTLINK_TLS_CERT"),
			KeyFile:     os.Getenv("TELEGRAM_BOTLINK_TLS_KEY"),
			CAFile:      os.Getenv("TELEGRAM_BOTLINK_TLS_CA"),
		},
	}
	var err error
	if cfg.GameChannelID, err = envInt64("TELEGRAM_GAME_CHANNEL_ID", 0); err != nil {
		return BotConfig{}, err
	}
	if cfg.ChatID, err = envInt64("TELEGRAM_CHAT_ID", 0); err != nil {
		return BotConfig{}, err
	}
	if cfg.SupportChatID, err = envInt64("TELEGRAM_SUPPORT_CHAT_ID", 0); err != nil {
		return BotConfig{}, err
	}
	if cfg.SendRatePerSecond, err = envInt("TELEGRAM_SEND_RATE_PER_SECOND", defaultSendRatePerSecond); err != nil {
		return BotConfig{}, err
	}
	if cfg.BotLink.ReconnectDelay, err = envDuration("TELEGRAM_BOTLINK_RECONNECT_DELAY", defaultBotReconnectDelay); err != nil {
		return BotConfig{}, err
	}
	tel, err := loadTelemetry("scrabble-telegram-bot")
	if err != nil {
		return BotConfig{}, err
	}
	cfg.Telemetry = tel

	if cfg.Token == "" {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_BOT_TOKEN is required")
	}
	if cfg.MiniAppURL == "" {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_MINIAPP_URL is required")
	}
	if cfg.BotLink.GatewayAddr == "" {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_GATEWAY_ADDR is required")
	}
	if cfg.BotLink.ServerName == "" {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_BOTLINK_SERVER_NAME is required")
	}
	if cfg.BotLink.CertFile == "" || cfg.BotLink.KeyFile == "" || cfg.BotLink.CAFile == "" {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_BOTLINK_TLS_CERT, _KEY and _CA are required")
	}
	if cfg.PromoBotToken != "" && (cfg.BotUsername == "" || cfg.BotLinkURL == "") {
		return BotConfig{}, fmt.Errorf("config: TELEGRAM_BOT_USERNAME and TELEGRAM_BOT_LINK are required when TELEGRAM_PROMO_BOT_TOKEN is set")
	}
	return cfg, nil
}

// loadTelemetry builds the shared OpenTelemetry config with the given default
// service name, applying the TELEGRAM_* overrides and validating the result.
func loadTelemetry(defaultService string) (pkgtel.Config, error) {
	tel := pkgtel.DefaultConfig(defaultService)
	tel.ServiceName = envOr("TELEGRAM_SERVICE_NAME", tel.ServiceName)
	tel.TracesExporter = envOr("TELEGRAM_OTEL_TRACES_EXPORTER", tel.TracesExporter)
	tel.MetricsExporter = envOr("TELEGRAM_OTEL_METRICS_EXPORTER", tel.MetricsExporter)
	if err := tel.Validate(); err != nil {
		return pkgtel.Config{}, fmt.Errorf("config: %w", err)
	}
	return tel, nil
}

// hostname returns the machine hostname, or "telegram-bot" when it cannot be read.
func hostname() string {
	if h, err := os.Hostname(); err == nil && h != "" {
		return h
	}
	return "telegram-bot"
}

// envOr returns the environment value for key, or def when it is unset or empty.
func envOr(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

// envInt parses the environment variable named key as an int, returning fallback
// when unset and an error when set but malformed.
func envInt(key string, fallback int) (int, error) {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback, nil
	}
	n, err := strconv.Atoi(v)
	if err != nil {
		return 0, fmt.Errorf("config: %s %q: %w", key, v, err)
	}
	return n, nil
}

// envInt64 parses the environment variable named key as an int64, returning
// fallback when unset and an error when set but malformed.
func envInt64(key string, fallback int64) (int64, error) {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback, nil
	}
	n, err := strconv.ParseInt(v, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("config: %s %q: %w", key, v, err)
	}
	return n, nil
}

// envDuration parses the environment variable named key as a Go duration,
// returning fallback when unset and an error when set but malformed.
func envDuration(key string, fallback time.Duration) (time.Duration, error) {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback, nil
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		return 0, fmt.Errorf("config: %s %q: %w", key, v, err)
	}
	return d, nil
}
