use std::path::{Path, PathBuf};

use anyhow::Context;
use serde::Deserialize;

pub const DEFAULT_LISTEN: &str = "0.0.0.0:7000";
pub const DEFAULT_DATA_DIR: &str = "data";
pub const DEFAULT_TLS_CERT: &str = "data/server-cert.der";
pub const DEFAULT_TLS_KEY: &str = "data/server-key.der";
pub const DEFAULT_STORE_BACKEND: &str = "file";
pub const DEFAULT_DB_PATH: &str = "data/qpq.db";

#[derive(Debug, Default, Deserialize)]
pub struct FileConfig {
    pub listen: Option<String>,
    pub data_dir: Option<String>,
    pub tls_cert: Option<PathBuf>,
    pub tls_key: Option<PathBuf>,
    pub auth_token: Option<String>,
    pub allow_insecure_auth: Option<bool>,
    /// When true, enqueue does not require an identity-bound session: only a valid token is required.
    /// The server does not associate the request with a specific sender (Sealed Sender).
    #[serde(default)]
    pub sealed_sender: Option<bool>,
    pub store_backend: Option<String>,
    pub db_path: Option<PathBuf>,
    pub db_key: Option<String>,
    /// Metrics HTTP listen address (e.g. "0.0.0.0:9090"). If set, /metrics is served there.
    pub metrics_listen: Option<String>,
    /// When true and metrics_listen is set, start the metrics server.
    #[serde(default)]
    pub metrics_enabled: Option<bool>,
    pub federation: Option<FederationFileConfig>,
    /// Directory containing plugin `.so` / `.dylib` files to load at startup.
    pub plugin_dir: Option<PathBuf>,
    /// When true, audit logs hash identity key prefixes and omit payload sizes.
    #[serde(default)]
    pub redact_logs: Option<bool>,
    /// WebSocket JSON-RPC bridge listen address (e.g. "0.0.0.0:9000").
    pub ws_listen: Option<String>,
    /// WebTransport (HTTP/3) listen address for browser clients (e.g. "0.0.0.0:7443").
    pub webtransport_listen: Option<String>,
    /// Graceful shutdown drain timeout in seconds.
    pub drain_timeout_secs: Option<u64>,
    /// Default per-RPC timeout in seconds.
    pub rpc_timeout_secs: Option<u64>,
    /// Storage/database operation timeout in seconds.
    pub storage_timeout_secs: Option<u64>,
}

#[derive(Debug)]
pub struct EffectiveConfig {
    pub listen: String,
    pub data_dir: String,
    pub tls_cert: PathBuf,
    pub tls_key: PathBuf,
    pub auth_token: Option<String>,
    pub allow_insecure_auth: bool,
    /// When true, enqueue does not require identity; valid token only (Sealed Sender).
    pub sealed_sender: bool,
    pub store_backend: String,
    pub db_path: PathBuf,
    pub db_key: String,
    /// If Some(addr), metrics server listens here (e.g. "0.0.0.0:9090").
    pub metrics_listen: Option<String>,
    /// Start metrics server only when true and metrics_listen is set.
    pub metrics_enabled: bool,
    pub federation: Option<EffectiveFederationConfig>,
    /// Directory to scan for plugin `.so` / `.dylib` files at startup. None = no plugins.
    pub plugin_dir: Option<PathBuf>,
    /// When true, audit logs hash identity key prefixes and omit payload sizes.
    pub redact_logs: bool,
    /// WebSocket JSON-RPC bridge listen address. If set, the bridge is started.
    pub ws_listen: Option<String>,
    /// WebTransport (HTTP/3) listen address. If set, the WebTransport endpoint is started.
    pub webtransport_listen: Option<String>,
    /// Graceful shutdown drain timeout in seconds.
    pub drain_timeout_secs: u64,
    /// Default per-RPC timeout in seconds.
    pub rpc_timeout_secs: u64,
    /// Storage/database operation timeout in seconds.
    pub storage_timeout_secs: u64,
}

pub const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_RPC_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_STORAGE_TIMEOUT_SECS: u64 = 10;

