use clap::{Parser, Subcommand};

pub use mcpr_core::proxy::{CspMode, parse_csp_mode};
use mcpr_tunnel::RelayConfig;

const CONFIG_FILE: &str = "mcpr.toml";

// ── Run mode ────────────────────────────────────────────────────────────

/// Top-level mode: either run as a relay server or as the gateway proxy.
#[allow(dead_code)]
pub enum Mode {
    Relay(RelayConfig),
    Gateway(Box<GatewayConfig>),
}

/// Result of parsing CLI args — either a subcommand action or the run mode.
pub enum CliAction {
    /// Start the daemon supervisor (no config needed).
    Start {
        foreground: bool,
    },
    /// Stop the running daemon via SIGTERM.
    Stop,
    /// Stop + start the daemon, re-launching previously running proxies and relay.
    Restart {
        restart_proxies: Vec<String>,
        restart_relay: bool,
    },
    /// Show daemon status (PID, port, uptime, proxy name).
    Status,
    Validate(ValidateArgs),
    Version,
    /// Update mcpr to the latest version.
    Update,
    /// Read-only query against the store — no server needed.
    Proxy(ProxyCommand),
    /// Run a proxy as a background process (fork before tokio).
    ProxyRun {
        mode: Mode,
        replace: bool,
        /// Raw TOML content for config snapshot.
        config_content: String,
        /// Absolute path to the original config file.
        config_path: String,
    },
    /// Interactive setup wizard (needs async for cloud API calls).
    ProxySetup {
        cloud_url: String,
        output: Option<String>,
    },
    /// Store maintenance commands.
    Store(StoreCommand),
    /// Relay subcommands (stop, restart, status — no config needed).
    Relay(RelayCommand),
    /// Run the relay server (foreground or daemonized).
    RelayRun {
        relay_config: RelayConfig,
        /// Raw TOML content for config snapshot.
        config_content: String,
        /// Absolute path to the original config file.
        config_path: String,
        /// `true` for `relay run` (foreground), `false` for `relay start` (daemonize).
        foreground: bool,
    },
}

// ── Log format ──────────────────────────────────────────────────────────

/// Log output format for stderr.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LogFormat {
    #[default]
    Json,
    Pretty,
}

impl std::str::FromStr for LogFormat {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "json" => Ok(LogFormat::Json),
            "pretty" | "text" => Ok(LogFormat::Pretty),
            _ => Err(format!("unknown log format: {s} (expected: json, pretty)")),
        }
    }
}

impl std::fmt::Display for LogFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LogFormat::Json => write!(f, "json"),
            LogFormat::Pretty => write!(f, "pretty"),
        }
    }
}

// ── CLI args ────────────────────────────────────────────────────────────

#[derive(Parser)]
#[command(
    name = "mcpr",
    version,
    about = "Open-source proxy for MCP Apps — fixes CSP, handles auth, observes every tool call."
)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Path to config file (default: ./mcpr.toml)
    #[arg(long, short, global = true)]
    config: Option<String>,
}

#[derive(Subcommand)]
enum Commands {
    /// Start the proxy (daemon by default, --foreground for attached mode)
    Start(StartArgs),
    /// Stop the running daemon
    Stop,
    /// Restart the daemon (stop + start)
    Restart,
    /// Show daemon status (PID, port, uptime, proxy name)
    Status,
    /// Validate config file and exit
    Validate(ValidateArgs),
    /// Print version information and exit
    Version,
    /// Update mcpr to the latest version
    Update,
    /// Query proxy observability data (logs, slow calls, stats, sessions, clients)
    Proxy(ProxyArgs),
    /// Storage maintenance (stats, vacuum)
    Store(StoreArgs),
    /// Relay server lifecycle (run, start, stop, restart, status)
    Relay(RelayArgs),
}

/// Arguments for `mcpr start`.
#[derive(Parser, Clone)]
pub struct StartArgs {
    /// Run in foreground instead of daemonizing (for Docker, systemd, debugging)
    #[arg(long)]
    pub foreground: bool,
}

// ── Proxy query subcommands ────────────────────────────────────────────

#[derive(Parser)]
pub struct ProxyArgs {
    #[command(subcommand)]
    pub command: ProxyCommand,
}

/// Proxy lifecycle and observability commands.
#[derive(Subcommand, Clone)]
pub enum ProxyCommand {
    // ── Lifecycle ─────────────────────────────────────────────────────
    /// Run a proxy in the background from a config file
    Run(ProxyRunArgs),
    /// Stop a running proxy (or all proxies with --all)
    Stop(ProxyStopArgs),
    /// Restart a running proxy from its saved config (or all with --all)
    Restart(ProxyRestartArgs),
    /// Start a stopped proxy by name (from its saved config snapshot)
    Start(ProxyStartArgs),
    /// List all known proxies and their status
    List(ProxyListArgs),

    // ── Observability ────────────────────────────────────────────────
    /// Show recent request logs
    Logs(ProxyLogsArgs),
    /// Show slowest requests above a latency threshold
    Slow(ProxySlowArgs),
    /// Show per-tool aggregated metrics
    Stats(ProxyStatsArgs),
    /// List MCP sessions with client info
    Sessions(ProxySessionsArgs),
    /// Show client (AI model) breakdown
    Clients(ProxyClientsArgs),
    /// Show proxy status overview (running proxies, activity, errors)
    Status(ProxyStatusArgs),
    /// Drill into a single session — show session info and all its requests
    Session(ProxySessionArgs),
    /// Show captured MCP server schema (tools, resources, prompts)
    Schema(ProxySchemaArgs),

