use std::collections::HashMap;
use std::path::PathBuf;

use serde::Deserialize;

/// Per-user retention settings from users.toml.
///
/// `auto_close_days = 0` disables the auto-close sweep (a case is never
/// closed for age alone). `auto_delete_days = 0` disables the auto-purge
/// sweep (closed cases stay forever until manual purge) — important for
/// admin / dev setups where data must survive across restarts.
#[derive(Debug, Default, Clone, Deserialize)]
pub struct RetentionUserSettings {
    #[serde(default)]
    pub auto_close_days: u32,
    #[serde(default)]
    pub auto_delete_days: u32,
}

/// Default time window (hours) of visible cases when the user entry
/// omits `window_hours`. Chosen so Monday morning still shows Friday's
/// unfinished work. Server-authoritative: clients never override this.
fn default_window_hours() -> u32 {
    72
}

/// Default number of analysis-preview lines rendered under each case on
/// the list view when the user entry omits `preview_lines`. Two lines
/// show the first sentence of the LLM output in most cases without
/// dominating the list row.
fn default_preview_lines() -> u32 {
    2
}

/// A single user entry from users.toml.
#[derive(Debug, Deserialize)]
pub struct User {
    pub slug: String,
    pub api_key: String,
    pub web_password: String,
    pub role: String,
    /// ASR input language code (e.g. "de", "en"). Forwarded as the
    /// `language` form/query field to the active ASR backend
    /// (Whisper or Canary). Missing field → backend default ("de").
    #[serde(default)]
    pub language: Option<String>,
    #[serde(default)]
    pub retention: RetentionUserSettings,
    /// Time window (hours) that defines which cases are visible to this
    /// user's clients. Server-authoritative: the `/api/oneliners` scan
    /// filters recordings older than `now - window_hours` before they
    /// reach any client. Clients render what the server returns and
    /// apply no additional time filter.
    #[serde(default = "default_window_hours")]
    pub window_hours: u32,
    /// Number of analysis-preview lines rendered under each case row on
    /// the list view. The preview itself is always the first paragraph of
    /// `document.md`; this value only controls how many *visual* lines
    /// the browser will show before truncating with `…`. Applied via a
    /// CSS `line-clamp` custom property, so a single server value adapts
    /// to any viewport width.
    #[serde(default = "default_preview_lines")]
    pub preview_lines: u32,
}

/// Wrapper for deserializing the [[user]] array from TOML.
#[derive(Deserialize)]
struct UsersFile {
    user: Vec<User>,
}

/// Active ASR backend, picked once at server startup from the
/// `ASR_BACKEND` env var. `Whisper` (default) routes the worker to the
/// `whisper-asr-webservice` at `[whisper].url`; `Canary` routes it to
/// the NeMo Canary container at `[canary].url`. Switching backends
/// requires a server restart — both cannot run on the same GPU at
/// once (12 GB VRAM budget; see docs/canary.md §2.6).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AsrBackend {
    #[default]
    Whisper,
    Canary,
}

impl std::str::FromStr for AsrBackend {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_ascii_lowercase().as_str() {
            "whisper" => Ok(Self::Whisper),
            "canary" => Ok(Self::Canary),
            other => Err(format!("expected 'whisper' or 'canary', got '{other}'")),
        }
    }
}

/// Bootstrap config loaded once from `.env` at startup. Runtime-tunable
/// values (retention, whisper/ollama/llm endpoints, prompts) live in
/// `Settings` (loaded from `settings.toml`).
pub struct Config {
    // Phase 1 — required
    pub server_port: u16,
    pub data_path: PathBuf,
    pub log_level: String,
    pub log_path: PathBuf,
    pub log_max_days: u32,
    pub users: Vec<User>,
    pub api_keys: HashMap<String, String>, // api_key_value → slug

    // Phase 2+ — optional with defaults
    /// Directory containing gazetteer `*.txt` files (anatomy, substances,
    /// medications). Loaded once at startup; a missing directory is a
    /// non-fatal WARN — analysis runs without proper-name correction.
    pub vocab_dir: PathBuf,
    /// Path prefix (without extension) to a hunspell dictionary pair
    /// `<stem>.aff` + `<stem>.dic`. Used by the gazetteer as a dict
    /// veto against false-positive rewrites (e.g. `Kaktus` → `Lantus`).
    /// Missing / unreadable files are a non-fatal WARN — the gazetteer
    /// falls back to dict-less behaviour.
    pub hunspell_dict_path: PathBuf,
    pub session_timeout_hours: u32,
    /// Whether to set the `Secure` flag on the session cookie. Default `true`.
    /// Set `COOKIE_SECURE=false` only for plain-HTTP local development —
    /// browsers refuse `Secure` cookies on non-HTTPS origins.
    pub cookie_secure: bool,
    /// Selected ASR backend (Whisper or Canary). Read from `ASR_BACKEND`
    /// env var at startup; default `Whisper`. Worker uses this to dispatch
    /// the transcription call to the matching endpoint.
    pub asr_backend: AsrBackend,
}

