package config

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

	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/alertstore/pkg/jira"
)

type Config struct {
	Server    ServerConfig    `yaml:"server"`
	Database  DatabaseConfig  `yaml:"database"`
	IM        IMConfig        `yaml:"im"`
	Clinic    ClinicConfig    `yaml:"clinic"`
	DevOps    DevOpsConfig    `yaml:"devops"`
	Oncall    OncallConfig    `yaml:"oncall"`
	Directory DirectoryConfig `yaml:"directory"`
	Alert     AlertConfig     `yaml:"alert"`
	Agent     AgentConfig     `yaml:"agent"`
	Webhook   WebhookConfig   `yaml:"webhook"`
	// Cutover and Jira are transitional and deleted along with jiralert.
	Cutover CutoverConfig `yaml:"cutover"`
	Jira    jira.Config   `yaml:"jira"`
}

// CutoverConfig is not part of the Jira config: ownership decides everything
// alertstore does with a human or an external system, not just the Jira issue.
type CutoverConfig struct {
	// OwnEverything ignores the per-rule canary label. Correct once jiralert no
	// longer responds to this deployment's feed at all.
	OwnEverything bool `yaml:"own_everything"`
}

type ServerConfig struct {
	Addr             string `yaml:"addr"`
	WebUIBaseURL     string `yaml:"web_ui_base_url"`
	OpsPortalBaseURL string `yaml:"ops_portal_base_url"`
}

type DatabaseConfig struct {
	DSN string `yaml:"dsn"`
}

type IMConfig struct {
	Provider      string `yaml:"provider"` // "lark"
	MainChannelID string `yaml:"main_channel_id"`
	AppID         string `yaml:"app_id"`
	AppSecret     string `yaml:"app_secret"`
	LarkBaseURL   string `yaml:"lark_base_url"`
}

type ClinicConfig struct {
	BaseURL        string        `yaml:"base_url"`
	Token          string        `yaml:"token"`
	ActivityURL    string        `yaml:"activity_url"`
	ActivitySource string        `yaml:"activity_source"`
	ActivityKey    string        `yaml:"activity_key"`
	Timeout        time.Duration `yaml:"timeout"`
}

// DevOpsConfig points at the DevOps ops portal, which owns the commercial view
// of a tenant (see pkg/devops). An empty base_url disables the lookup: alerts
// then carry no biz region and no tenant kind, and everything else about them
// is unchanged.
//
// api_token is empty in-cluster, where alertstore reads the portal's internal
// route and nothing authenticates it. It exists for the deployment that has to
// go through the shared gateway instead.
type DevOpsConfig struct {
	BaseURL  string        `yaml:"base_url"`
	APIToken string        `yaml:"api_token"`
	Timeout  time.Duration `yaml:"timeout"`
	CacheTTL time.Duration `yaml:"cache_ttl"`
}

// OncallConfig points at the oncall v2 service (services/oncall). An empty
// base_url disables on-call lookup: alerts are then created unassigned.
type OncallConfig struct {
	BaseURL string `yaml:"base_url"`
	APIKey  string `yaml:"api_key"`
	// DefaultTeam owns alerts that carry no oncall_team label.
	DefaultTeam string        `yaml:"default_team"`
	Timeout     time.Duration `yaml:"timeout"`
	CacheTTL    time.Duration `yaml:"cache_ttl"`
	// IgnoreAssignLabel drops the per-rule `assign` label so `assign: none`
	// and explicit-email overrides are ignored and every alert routes through
	// the normal duty schedule. Set in non-prod, where production alert rules'
	// opt-outs and real-engineer emails must not leak into a test deployment.
	IgnoreAssignLabel bool `yaml:"ignore_assign_label"`
}

// DirectoryConfig points at core-server's user directory
// (services/core-server), the mirror of the Lark org chart the web UI searches
// when someone reassigns an alert. An empty base_url disables the suggestions;
// the assignee field is free text either way.
type DirectoryConfig struct {
	BaseURL  string        `yaml:"base_url"`
	Timeout  time.Duration `yaml:"timeout"`
	CacheTTL time.Duration `yaml:"cache_ttl"`
}

type AlertConfig struct {
	ResponseDeadline time.Duration `yaml:"response_deadline"`
	RecoveryDeadline time.Duration `yaml:"recovery_deadline"`
}