    // ── Onboarding ──────────────────────────────────────────────────
    /// Interactive setup wizard — authenticate, pick project, generate config
    Setup(ProxySetupArgs),
}

// ── Proxy lifecycle args ──────────────────────────────────────────────

/// Arguments for `mcpr proxy run [config]`.
#[derive(Parser, Clone)]
pub struct ProxyRunArgs {
    /// Config file path (default: mcpr.toml)
    #[arg(long, short)]
    pub config: Option<String>,

    /// Replace existing proxy with the same name
    #[arg(long)]
    pub replace: bool,
}

/// Arguments for `mcpr proxy stop [name]`.
#[derive(Parser, Clone)]
pub struct ProxyStopArgs {
    /// Proxy name to stop
    pub name: Option<String>,

    /// Stop all running proxies
    #[arg(long)]
    pub all: bool,
}

/// Arguments for `mcpr proxy restart [name]`.
#[derive(Parser, Clone)]
pub struct ProxyRestartArgs {
    /// Proxy name to restart from its saved config snapshot
    pub name: Option<String>,

    /// Restart all running proxies
    #[arg(long)]
    pub all: bool,
}

/// Arguments for `mcpr proxy start <name>`.
#[derive(Parser, Clone)]
pub struct ProxyStartArgs {
    /// Proxy name to start
    pub name: String,
}

/// Arguments for `mcpr proxy list`.
#[derive(Parser, Clone)]
pub struct ProxyListArgs {
    /// Output as JSON (one object per proxy)
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy logs [name]`.
#[derive(Parser, Clone)]
pub struct ProxyLogsArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Number of recent rows to show
    #[arg(long, default_value = "50")]
    pub tail: i64,

    /// Time window: only rows newer than this duration (e.g., 1h, 30m, 7d).
    /// Defaults to 1h, or all time when --session is used.
    #[arg(long)]
    pub since: Option<String>,

    /// Filter to a specific tool name
    #[arg(long)]
    pub tool: Option<String>,

    /// Filter by MCP method (e.g., tools/call, resources/read, initialize)
    #[arg(long)]
    pub method: Option<String>,

    /// Filter by session ID (supports prefix matching, e.g. first 8 chars)
    #[arg(long)]
    pub session: Option<String>,

    /// Filter by status: ok, error, timeout
    #[arg(long)]
    pub status: Option<String>,

    /// Filter by JSON-RPC error code (e.g., -32601 for method not found)
    #[arg(long)]
    pub error_code: Option<String>,

    /// Output as newline-delimited JSON (one object per line)
    #[arg(long)]
    pub json: bool,

    /// Poll for new rows every 500ms (like tail -f)
    #[arg(short, long)]
    pub follow: bool,
}

/// Arguments for `mcpr proxy slow [name]`.
#[derive(Parser, Clone)]
pub struct ProxySlowArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Minimum latency to include (e.g., 200us, 500ms, 1s). Default: 500ms
    #[arg(long, default_value = "500ms")]
    pub threshold: String,

    /// Time window (e.g., 1h, 24h)
    #[arg(long, default_value = "1h")]
    pub since: String,

    /// Filter to a specific tool name
    #[arg(long)]
    pub tool: Option<String>,

    /// Maximum rows to return
    #[arg(long, default_value = "20")]
    pub limit: i64,

    /// Output as newline-delimited JSON
    #[arg(long)]
    pub json: bool,

    /// Poll for new slow calls every 1s (like tail -f)
    #[arg(short, long)]
    pub follow: bool,
}

/// Arguments for `mcpr proxy stats [name]`.
#[derive(Parser, Clone)]
pub struct ProxyStatsArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Aggregation window (e.g., 1h, 24h)
    #[arg(long, default_value = "1h")]
    pub since: String,

    /// Output as JSON snapshot (no table)
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy sessions [name]`.
#[derive(Parser, Clone)]
pub struct ProxySessionsArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Only show active sessions (seen in last 5 minutes)
    #[arg(long)]
    pub active: bool,

    /// Filter by client name (e.g., claude-desktop)
    #[arg(long)]
    pub client: Option<String>,

    /// Time window for session start (e.g., 1h, 24h)
    #[arg(long, default_value = "1h")]
    pub since: String,

    /// Maximum rows to return
    #[arg(long, default_value = "50")]
    pub limit: i64,

    /// Output as newline-delimited JSON
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy clients [name]`.
#[derive(Parser, Clone)]
pub struct ProxyClientsArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Lookback window (default longer: clients change slowly)
    #[arg(long, default_value = "7d")]
    pub since: String,

    /// Output as newline-delimited JSON
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy status [name]`.
#[derive(Parser, Clone)]
pub struct ProxyStatusArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Lookback window for activity summary (e.g., 1h, 24h)
    #[arg(long, default_value = "1h")]
    pub since: String,

