use serde::Deserialize;
use std::path::Path;

pub use seedfaker_core::eval::{resolve_expr_types, topo_sort_columns, Column};

#[derive(Deserialize, Default)]
struct RawConfig {
    #[serde(default)]
    columns: serde_yaml_bw::Mapping,
    template: Option<String>,
    #[serde(default)]
    options: RawConfigOptions,
}

#[derive(Deserialize, Default)]
struct RawConfigOptions {
    seed: Option<String>,
    #[serde(default)]
    locale: Vec<String>,
    ctx: Option<String>,
    corrupt: Option<String>,
    abc: Option<String>,
    count: Option<u64>,
    format: Option<String>,
    rate: Option<u64>,
    tz: Option<String>,
    since: Option<String>,
    until: Option<String>,
    no_header: Option<bool>,
    delim: Option<String>,
    fingerprint: Option<String>,
    validate: Option<bool>,
    annotated: Option<bool>,
}

pub struct GenConfig {
    pub columns: Vec<Column>,
    /// Evaluation order — indices into `columns`, topologically sorted by dependencies.
    pub eval_order: Vec<usize>,
    pub template: Option<String>,
    pub options: GenConfigOptions,
}

#[derive(Default)]
pub struct GenConfigOptions {
    pub seed: Option<String>,
    pub locale: Vec<String>,
    pub ctx: Option<String>,
    pub corrupt: Option<String>,
    pub abc: Option<String>,
    pub count: Option<u64>,
    pub format: Option<String>,
    pub rate: Option<u64>,
    pub tz: Option<String>,
    pub since: Option<String>,
    pub until: Option<String>,
    pub no_header: bool,
    pub delim: Option<String>,
    pub validate: bool,
    pub annotated: bool,
}

const PRESETS: &[(&str, &str)] = &[
    ("nginx", include_str!("presets/nginx.yaml")),
    ("auth", include_str!("presets/auth.yaml")),
    ("app-json", include_str!("presets/app-json.yaml")),
    ("postgres", include_str!("presets/postgres.yaml")),
    ("payment", include_str!("presets/payment.yaml")),
    ("pii-leak", include_str!("presets/pii-leak.yaml")),
    ("user-table", include_str!("presets/user-table.yaml")),
    ("email", include_str!("presets/email-thread.yaml")),
    ("stacktrace", include_str!("presets/stacktrace.yaml")),
    ("chaos", include_str!("presets/chaos.yaml")),
    ("llm-prompt", include_str!("presets/llm-prompt.yaml")),
    ("syslog", include_str!("presets/syslog.yaml")),
    ("medical", include_str!("presets/medical.yaml")),
];

pub fn list_presets() -> Vec<&'static str> {
    PRESETS.iter().map(|(name, _)| *name).collect()
}

pub fn parse(source: &str) -> Result<GenConfig, String> {
    let raw: RawConfig =
        serde_yaml_bw::from_str(source).map_err(|e| format!("invalid config: {e}"))?;

    if raw.columns.is_empty() && raw.template.is_none() {
        return Err("config must have 'columns' or 'template'".into());
    }

    if let Some(ref expected) = raw.options.fingerprint {
        let current = seedfaker_core::fingerprint();
        if *expected != current {
            return Err(format!(
                "config fingerprint {expected} does not match current {current}; \
                 output would differ — update or remove fingerprint from config"
            ));
        }
    }

    // Collect all column names first (for free-order resolution).
    let all_names: Vec<String> =
        raw.columns.keys().filter_map(|k| k.as_str().map(str::to_string)).collect();

    let mut columns = Vec::with_capacity(raw.columns.len());
    for (key, value) in &raw.columns {
        let name = key
            .as_str()
            .ok_or_else(|| format!("column key must be a string, got: {key:?}"))?
            .to_string();
        let text = extract_scalar(value)?;
        let gen = crate::tpl::resolve_column(&name, &text, &all_names)?;
        columns.push(Column { name, gen });
    }

    // Second pass: resolve expression result types using full column info.
    resolve_expr_types(&mut columns)?;
    let eval_order = topo_sort_columns(&columns)?;

    let options = GenConfigOptions {
        seed: raw.options.seed,
        locale: raw.options.locale,
        ctx: raw.options.ctx,
        corrupt: raw.options.corrupt,
        abc: raw.options.abc,
        count: raw.options.count,
        format: raw.options.format,
        rate: raw.options.rate,
        tz: raw.options.tz,
        since: raw.options.since,
        until: raw.options.until,
        no_header: raw.options.no_header.unwrap_or(false),
        delim: raw.options.delim,
        validate: raw.options.validate.unwrap_or(false),
        annotated: raw.options.annotated.unwrap_or(false),
    };

    Ok(GenConfig { columns, eval_order, template: raw.template, options })
}

/// Extract scalar string from a YAML value, ignoring any tags.
fn extract_scalar(val: &serde_yaml_bw::Value) -> Result<String, String> {
    match val {
        serde_yaml_bw::Value::Tagged(tv) => extract_scalar(&tv.value),
        serde_yaml_bw::Value::String(s, _) => Ok(s.clone()),
        serde_yaml_bw::Value::Number(n, _) => Ok(n.to_string()),
        serde_yaml_bw::Value::Bool(b, _) => Ok(b.to_string()),
        serde_yaml_bw::Value::Null(_) => Ok(String::new()),
        other => Err(format!("unexpected column value: {other:?}")),
    }
}

/// Maximum config file size: 100 MB.
/// Enough for 300+ tables with 1000-line templates each.
/// Prevents accidental reads of /dev/urandom, huge binaries, etc.
const MAX_CONFIG_SIZE: u64 = 100 * 1024 * 1024;

pub fn load_config(name_or_path: &str) -> Result<GenConfig, String> {
    for (bname, content) in PRESETS {
        if *bname == name_or_path {
            return parse(content);
        }
    }

    let path = Path::new(name_or_path);
    if path.exists() {
        let meta =
            std::fs::metadata(path).map_err(|e| format!("cannot stat '{name_or_path}': {e}"))?;

        if !meta.is_file() {
            return Err(format!("'{name_or_path}' is not a regular file"));
        }

        if meta.len() > MAX_CONFIG_SIZE {
            return Err(format!(
                "config file too large: {} bytes (max {} MB). \
                 Split into multiple configs or check the file path.",
                meta.len(),
                MAX_CONFIG_SIZE / (1024 * 1024)
            ));
        }

        let content = std::fs::read_to_string(path)
            .map_err(|e| format!("cannot read '{name_or_path}': {e}"))?;
        return parse(&content);
    }

    let names: Vec<&str> = PRESETS.iter().map(|(n, _)| *n).collect();
    Err(format!(
        "'{name_or_path}' is not a file or preset; available presets: {}",
        names.join(", ")
    ))
}