// AgentConfig governs dispatch to agents. An empty callback_base_url disables
// it: no alert is dispatched, and everything else about the alert path is
// unchanged — which is the state alertstore ran in before agents existed and
// the state it must degrade to whenever they are unavailable.
type AgentConfig struct {
	// CallbackBaseURL is alertstore's own API root as an agent can reach it
	// (usually the in-cluster service address). It is where a dispatched agent
	// writes its result back to, so it is required for dispatch to mean
	// anything at all.
	CallbackBaseURL string        `yaml:"callback_base_url"`
	Timeout         time.Duration `yaml:"timeout"`
	// DispatchHMACSecret signs the dispatch body so an agent behind a public
	// ingress can tell a real dispatch from a forged or replayed one. Empty
	// leaves dispatches unsigned, which is correct for an in-cluster agent whose
	// only reachable path is the private network. Wire it from a secret via
	// ${ENV} rather than committing it here.
	DispatchHMACSecret string `yaml:"dispatch_hmac_secret"`
	// HoldRate is the share of runs whose verdict is withheld from human
	// surfaces until the resolver has recorded their own. The held slice is the
	// only population where the agent's verdict and the human's are
	// independent, which is the only place agreement between them measures
	// accuracy rather than persuasion.
	//
	// It is 0 until agent output is rendered anywhere a resolver can see it.
	// From that release on it must be non-zero, and it cannot be applied
	// retroactively: labels collected while every verdict was visible are
	// contaminated for good.
	HoldRate float64 `yaml:"hold_rate"`
}

type WebhookConfig struct {
	AlertmanagerTokens []WebhookToken `yaml:"alertmanager_tokens"`

	// Deprecated: legacy single bare token, used only when AlertmanagerTokens
	// is empty. Kept so an unmigrated config.yaml still authorizes.
	AlertmanagerToken string `yaml:"alertmanager_token"`
}

// WebhookToken is one accepted bearer credential. Name is for observability
// only; access is by Secret.
type WebhookToken struct {
	Name   string `yaml:"name"`
	Secret string `yaml:"secret"`
}

// Tokens returns the configured tokens, trimmed, dropping empty secrets and
// defaulting a blank name to "default", falling back to AlertmanagerToken when
// the list is empty.
func (w WebhookConfig) Tokens() []WebhookToken {
	var out []WebhookToken
	for _, t := range w.AlertmanagerTokens {
		name, secret := strings.TrimSpace(t.Name), strings.TrimSpace(t.Secret)
		if secret == "" {
			continue
		}
		if name == "" {
			name = "default"
		}
		out = append(out, WebhookToken{Name: name, Secret: secret})
	}
	if len(out) == 0 {
		if secret := strings.TrimSpace(w.AlertmanagerToken); secret != "" {
			out = append(out, WebhookToken{Name: "default", Secret: secret})
		}
	}
	return out
}

// Load reads YAML files onto one Config in order, so a later file (a secrets
// overlay) overrides only the fields it sets. ${ENV} is expanded in each.
func Load(paths ...string) (*Config, error) {
	cfg := &Config{}
	for _, path := range paths {
		data, err := os.ReadFile(path)
		if err != nil {
			return nil, fmt.Errorf("read config file %q: %w", path, err)
		}
		data = []byte(os.ExpandEnv(string(data)))
		if err := yaml.Unmarshal(data, cfg); err != nil {
			return nil, fmt.Errorf("parse config %q: %w", path, err)
		}
	}

	if cfg.Server.Addr == "" {
		cfg.Server.Addr = ":8080"
	}
	if cfg.Server.WebUIBaseURL == "" {
		// Alertstore is currently dev-only. Keep local/dev deployments useful
		// while allowing later environments to override this explicitly.
		cfg.Server.WebUIBaseURL = "https://alerts.ops-dev.pingcap.net/ui/"
	}
	if cfg.Server.OpsPortalBaseURL == "" {
		cfg.Server.OpsPortalBaseURL = "https://ops.tidbcloud.com"
	}
	if cfg.Agent.Timeout <= 0 {
		// The dispatch POST, not the agent's work: the far side is expected to
		// accept the alert and answer later through the callback. A handoff that
		// takes longer than this is a broken endpoint, not a thorough one.
		cfg.Agent.Timeout = 10 * time.Second
	}

	return cfg, nil
}