    /// Output as JSON snapshot
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy session <session_id>`.
#[derive(Parser, Clone)]
pub struct ProxySessionArgs {
    /// Session ID to look up
    pub session_id: String,

    /// Output as JSON
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy schema [name]`.
#[derive(Parser, Clone)]
pub struct ProxySchemaArgs {
    /// Filter to a specific proxy name (omit to show all proxies)
    #[arg(long)]
    pub proxy: Option<String>,

    /// Filter to a specific MCP method (e.g., tools/list, initialize)
    #[arg(long)]
    pub method: Option<String>,

    /// Show change history instead of current schema
    #[arg(long)]
    pub changes: bool,

    /// Show tool usage — listed tools vs actual calls (unused tools highlighted)
    #[arg(long)]
    pub unused: bool,

    /// Time window for usage stats (used with --unused, default: 7d)
    #[arg(long, default_value = "7d")]
    pub since: String,

    /// Number of change history rows to show (used with --changes)
    #[arg(long, default_value = "50")]
    pub limit: i64,

    /// Output as JSON
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `mcpr proxy setup`.
#[derive(Parser, Clone)]
pub struct ProxySetupArgs {
    /// Cloud API base URL (default: https://api.mcpr.app)
    #[arg(long, default_value = "https://api.mcpr.app")]
    pub cloud_url: String,

    /// Output config path (default: ./mcpr.toml)
    #[arg(long, short)]
    pub output: Option<String>,
}

// ── Store subcommands ──────────────────────────────────────────────────

#[derive(Parser)]
pub struct StoreArgs {
    #[command(subcommand)]
    pub command: StoreCommand,
}

/// Storage maintenance commands.
#[derive(Subcommand, Clone)]
pub enum StoreCommand {
    /// Show database size, row counts, and age of records
    Stats,
    /// Delete old records and reclaim disk space
    Vacuum(StoreVacuumArgs),
}

/// Arguments for `mcpr store vacuum`.
#[derive(Parser, Clone)]
pub struct StoreVacuumArgs {
    /// Delete records older than this duration or date (e.g., 7d, 30d)
    #[arg(long)]
    pub before: String,

    /// Only vacuum records for one proxy
    #[arg(long)]
    pub proxy: Option<String>,

    /// Show what would be deleted without actually deleting
    #[arg(long)]
    pub dry_run: bool,
}

// ── Relay subcommands ─────────────────────────────────────────────────

#[derive(Parser)]
pub struct RelayArgs {
    #[command(subcommand)]
    pub command: RelayCommand,
}

/// Relay server lifecycle commands.
#[derive(Subcommand, Clone)]
pub enum RelayCommand {
    /// Run relay server in the foreground
    Run(RelayRunArgs),
    /// Start relay server in the background
    Start(RelayRunArgs),
    /// Stop the running relay server
    Stop,
    /// Restart the relay server
    Restart(RelayRestartArgs),
    /// Show relay server status
    Status,
}

/// Arguments for `mcpr relay run` and `mcpr relay start`.
#[derive(Parser, Clone)]
pub struct RelayRunArgs {
    /// Config file path
    pub config: Option<String>,
}

/// Arguments for `mcpr relay restart`.
#[derive(Parser, Clone)]
pub struct RelayRestartArgs {
    /// New config file (omit to reuse saved config)
    pub config: Option<String>,
}

/// Arguments for the `validate` subcommand.
#[derive(Parser, Clone)]
pub struct ValidateArgs {
    /// Config file path to validate
    #[arg(short, long)]
    pub config: Option<String>,
    /// Dump resolved config to stdout after validation
    #[arg(long)]
    pub dump: bool,
}

/// Runtime options extracted from CLI args (used by main.rs).
pub struct RuntimeOptions {
    pub drain_timeout: u64,
    pub log_format: LogFormat,
    pub admin_bind: String,
}

// ── TOML config file ────────────────────────────────────────────────────

/// `[csp]` table in config file
#[derive(serde::Deserialize, Default)]
#[serde(default)]
struct FileCspConfig {
    mode: Option<String>,
    domains: Vec<String>,
}

/// Entry in `[[relay.tokens]]` array
#[derive(serde::Deserialize)]
struct FileTokenEntry {
    token: String,
    subdomains: Vec<String>,
}

/// `[relay]` table in config file
#[derive(serde::Deserialize, Default)]
#[serde(default)]
struct FileRelayConfig {
    domain: Option<String>,
    auth_provider: Option<String>,
    auth_provider_secret: Option<String>,
    tokens: Vec<FileTokenEntry>,
}

/// `[tunnel]` table in config file
#[derive(serde::Deserialize, Default)]
#[serde(default)]
struct FileTunnelConfig {
    /// Enable tunnel for a public URL. Default: false (proxy-only mode).
    enabled: bool,
    relay_url: Option<String>,
    token: Option<String>,
    subdomain: Option<String>,
}

/// `[events]` table in config file
/// `[cloud]` table in config file
#[derive(serde::Deserialize, Default)]
#[serde(default)]
struct FileCloudConfig {
    token: Option<String>,
    server: Option<String>,
    endpoint: Option<String>,
    batch_size: Option<usize>,
    flush_interval_ms: Option<u64>,
}

/// Config file format (mcpr.toml)
#[derive(serde::Deserialize, Default)]
#[serde(default)]
struct FileConfig {
    // -- Identity --
    name: Option<String>,

    // -- Shared --
    port: Option<u16>,
    mode: Option<String>, // "relay" | "gateway" (default)