impl Config {
    pub fn from_env() -> Self {
        let users_file = required_env("USERS_FILE");
        let (users, api_keys) = load_users(&users_file);

        Self {
            server_port: required_env_parsed("SERVER_PORT"),
            data_path: PathBuf::from(required_env("DATA_PATH")),
            log_level: optional_env("LOG_LEVEL", "info"),
            log_path: PathBuf::from(optional_env("LOG_PATH", "/var/log/recorder")),
            log_max_days: optional_env_parsed("LOG_MAX_DAYS", 90),
            users,
            api_keys,

            vocab_dir: PathBuf::from(optional_env("VOCAB_DIR", "./vocab")),
            hunspell_dict_path: PathBuf::from(optional_env(
                "HUNSPELL_DICT",
                "/usr/share/hunspell/de_DE",
            )),
            session_timeout_hours: optional_env_parsed("SESSION_TIMEOUT_HOURS", 8),
            cookie_secure: optional_env_parsed("COOKIE_SECURE", true),
            asr_backend: optional_env("ASR_BACKEND", "whisper")
                .parse::<AsrBackend>()
                .unwrap_or_else(|e| panic!("Invalid value for ASR_BACKEND: {e}")),
        }
    }

    /// Sane defaults for integration tests. Not a `Default` impl on purpose:
    /// production code must go through `from_env()`, and `Default::default()`
    /// carries an implicit "safe fallback" connotation this value does not
    /// satisfy (empty users, `/tmp` paths). Override fields via
    /// struct-update syntax:
    ///
    /// ```ignore
    /// let cfg = Config {
    ///     data_path: tmp_dir,
    ///     users: vec![my_user],
    ///     ..Config::test_default()
    /// };
    /// ```
    #[doc(hidden)]
    pub fn test_default() -> Self {
        Self {
            server_port: 3000,
            data_path: PathBuf::from("/tmp"),
            log_level: "info".into(),
            log_path: PathBuf::from("/tmp"),
            log_max_days: 90,
            users: Vec::new(),
            api_keys: HashMap::new(),
            vocab_dir: PathBuf::new(),
            hunspell_dict_path: PathBuf::new(),
            session_timeout_hours: 8,
            cookie_secure: false,
            asr_backend: AsrBackend::Whisper,
        }
    }
}

/// Load users from a TOML file and build the API key lookup map.
fn load_users(path: &str) -> (Vec<User>, HashMap<String, String>) {
    let content = std::fs::read_to_string(path)
        .unwrap_or_else(|e| panic!("Failed to read users file {path}: {e}"));

    let parsed: UsersFile = toml::from_str(&content)
        .unwrap_or_else(|e| panic!("Failed to parse users file {path}: {e}"));

    if parsed.user.is_empty() {
        panic!("No users defined in {path}. At least one is required.");
    }

    let api_keys = validate_and_index_users(&parsed.user)
        .unwrap_or_else(|e| panic!("Invalid users file {path}: {e}"));

    (parsed.user, api_keys)
}

/// Build the api_key → slug lookup map. Rejects duplicate slugs (collide on
/// data directories) and duplicate api_keys (ambiguous authentication).
pub fn validate_and_index_users(users: &[User]) -> Result<HashMap<String, String>, String> {
    let mut slugs: std::collections::HashSet<&str> = std::collections::HashSet::new();
    let mut api_keys: HashMap<String, String> = HashMap::new();

    for u in users {
        if !slugs.insert(u.slug.as_str()) {
            return Err(format!("duplicate slug: {}", u.slug));
        }
        if let Some(existing) = api_keys.insert(u.api_key.clone(), u.slug.clone()) {
            return Err(format!(
                "duplicate api_key shared by users {existing} and {}",
                u.slug
            ));
        }
    }

    Ok(api_keys)
}

/// Read a required environment variable. Panics with a clear message if missing.
fn required_env(name: &str) -> String {
    std::env::var(name).unwrap_or_else(|_| panic!("Missing required environment variable: {name}"))
}

/// Read a required environment variable and parse it. Panics if missing or unparseable.
fn required_env_parsed<T: std::str::FromStr>(name: &str) -> T
where
    T::Err: std::fmt::Display,
{
    let raw = required_env(name);
    raw.parse()
        .unwrap_or_else(|e| panic!("Invalid value for {name}: {e}"))
}

/// Read an optional environment variable, falling back to a default.
fn optional_env(name: &str, default: &str) -> String {
    std::env::var(name).unwrap_or_else(|_| default.to_owned())
}

