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

pub use crate::tpl::column::Column;

#[derive(Deserialize, Default)]
struct RawConfig {
    #[serde(default)]
    columns: serde_yaml::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>,
    ground_truth: 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 ground_truth: 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::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());
    }

    // 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),
        ground_truth: raw.options.ground_truth.unwrap_or(false),
        delim: raw.options.delim,
    };

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

/// Topological sort of columns by dependencies. Returns evaluation order.
/// Columns with no dependencies come first. Cycles produce an error.
pub fn topo_sort_columns(columns: &[Column]) -> Result<Vec<usize>, String> {
    let n = columns.len();
    let names: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();

    // Build adjacency: for each column, which columns it depends on (by index).
    let mut in_degree = vec![0u32; n];
    let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];

    for (i, col) in columns.iter().enumerate() {
        let deps = crate::tpl::dependencies(&col.gen);
        for dep_name in deps {
            if let Some(dep_idx) = names.iter().position(|n| *n == dep_name) {
                dependents[dep_idx].push(i);
                in_degree[i] += 1;
            }
            // dep_name not in columns → external (field spec source for aggr), skip
        }
    }

    // Kahn's algorithm
    let mut queue: Vec<usize> = (0..n).filter(|i| in_degree[*i] == 0).collect();
    let mut order = Vec::with_capacity(n);

    while let Some(idx) = queue.pop() {
        order.push(idx);
        for &dep in &dependents[idx] {
            in_degree[dep] -= 1;
            if in_degree[dep] == 0 {
                queue.push(dep);
            }
        }
    }

    if order.len() != n {
        let cycle: Vec<&str> = (0..n).filter(|i| in_degree[*i] > 0).map(|i| names[i]).collect();
        return Err(format!("circular dependency between columns: {}", cycle.join(", ")));
    }

    Ok(order)
}

/// Second pass: resolve expression result types using full column info.
/// At parse time, column references default to Int. Now we can check actual types.
pub fn resolve_expr_types(columns: &mut [Column]) -> Result<(), String> {
    // Collect a snapshot of columns for type resolution (avoids borrow issues).
    let snapshot: Vec<(String, crate::tpl::column::ColumnGen)> =
        columns.iter().map(|c| (c.name.clone(), unsafe_clone_gen(&c.gen))).collect();
    let snap_cols: Vec<Column> =
        snapshot.into_iter().map(|(name, gen)| Column { name, gen }).collect();

    for col in columns.iter_mut() {
        if let crate::tpl::ColumnGen::Expr { left, op, right, result_type } = &mut col.gen {
            let lt = operand_type(left, &snap_cols);
            let rt = operand_type(right, &snap_cols);
            *result_type = crate::tpl::check_expr_types(lt, *op, rt)
                .map_err(|e| format!("column '{}': {e}", col.name))?;
        }
    }
    Ok(())
}

fn operand_type(operand: &crate::tpl::ExprOperand, columns: &[Column]) -> crate::tpl::FieldType {
    match operand {
        crate::tpl::ExprOperand::Col(name) => {
            crate::tpl::column::resolve_col_field_type(name, columns)
        }
        crate::tpl::ExprOperand::Field { field, .. } => crate::tpl::field_type(field.name),
    }
}

/// Create a shallow copy of `ColumnGen` for type resolution.
/// Only needs to preserve the variant and field references, not deep data.
fn unsafe_clone_gen(gen: &crate::tpl::ColumnGen) -> crate::tpl::ColumnGen {
    use crate::tpl::ColumnGen;
    match gen {
        ColumnGen::Field { field, modifier, transform, range, ordering } => ColumnGen::Field {
            field,
            modifier: modifier.clone(),
            transform: *transform,
            range: *range,
            ordering: *ordering,
        },
        ColumnGen::Literal(s) => ColumnGen::Literal(s.clone()),
        ColumnGen::Aggr { func, source_col, group_by } => ColumnGen::Aggr {
            func: *func,
            source_col: source_col.clone(),
            group_by: group_by.clone(),
        },
        ColumnGen::Ref { source_col, modifier } => {
            ColumnGen::Ref { source_col: source_col.clone(), modifier: modifier.clone() }
        }
        ColumnGen::Expr { op, result_type, .. } => ColumnGen::Expr {
            left: crate::tpl::ExprOperand::Col(String::new()),
            op: *op,
            right: crate::tpl::ExprOperand::Col(String::new()),
            result_type: *result_type,
        },
    }
}

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

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 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(", ")
    ))
}