    // -- Gateway --
    mcp: Option<String>,
    widgets: Option<String>,
    csp: FileCspConfig,

    // -- Relay --
    relay: FileRelayConfig,

    // -- Tunnel client --
    tunnel: FileTunnelConfig,

    // -- Cloud sync --
    cloud: FileCloudConfig,

    // -- Runtime --
    drain_timeout: Option<u64>,
    log_format: Option<String>,

    // -- Admin --
    admin_bind: Option<String>,

    max_request_body_size: Option<usize>,
    max_response_body_size: Option<usize>,
    max_concurrent_upstream: Option<usize>,
    connect_timeout: Option<u64>,
    request_timeout: Option<u64>,
}

impl FileConfig {
    /// Load config from a specific path, or ./mcpr.toml by default.
    fn load(explicit_path: Option<&str>) -> (Self, Option<std::path::PathBuf>) {
        let path = match explicit_path {
            Some(p) => std::path::PathBuf::from(p),
            None => std::env::current_dir()
                .unwrap_or_default()
                .join(CONFIG_FILE),
        };

        if !path.exists() {
            if explicit_path.is_some() {
                eprintln!(
                    "  {}: config file not found: {}",
                    colored::Colorize::red("error"),
                    path.display()
                );
                std::process::exit(1);
            }
            return (FileConfig::default(), None);
        }

        match std::fs::read_to_string(&path) {
            Ok(contents) => match toml::from_str::<FileConfig>(&contents) {
                Ok(config) => {
                    eprintln!(
                        "  {} loaded {}",
                        colored::Colorize::dimmed("config"),
                        path.display()
                    );
                    (config, Some(path))
                }
                Err(e) => {
                    eprintln!(
                        "  {}: failed to parse {}: {}",
                        colored::Colorize::red("error"),
                        path.display(),
                        e
                    );
                    std::process::exit(1);
                }
            },
            Err(e) => {
                eprintln!(
                    "  {}: failed to read {}: {}",
                    colored::Colorize::red("error"),
                    path.display(),
                    e
                );
                std::process::exit(1);
            }
        }
    }

    /// Is relay mode via config file: mode = "relay"
    fn is_relay(&self) -> bool {
        self.mode.as_deref() == Some("relay")
    }
}

// ── Gateway config ──────────────────────────────────────────────────────

/// Resolved configuration for gateway (proxy) mode.
pub struct GatewayConfig {
    pub name: String,
    pub mcp: Option<String>,
    pub widgets: Option<String>,
    pub port: Option<u16>,
    pub csp_domains: Vec<String>,
    pub csp_mode: CspMode,
    pub relay_url: Option<String>,
    pub tunnel_token: Option<String>,
    pub tunnel_subdomain: Option<String>,
    /// Whether tunnel is enabled. Default: false (proxy-only mode).
    pub tunnel: bool,
    pub max_request_body_size: Option<usize>,
    pub max_response_body_size: Option<usize>,
    pub max_concurrent_upstream: Option<usize>,
    pub connect_timeout: Option<u64>,
    pub request_timeout: Option<u64>,
    pub cloud_token: Option<String>,
    pub cloud_server: Option<String>,
    pub cloud_endpoint: Option<String>,
    pub cloud_batch_size: Option<usize>,
    pub cloud_flush_interval_ms: Option<u64>,
    pub runtime: RuntimeOptions,
}

impl GatewayConfig {
    /// Resolve tunnel identity from config.
    /// Token is required (caller must check before calling). Subdomain is optional.
    /// Returns (token, desired_subdomain).
    pub fn resolve_tunnel_identity(
        tunnel_subdomain: Option<String>,
        tunnel_token: Option<String>,
    ) -> (String, Option<String>) {
        let token =
            tunnel_token.expect("tunnel token must be set before calling resolve_tunnel_identity");
        (token, tunnel_subdomain)
    }
}

// ── Load + dispatch ─────────────────────────────────────────────────────

/// Parse CLI args, load config file, and return the resolved action.
pub fn load() -> CliAction {
    let cli = Cli::parse();

    match cli.command {
        Some(Commands::Validate(args)) => CliAction::Validate(args),
        Some(Commands::Version) => CliAction::Version,
        Some(Commands::Update) => CliAction::Update,
        Some(Commands::Proxy(ProxyArgs {
            command: ProxyCommand::Run(run_args),
        })) => {
            // `mcpr proxy run` needs to load config and fork — handle like Start.
            let explicit_path = run_args.config.as_deref().or(cli.config.as_deref());
            let (file, cfg_path) = FileConfig::load(explicit_path);
            let config_content = cfg_path
                .as_ref()
                .map(|p| std::fs::read_to_string(p).unwrap_or_default())
                .unwrap_or_default();
            let config_path_str = cfg_path
                .as_ref()
                .and_then(|p| p.canonicalize().ok())
                .map(|p| p.display().to_string())
                .unwrap_or_default();

            let runtime = RuntimeOptions {
                drain_timeout: file.drain_timeout.unwrap_or(30),
                log_format: file
                    .log_format
                    .as_deref()
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(LogFormat::Json),
                admin_bind: file
                    .admin_bind
                    .clone()
                    .unwrap_or_else(|| "127.0.0.1:9901".to_string()),
            };

            let mode = if file.is_relay() {
                load_relay(file, runtime)
            } else {
                load_gateway(file, cfg_path, runtime)
            };

            CliAction::ProxyRun {
                mode,
                replace: run_args.replace,
                config_content,
                config_path: config_path_str,
            }
        }
        Some(Commands::Proxy(ProxyArgs {
            command: ProxyCommand::Setup(setup_args),
        })) => CliAction::ProxySetup {
            cloud_url: setup_args.cloud_url,
            output: setup_args.output,
        },
        Some(Commands::Proxy(args)) => CliAction::Proxy(args.command),
        Some(Commands::Store(args)) => CliAction::Store(args.command),
        Some(Commands::Relay(RelayArgs {
            command: RelayCommand::Run(run_args),
        })) => load_relay_run(run_args, cli.config.as_deref(), true),
        Some(Commands::Relay(RelayArgs {
            command: RelayCommand::Start(run_args),
        })) => load_relay_run(run_args, cli.config.as_deref(), false),
        Some(Commands::Relay(args)) => CliAction::Relay(args.command),
        Some(Commands::Stop) => CliAction::Stop,
        Some(Commands::Status) => CliAction::Status,
        Some(Commands::Start(args)) => CliAction::Start {
            foreground: args.foreground,
        },
        Some(Commands::Restart) => CliAction::Restart {
            restart_proxies: vec![],
            restart_relay: false,
        },
        // No subcommand — default to starting the daemon.
        None => CliAction::Start { foreground: false },
    }
}

/// Load config for `mcpr relay run` / `mcpr relay start`.
/// The `mode = "relay"` field is not required — it is implicit from the command.
fn load_relay_run(args: RelayRunArgs, global_config: Option<&str>, foreground: bool) -> CliAction {
    let explicit_path = args.config.as_deref().or(global_config);
    let (file, cfg_path) = FileConfig::load(explicit_path);
    let config_content = cfg_path
        .as_ref()
        .map(|p| std::fs::read_to_string(p).unwrap_or_default())
        .unwrap_or_default();
    let config_path_str = cfg_path
        .as_ref()
        .and_then(|p| p.canonicalize().ok())
        .map(|p| p.display().to_string())
        .unwrap_or_default();

    let port = file
        .port
        .expect("port is required for relay mode in config");
    let relay_domain = file
        .relay
        .domain
        .expect("relay.domain is required for relay mode in config");

    let tokens = file
        .relay
        .tokens
        .into_iter()
        .map(|e| (e.token, e.subdomains))
        .collect();

    let relay_config = RelayConfig {
        port,
        relay_domain,
        auth_provider: file.relay.auth_provider,
        auth_provider_secret: file.relay.auth_provider_secret,
        tokens,
        max_request_body_size: file.max_request_body_size,
        max_response_body_size: file.max_response_body_size,
    };

    CliAction::RelayRun {
        relay_config,
        config_content,
        config_path: config_path_str,
        foreground,
    }
}

/// Validate a config file and return a list of (severity, message) tuples.
/// Severity is "error" or "warn".
pub fn validate_config(path: Option<&str>) -> Vec<(&'static str, String)> {
    let mut issues = Vec::new();

    let config_path = path
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| std::env::current_dir().unwrap().join(CONFIG_FILE));