/// Read an optional environment variable and parse it, falling back to a default.
fn optional_env_parsed<T: std::str::FromStr>(name: &str, default: T) -> T
where
    T::Err: std::fmt::Display,
{
    match std::env::var(name) {
        Ok(raw) => raw
            .parse()
            .unwrap_or_else(|e| panic!("Invalid value for {name}: {e}")),
        Err(_) => default,
    }
}

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

    fn u(slug: &str, api_key: &str) -> User {
        User {
            slug: slug.into(),
            api_key: api_key.into(),
            web_password: String::new(),
            role: "doctor".into(),
            language: None,
            retention: RetentionUserSettings::default(),
            window_hours: default_window_hours(),
            preview_lines: default_preview_lines(),
        }
    }

    #[test]
    fn parses_user_without_language() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        let u = &parsed.user[0];
        assert!(u.language.is_none());
    }

    #[test]
    fn parses_user_without_retention_block_defaults_to_zero() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        let r = &parsed.user[0].retention;
        // Both zero means "feature disabled", matching the pre-migration
        // status quo — no case is ever auto-closed or auto-purged.
        assert_eq!(r.auto_close_days, 0);
        assert_eq!(r.auto_delete_days, 0);
    }

    #[test]
    fn parses_user_with_retention_block() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
              [user.retention]
              auto_close_days = 7
              auto_delete_days = 30
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        let r = &parsed.user[0].retention;
        assert_eq!(r.auto_close_days, 7);
        assert_eq!(r.auto_delete_days, 30);
    }

    #[test]
    fn parses_user_without_preview_lines_defaults_to_2() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.user[0].preview_lines, 2);
    }

    #[test]
    fn parses_user_with_explicit_preview_lines() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
            preview_lines = 5
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.user[0].preview_lines, 5);
    }

    #[test]
    fn parses_user_with_partial_retention_block() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
              [user.retention]
              auto_close_days = 5
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        let r = &parsed.user[0].retention;
        assert_eq!(r.auto_close_days, 5);
        // Missing field defaults to 0 = disabled — lets admins test auto-close
        // in isolation without losing data to auto-purge.
        assert_eq!(r.auto_delete_days, 0);
    }

    #[test]
    fn parses_user_with_explicit_language() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
            language = "de"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.user[0].language.as_deref(), Some("de"));
    }

    #[test]
    fn accepts_admin_role() {
        let toml_str = r#"
            [[user]]
            slug = "root"
            api_key = "k1"
            web_password = ""
            role = "admin"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.user[0].role, "admin");
    }

    #[test]
    fn accepts_distinct_users() {
        let users = vec![u("a", "k1"), u("b", "k2")];
        let map = validate_and_index_users(&users).unwrap();
        assert_eq!(map.len(), 2);
        assert_eq!(map.get("k1").unwrap(), "a");
    }

    #[test]
    fn rejects_duplicate_api_keys() {
        let users = vec![u("a", "same"), u("b", "same")];
        let err = validate_and_index_users(&users).unwrap_err();
        assert!(err.contains("duplicate api_key"), "got: {err}");
    }

    #[test]
    fn rejects_duplicate_slugs() {
        let users = vec![u("same", "k1"), u("same", "k2")];
        let err = validate_and_index_users(&users).unwrap_err();
        assert!(err.contains("duplicate slug"), "got: {err}");
    }

    #[test]
    fn parses_user_without_window_hours_defaults_to_72() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        // Missing field falls back to the serde default so existing
        // users.toml files continue to work unchanged.
        assert_eq!(parsed.user[0].window_hours, 72);
    }

    #[test]
    fn parses_user_with_explicit_window_hours() {
        let toml_str = r#"
            [[user]]
            slug = "a"
            api_key = "k1"
            web_password = ""
            role = "doctor"
            window_hours = 168
        "#;
        let parsed: UsersFile = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.user[0].window_hours, 168);
    }

    // ----- AsrBackend -----

    #[test]
    fn asr_backend_default_is_whisper() {
        assert_eq!(AsrBackend::default(), AsrBackend::Whisper);
    }

    #[test]
    fn asr_backend_parses_whisper() {
        assert_eq!(
            "whisper".parse::<AsrBackend>().unwrap(),
            AsrBackend::Whisper
        );
    }

    #[test]
    fn asr_backend_parses_canary() {
        assert_eq!("canary".parse::<AsrBackend>().unwrap(), AsrBackend::Canary);
    }

    #[test]
    fn asr_backend_parses_case_insensitive_with_whitespace() {
        assert_eq!(
            "  Whisper  ".parse::<AsrBackend>().unwrap(),
            AsrBackend::Whisper
        );
        assert_eq!("CANARY".parse::<AsrBackend>().unwrap(), AsrBackend::Canary);
    }

    #[test]
    fn asr_backend_rejects_invalid() {
        let err = "wav2vec".parse::<AsrBackend>().unwrap_err();
        assert!(err.contains("wav2vec"), "got: {err}");
    }
}