#[derive(Debug, Default, Deserialize)]
pub struct FederationFileConfig {
    pub enabled: Option<bool>,
    pub domain: Option<String>,
    pub listen: Option<String>,
    pub federation_cert: Option<PathBuf>,
    pub federation_key: Option<PathBuf>,
    pub federation_ca: Option<PathBuf>,
    #[serde(default)]
    pub peers: Vec<FederationPeerConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct FederationPeerConfig {
    pub domain: String,
    pub address: String,
}

#[derive(Debug)]
#[allow(dead_code)] // federation not yet wired up
pub struct EffectiveFederationConfig {
    pub enabled: bool,
    pub domain: String,
    pub listen: String,
    pub federation_cert: PathBuf,
    pub federation_key: PathBuf,
    pub federation_ca: PathBuf,
    pub peers: Vec<FederationPeerConfig>,
}

pub fn load_config(path: Option<&Path>) -> anyhow::Result<FileConfig> {
    let path = match path {
        Some(p) => PathBuf::from(p),
        None => PathBuf::from("qpq-server.toml"),
    };

    if !path.exists() {
        return Ok(FileConfig::default());
    }

    let contents =
        std::fs::read_to_string(&path).with_context(|| format!("read config file {path:?}"))?;
    let cfg: FileConfig =
        toml::from_str(&contents).with_context(|| format!("parse config file {path:?}"))?;
    Ok(cfg)
}

pub fn merge_config(args: &crate::Args, file: &FileConfig) -> EffectiveConfig {
    let listen = if args.listen == DEFAULT_LISTEN {
        file.listen
            .clone()
            .unwrap_or_else(|| DEFAULT_LISTEN.to_string())
    } else {
        args.listen.clone()
    };

    let data_dir = if args.data_dir == DEFAULT_DATA_DIR {
        file.data_dir
            .clone()
            .unwrap_or_else(|| DEFAULT_DATA_DIR.to_string())
    } else {
        args.data_dir.clone()
    };

    let tls_cert = if args.tls_cert == Path::new(DEFAULT_TLS_CERT) {
        file.tls_cert
            .clone()
            .unwrap_or_else(|| PathBuf::from(DEFAULT_TLS_CERT))
    } else {
        args.tls_cert.clone()
    };

    let tls_key = if args.tls_key == Path::new(DEFAULT_TLS_KEY) {
        file.tls_key
            .clone()
            .unwrap_or_else(|| PathBuf::from(DEFAULT_TLS_KEY))
    } else {
        args.tls_key.clone()
    };

    let auth_token = if args.auth_token.is_some() {
        args.auth_token.clone()
    } else {
        file.auth_token.clone()
    };

    let allow_insecure_auth = if args.allow_insecure_auth {
        true
    } else {
        file.allow_insecure_auth.unwrap_or(false)
    };

    let sealed_sender = args.sealed_sender || file.sealed_sender.unwrap_or(false);

    let store_backend = if args.store_backend == DEFAULT_STORE_BACKEND {
        file.store_backend
            .clone()
            .unwrap_or_else(|| DEFAULT_STORE_BACKEND.to_string())
    } else {
        args.store_backend.clone()
    };

    let db_path = if args.db_path == Path::new(DEFAULT_DB_PATH) {
        file.db_path
            .clone()
            .unwrap_or_else(|| PathBuf::from(DEFAULT_DB_PATH))
    } else {
        args.db_path.clone()
    };

    let db_key = if args.db_key.is_empty() {
        file.db_key.clone().unwrap_or_else(|| args.db_key.clone())
    } else {
        args.db_key.clone()
    };

    let metrics_listen = args
        .metrics_listen
        .clone()
        .or_else(|| file.metrics_listen.clone());
    let metrics_enabled = args
        .metrics_enabled
        .or(file.metrics_enabled)
        .unwrap_or(metrics_listen.is_some());

    let federation = {
        let file_fed = file.federation.as_ref();
        let enabled = args.federation_enabled
            || file_fed.and_then(|f| f.enabled).unwrap_or(false);

        if enabled {
            let domain = args.federation_domain.clone()
                .or_else(|| file_fed.and_then(|f| f.domain.clone()))
                .unwrap_or_default();
            let listen_fed = args.federation_listen.clone()
                .or_else(|| file_fed.and_then(|f| f.listen.clone()))
                .unwrap_or_else(|| "0.0.0.0:7001".to_string());
            let federation_cert = file_fed.and_then(|f| f.federation_cert.clone())
                .unwrap_or_else(|| PathBuf::from("data/federation-cert.der"));
            let federation_key = file_fed.and_then(|f| f.federation_key.clone())
                .unwrap_or_else(|| PathBuf::from("data/federation-key.der"));
            let federation_ca = file_fed.and_then(|f| f.federation_ca.clone())
                .unwrap_or_else(|| PathBuf::from("data/federation-ca.der"));
            let peers = file_fed
                .map(|f| f.peers.clone())
                .unwrap_or_default();

            Some(EffectiveFederationConfig {
                enabled,
                domain,
                listen: listen_fed,
                federation_cert,
                federation_key,
                federation_ca,
                peers,
            })
        } else {
            None
        }
    };

    let plugin_dir = args.plugin_dir.clone().or_else(|| file.plugin_dir.clone());
    let redact_logs = args.redact_logs || file.redact_logs.unwrap_or(false);
    let ws_listen = args
        .ws_listen
        .clone()
        .or_else(|| file.ws_listen.clone());

    let webtransport_listen = args
        .webtransport_listen
        .clone()
        .or_else(|| file.webtransport_listen.clone());

    let drain_timeout_secs = if args.drain_timeout == DEFAULT_DRAIN_TIMEOUT_SECS {
        file.drain_timeout_secs.unwrap_or(DEFAULT_DRAIN_TIMEOUT_SECS)
    } else {
        args.drain_timeout
    };
    let rpc_timeout_secs = if args.rpc_timeout == DEFAULT_RPC_TIMEOUT_SECS {
        file.rpc_timeout_secs.unwrap_or(DEFAULT_RPC_TIMEOUT_SECS)
    } else {
        args.rpc_timeout
    };
    let storage_timeout_secs = if args.storage_timeout == DEFAULT_STORAGE_TIMEOUT_SECS {
        file.storage_timeout_secs.unwrap_or(DEFAULT_STORAGE_TIMEOUT_SECS)
    } else {
        args.storage_timeout
    };

    EffectiveConfig {
        listen,
        data_dir,
        tls_cert,
        tls_key,
        auth_token,
        allow_insecure_auth,
        sealed_sender,
        store_backend,
        db_path,
        db_key,
        metrics_listen,
        metrics_enabled,
        federation,
        plugin_dir,
        redact_logs,
        ws_listen,
        webtransport_listen,
        drain_timeout_secs,
        rpc_timeout_secs,
        storage_timeout_secs,
    }
}

pub fn validate_production_config(effective: &EffectiveConfig) -> anyhow::Result<()> {
    if effective.allow_insecure_auth {
        anyhow::bail!("production forbids --allow-insecure-auth");
    }
    let token = effective
        .auth_token
        .as_deref()
        .filter(|s| !s.is_empty())
        .ok_or_else(|| {
            anyhow::anyhow!("production requires QPQ_AUTH_TOKEN (non-empty)")
        })?;
    if token == "devtoken" {
        anyhow::bail!(
            "production forbids auth_token 'devtoken'; set a strong QPQ_AUTH_TOKEN"
        );
    }
    if token.len() < 16 {
        anyhow::bail!(
            "production requires QPQ_AUTH_TOKEN of at least 16 characters (got {})",
            token.len()
        );
    }
    if effective.store_backend == "sql" && effective.db_key.is_empty() {
        anyhow::bail!("production with store_backend=sql requires non-empty QPQ_DB_KEY");
    }
    if effective.store_backend == "sql" {
        let db_dir = effective
            .db_path
            .parent()
            .unwrap_or_else(|| Path::new("."));
        // Verify the directory exists and is writable by creating+removing a probe file.
        let probe = db_dir.join(".qpq-write-probe");
        std::fs::write(&probe, b"probe")
            .with_context(|| format!("DB path parent {:?} is not writable", db_dir))?;
        let _ = std::fs::remove_file(&probe);
    }
    if effective.store_backend != "sql" {
        tracing::warn!(
            "production is using file-backed storage; \
             consider store_backend=sql with QPQ_DB_KEY for encryption at rest"
        );
    }
    if !effective.tls_cert.exists() || !effective.tls_key.exists() {
        anyhow::bail!(
            "production requires existing TLS cert and key (no auto-generation); provide QPQ_TLS_CERT and QPQ_TLS_KEY"
        );
    }
    Ok(())
}