    if !config_path.exists() {
        issues.push((
            "error",
            format!("config file not found: {}", config_path.display()),
        ));
        return issues;
    }

    let contents = match std::fs::read_to_string(&config_path) {
        Ok(c) => c,
        Err(e) => {
            issues.push((
                "error",
                format!("cannot read {}: {}", config_path.display(), e),
            ));
            return issues;
        }
    };

    match toml::from_str::<FileConfig>(&contents) {
        Ok(config) => {
            // Check required fields for gateway mode
            if !config.is_relay() && config.mcp.is_none() {
                issues.push((
                    "warn",
                    "no 'mcp' URL set — required for gateway mode".to_string(),
                ));
            }

            // Check relay mode requirements
            if config.is_relay() {
                if config.port.is_none() {
                    issues.push(("error", "'port' is required for relay mode".to_string()));
                }
                if config.relay.domain.is_none() {
                    issues.push((
                        "error",
                        "'relay.domain' is required for relay mode".to_string(),
                    ));
                }
            }

            // Validate MCP URL if set
            if let Some(ref mcp) = config.mcp
                && url::Url::parse(mcp).is_err()
            {
                issues.push(("error", format!("invalid MCP URL: {mcp}")));
            }

            // Validate port range
            if let Some(port) = config.port
                && port == 0
            {
                issues.push(("warn", "port 0 will bind to a random port".to_string()));
            }

            // Validate CSP mode
            if let Some(ref mode) = config.csp.mode
                && mode != "extend"
                && mode != "override"
            {
                issues.push((
                    "error",
                    format!("invalid csp.mode: {mode} (expected: extend, override)"),
                ));
            }

            // Validate log format
            if let Some(ref fmt) = config.log_format
                && fmt.parse::<LogFormat>().is_err()
            {
                issues.push((
                    "error",
                    format!("invalid log_format: {fmt} (expected: json, pretty)"),
                ));
            }

            if issues.is_empty() {
                issues.push(("ok", format!("config valid: {}", config_path.display())));
            }
        }
        Err(e) => {
            issues.push(("error", format!("invalid TOML: {e}")));
        }
    }

    issues
}

fn load_relay(file: FileConfig, _runtime: RuntimeOptions) -> Mode {
    let port = file
        .port
        .expect("port is required for relay mode in mcpr.toml");
    let relay_domain = file
        .relay
        .domain
        .expect("relay.domain is required for relay mode in mcpr.toml");

    let tokens = file
        .relay
        .tokens
        .into_iter()
        .map(|e| (e.token, e.subdomains))
        .collect();

    Mode::Relay(RelayConfig {
        port,
        relay_domain,
        auth_provider: file.relay.auth_provider,
        auth_provider_secret: file.relay.auth_provider_secret,
        tokens,
        max_request_body_size: file.max_request_body_size,
        max_response_body_size: file.max_response_body_size,
    })
}

/// Resolve proxy name: explicit name > filename stem > "default".
/// Characters that aren't alphanumeric or '-' are replaced with '-'.
fn resolve_proxy_name(
    explicit_name: Option<&str>,
    config_path: Option<&std::path::Path>,
) -> String {
    let raw = match explicit_name {
        Some(n) => n.to_string(),
        None => config_path
            .and_then(|p| p.file_stem())
            .and_then(|s| s.to_str())
            .map(|s| if s == "mcpr" { "default" } else { s })
            .unwrap_or("default")
            .to_string(),
    };

    raw.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' {
                c
            } else {
                '-'
            }
        })
        .collect()
}

fn load_gateway(
    file: FileConfig,
    config_path: Option<std::path::PathBuf>,
    runtime: RuntimeOptions,
) -> Mode {
    let csp_mode = match file.csp.mode.as_deref() {
        Some(m) => parse_csp_mode(m),
        None => CspMode::default(),
    };

    let name = resolve_proxy_name(file.name.as_deref(), config_path.as_deref());

    Mode::Gateway(Box::new(GatewayConfig {
        name,
        mcp: file.mcp,
        widgets: file.widgets,
        port: file.port,
        csp_domains: file.csp.domains,
        csp_mode,
        relay_url: Some(
            file.tunnel
                .relay_url
                .unwrap_or_else(|| "https://tunnel.mcpr.app".to_string()),
        ),
        tunnel_token: file.tunnel.token,
        tunnel_subdomain: file.tunnel.subdomain,
        tunnel: file.tunnel.enabled,
        max_request_body_size: file.max_request_body_size,
        max_response_body_size: file.max_response_body_size,
        max_concurrent_upstream: file.max_concurrent_upstream,
        connect_timeout: file.connect_timeout,
        request_timeout: file.request_timeout,
        cloud_token: file.cloud.token,
        cloud_server: file.cloud.server,
        cloud_endpoint: file.cloud.endpoint,
        cloud_batch_size: file.cloud.batch_size,
        cloud_flush_interval_ms: file.cloud.flush_interval_ms,
        runtime,
    }))
}

// ── Tests ───────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
    use super::*;

    #[test]
    fn resolve_tunnel_identity__subdomain_and_token_independent() {
        let (token, sub) = GatewayConfig::resolve_tunnel_identity(
            Some("myapp".into()),
            Some("mcpr_secret_token_123".into()),
        );
        assert_eq!(token, "mcpr_secret_token_123");
        assert_eq!(sub.as_deref(), Some("myapp"));
    }

    #[test]
    fn resolve_tunnel_identity__no_subdomain_uses_token() {
        let (token, sub) =
            GatewayConfig::resolve_tunnel_identity(None, Some("my-saved-token".into()));
        assert_eq!(token, "my-saved-token");
        assert_eq!(sub, None);
    }

    #[test]
    #[should_panic(expected = "tunnel token must be set")]
    fn resolve_tunnel_identity__no_token_panics() {
        GatewayConfig::resolve_tunnel_identity(Some("myapp".into()), None);
    }

    #[test]
    fn file_config__max_request_body_size() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 8080
            max_request_body_size = 10485760
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.max_request_body_size, Some(10_485_760));
    }

    #[test]
    fn file_config__max_response_body_size() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 8080
            max_response_body_size = 20971520
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.max_response_body_size, Some(20_971_520));
    }

    #[test]
    fn file_config__body_size_defaults_to_none() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 8080
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.max_request_body_size, None);
        assert_eq!(config.max_response_body_size, None);
        assert_eq!(config.max_concurrent_upstream, None);
    }

    #[test]
    fn file_config__max_concurrent_upstream() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 8080
            max_concurrent_upstream = 50
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.max_concurrent_upstream, Some(50));
    }

    // ── Cloud config parsing tests ─────────────────────────────────────

    #[test]
    fn cloud_config__parses_all_fields() {
        let toml_str = r#"
            [cloud]
            token = "mcpr_abc123"
            server = "my-proxy"
            endpoint = "https://custom.api/ingest"
            batch_size = 50
            flush_interval_ms = 10000
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.cloud.token.as_deref(), Some("mcpr_abc123"));
        assert_eq!(config.cloud.server.as_deref(), Some("my-proxy"));
        assert_eq!(
            config.cloud.endpoint.as_deref(),
            Some("https://custom.api/ingest")
        );
        assert_eq!(config.cloud.batch_size, Some(50));
        assert_eq!(config.cloud.flush_interval_ms, Some(10000));
    }

    #[test]
    fn cloud_config__defaults_to_none() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert!(config.cloud.token.is_none());
        assert!(config.cloud.server.is_none());
        assert!(config.cloud.endpoint.is_none());
        assert!(config.cloud.batch_size.is_none());
        assert!(config.cloud.flush_interval_ms.is_none());
    }

    #[test]
    fn cloud_config__partial_fields() {
        let toml_str = r#"
            [cloud]
            token = "mcpr_xyz"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.cloud.token.as_deref(), Some("mcpr_xyz"));
        assert!(config.cloud.server.is_none());
        assert!(config.cloud.endpoint.is_none());
        assert!(config.cloud.batch_size.is_none());
        assert!(config.cloud.flush_interval_ms.is_none());
    }

    #[test]
    fn cloud_config__coexists_with_other_sections() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 8080

            [cloud]
            token = "mcpr_tok"
            server = "prod-1"

            [tunnel]
            relay_url = "https://tunnel.mcpr.app"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.cloud.token.as_deref(), Some("mcpr_tok"));
        assert_eq!(config.cloud.server.as_deref(), Some("prod-1"));
        assert_eq!(config.mcp.as_deref(), Some("http://localhost:9000"));
        assert_eq!(
            config.tunnel.relay_url.as_deref(),
            Some("https://tunnel.mcpr.app")
        );
    }

    #[test]
    fn cloud_config__empty_section_uses_defaults() {
        let toml_str = r#"
            [cloud]
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert!(config.cloud.token.is_none());
        assert!(config.cloud.server.is_none());
    }

    // ── Proxy name resolution ──────────────────────────────────────────

    #[test]
    fn resolve_proxy_name__explicit_wins() {
        let name = resolve_proxy_name(
            Some("my-proxy"),
            Some(std::path::Path::new("/tmp/search.toml")),
        );
        assert_eq!(name, "my-proxy");
    }

    #[test]
    fn resolve_proxy_name__from_filename_stem() {
        let name = resolve_proxy_name(None, Some(std::path::Path::new("/tmp/search.toml")));
        assert_eq!(name, "search");
    }

    #[test]
    fn resolve_proxy_name__mcpr_toml_becomes_default() {
        let name = resolve_proxy_name(None, Some(std::path::Path::new("/tmp/mcpr.toml")));
        assert_eq!(name, "default");
    }

    #[test]
    fn resolve_proxy_name__no_config_becomes_default() {
        let name = resolve_proxy_name(None, None);
        assert_eq!(name, "default");
    }

    #[test]
    fn resolve_proxy_name__sanitizes_special_chars() {
        let name = resolve_proxy_name(Some("my proxy!@#$"), None);
        assert_eq!(name, "my-proxy----");
    }

    #[test]
    fn resolve_proxy_name__preserves_hyphens() {
        let name = resolve_proxy_name(Some("search-v2"), None);
        assert_eq!(name, "search-v2");
    }

    #[test]
    fn file_config__name_from_toml() {
        let toml_str = r#"
            name = "email"
            mcp = "http://localhost:9000"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.name.as_deref(), Some("email"));
    }

    // ── Relay config parsing ──────────────────────────────────────────

    #[test]
    fn file_config__relay_mode_detected() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            [relay]
            domain = "tunnel.example.com"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert!(config.is_relay());
    }

    #[test]
    fn file_config__gateway_mode_by_default() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
            port = 3000
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert!(!config.is_relay());
    }

    #[test]
    fn file_config__relay_domain_parsed() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            [relay]
            domain = "tunnel.example.com"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.relay.domain.as_deref(), Some("tunnel.example.com"));
    }

    #[test]
    fn file_config__relay_static_tokens() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            [relay]
            domain = "tunnel.example.com"
            [[relay.tokens]]
            token = "tok_abc"
            subdomains = ["myapp", "myapp-*"]
            [[relay.tokens]]
            token = "tok_xyz"
            subdomains = ["other"]
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.relay.tokens.len(), 2);
        assert_eq!(config.relay.tokens[0].token, "tok_abc");
        assert_eq!(config.relay.tokens[0].subdomains, vec!["myapp", "myapp-*"]);
        assert_eq!(config.relay.tokens[1].token, "tok_xyz");
        assert_eq!(config.relay.tokens[1].subdomains, vec!["other"]);
    }

    #[test]
    fn file_config__relay_auth_provider() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            [relay]
            domain = "tunnel.example.com"
            auth_provider = "https://auth.example.com"
            auth_provider_secret = "secret123"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(
            config.relay.auth_provider.as_deref(),
            Some("https://auth.example.com")
        );
        assert_eq!(
            config.relay.auth_provider_secret.as_deref(),
            Some("secret123")
        );
    }

    #[test]
    fn file_config__relay_defaults_empty() {
        let toml_str = r#"
            mcp = "http://localhost:9000"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert!(config.relay.domain.is_none());
        assert!(config.relay.auth_provider.is_none());
        assert!(config.relay.auth_provider_secret.is_none());
        assert!(config.relay.tokens.is_empty());
    }

    #[test]
    fn file_config__relay_body_size_limits() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            max_request_body_size = 1048576
            max_response_body_size = 2097152
            [relay]
            domain = "tunnel.example.com"
        "#;
        let config: FileConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.max_request_body_size, Some(1_048_576));
        assert_eq!(config.max_response_body_size, Some(2_097_152));
    }

    // ── load_relay ────────────────────────────────────────────────────

    #[test]
    fn load_relay__builds_relay_config() {
        let toml_str = r#"
            mode = "relay"
            port = 9090
            [relay]
            domain = "tunnel.test"
            [[relay.tokens]]
            token = "tok_a"
            subdomains = ["app1"]
        "#;
        let file: FileConfig = toml::from_str(toml_str).unwrap();
        let runtime = RuntimeOptions {
            drain_timeout: 30,
            log_format: LogFormat::Json,
            admin_bind: "127.0.0.1:9901".to_string(),
        };
        let mode = load_relay(file, runtime);
        match mode {
            Mode::Relay(cfg) => {
                assert_eq!(cfg.port, 9090);
                assert_eq!(cfg.relay_domain, "tunnel.test");
                assert_eq!(cfg.tokens.len(), 1);
                assert!(cfg.tokens.contains_key("tok_a"));
                assert_eq!(cfg.tokens["tok_a"], vec!["app1"]);
            }
            Mode::Gateway(_) => panic!("expected Mode::Relay"),
        }
    }

    #[test]
    #[should_panic(expected = "port is required")]
    fn load_relay__panics_without_port() {
        let toml_str = r#"
            mode = "relay"
            [relay]
            domain = "tunnel.test"
        "#;
        let file: FileConfig = toml::from_str(toml_str).unwrap();
        let runtime = RuntimeOptions {
            drain_timeout: 30,
            log_format: LogFormat::Json,
            admin_bind: "127.0.0.1:9901".to_string(),
        };
        load_relay(file, runtime);
    }

    #[test]
    #[should_panic(expected = "relay.domain is required")]
    fn load_relay__panics_without_domain() {
        let toml_str = r#"
            mode = "relay"
            port = 8080
            [relay]
        "#;
        let file: FileConfig = toml::from_str(toml_str).unwrap();
        let runtime = RuntimeOptions {
            drain_timeout: 30,
            log_format: LogFormat::Json,
            admin_bind: "127.0.0.1:9901".to_string(),
        };
        load_relay(file, runtime);
    }

    // ── load_relay_run ────────────────────────────────────────────────

    #[test]
    fn load_relay_run__foreground() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(
            &cfg_path,
            "port = 9090\n[relay]\ndomain = \"tunnel.test\"\n",
        )
        .unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        let action = load_relay_run(args, None, true);
        match action {
            CliAction::RelayRun {
                relay_config,
                foreground,
                config_content,
                ..
            } => {
                assert!(foreground);
                assert_eq!(relay_config.port, 9090);
                assert_eq!(relay_config.relay_domain, "tunnel.test");
                assert!(!config_content.is_empty());
            }
            _ => panic!("expected CliAction::RelayRun"),
        }
    }

    #[test]
    fn load_relay_run__background() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(&cfg_path, "port = 8080\n[relay]\ndomain = \"tunnel.bg\"\n").unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        let action = load_relay_run(args, None, false);
        match action {
            CliAction::RelayRun {
                relay_config,
                foreground,
                ..
            } => {
                assert!(!foreground);
                assert_eq!(relay_config.port, 8080);
                assert_eq!(relay_config.relay_domain, "tunnel.bg");
            }
            _ => panic!("expected CliAction::RelayRun"),
        }
    }

    #[test]
    fn load_relay_run__with_tokens() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(
            &cfg_path,
            r#"
port = 8080
[relay]
domain = "tunnel.test"
[[relay.tokens]]
token = "tok_abc"
subdomains = ["myapp"]
"#,
        )
        .unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        let action = load_relay_run(args, None, true);
        match action {
            CliAction::RelayRun { relay_config, .. } => {
                assert_eq!(relay_config.tokens.len(), 1);
                assert!(relay_config.tokens.contains_key("tok_abc"));
            }
            _ => panic!("expected CliAction::RelayRun"),
        }
    }

    #[test]
    fn load_relay_run__with_auth_provider() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(
            &cfg_path,
            r#"
port = 8080
[relay]
domain = "tunnel.test"
auth_provider = "https://auth.example.com"
auth_provider_secret = "secret123"
"#,
        )
        .unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        let action = load_relay_run(args, None, true);
        match action {
            CliAction::RelayRun { relay_config, .. } => {
                assert_eq!(
                    relay_config.auth_provider.as_deref(),
                    Some("https://auth.example.com")
                );
                assert_eq!(
                    relay_config.auth_provider_secret.as_deref(),
                    Some("secret123")
                );
            }
            _ => panic!("expected CliAction::RelayRun"),
        }
    }

    #[test]
    #[should_panic(expected = "port is required")]
    fn load_relay_run__panics_without_port() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(&cfg_path, "[relay]\ndomain = \"tunnel.test\"\n").unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        load_relay_run(args, None, true);
    }

    #[test]
    #[should_panic(expected = "relay.domain is required")]
    fn load_relay_run__panics_without_domain() {
        let dir = tempfile::tempdir().unwrap();
        let cfg_path = dir.path().join("relay.toml");
        std::fs::write(&cfg_path, "port = 8080\n[relay]\n").unwrap();

        let args = RelayRunArgs {
            config: Some(cfg_path.display().to_string()),
        };
        load_relay_run(args, None, true);
    }
}
