//! DDL-string generators split out of `engine.rs` to keep that file
//! under the Constructor-Pattern 200-LOC cap. One function per emitted
//! `CREATE` statement; the engine's `run_migrations` orchestrates the
//! calls and stamps `user_version`.

use crate::schema::{EdgeKeyKind, EntitySchema, FieldDef, FieldKind};

pub fn primary_table(schema: &EntitySchema) -> String {
    let cols: Vec<String> = schema.fields.iter().map(column).collect();
    format!(
        "CREATE TABLE IF NOT EXISTS {} (\n  {}\n);",
        schema.table,
        cols.join(",\n  ")
    )
}

fn column(f: &FieldDef) -> String {
    match f.kind {
        FieldKind::IntegerPk => format!("{} INTEGER PRIMARY KEY", f.name),
        FieldKind::TextPk => format!("{} TEXT PRIMARY KEY", f.name),
        FieldKind::IntegerNotNull => format!("{} INTEGER NOT NULL", f.name),
        FieldKind::Integer => format!("{} INTEGER DEFAULT 0", f.name),
        FieldKind::TextNotNull => format!("{} TEXT NOT NULL", f.name),
        FieldKind::Text => format!("{} TEXT DEFAULT ''", f.name),
        FieldKind::TextDefault => text_default_column(f),
        FieldKind::TextArchiveEnum => archive_enum_column(f),
        FieldKind::Real => format!("{} REAL NOT NULL DEFAULT 0.0", f.name),
        FieldKind::RealDefault => real_default_column(f),
        FieldKind::TimestampCreated => format!("{} INTEGER NOT NULL", f.name),
        FieldKind::TimestampUpdated => format!("{} INTEGER NOT NULL", f.name),
    }
}

fn text_default_column(f: &FieldDef) -> String {
    let d = f.default.unwrap_or("");
    // SQL-escape embedded single quotes (per SQL standard: `'` → `''`)
    // so `text_default("status", "don't know")` does not inject.
    let escaped = d.replace('\'', "''");
    format!("{} TEXT NOT NULL DEFAULT '{}'", f.name, escaped)
}

fn archive_enum_column(f: &FieldDef) -> String {
    let (active, _archived) = f.archive_enum.unwrap_or(("active", "archived"));
    let escaped = active.replace('\'', "''");
    format!("{} TEXT NOT NULL DEFAULT '{}'", f.name, escaped)
}

fn real_default_column(f: &FieldDef) -> String {
    let d = f.real_default.unwrap_or(0.0);
    format!("{} REAL NOT NULL DEFAULT {}", f.name, format_real(d))
}

/// Deterministic SQL literal for an f64 — always has a decimal point,
/// no exponent for finite values. Non-finite values fall back to 0.0.
fn format_real(v: f64) -> String {
    if !v.is_finite() {
        return "0.0".to_string();
    }
    if v.fract() == 0.0 {
        format!("{:.1}", v)
    } else {
        format!("{}", v)
    }
}

pub fn indexes(schema: &EntitySchema) -> String {
    let mut out = String::new();
    for f in schema.fields.iter().filter(|f| f.indexed) {
        out.push_str(&format!(
            "CREATE INDEX IF NOT EXISTS idx_{t}_{c} ON {t}({c});\n",
            t = schema.table,
            c = f.name
        ));
    }
    out
}

pub fn fts_table(table: &str, cols: &[&str]) -> String {
    let col_list = cols.join(", ");
    format!(
        "CREATE VIRTUAL TABLE IF NOT EXISTS fts_{table} \
         USING fts5({table}_id UNINDEXED, {col_list}, tokenize='porter unicode61');"
    )
}

/// Dispatcher — picks edge-table DDL for a given `EdgeKeyKind`. Added
/// for kei-sage migration; `IntegerPair` branch preserves legacy body.
pub fn edge_table_for(edge: &str, kind: EdgeKeyKind) -> String {
    match kind {
        EdgeKeyKind::IntegerPair => edge_integer(edge),
        EdgeKeyKind::TextPair => edge_text(edge),
        EdgeKeyKind::TextPairWithMetadata {
            from_col,
            to_col,
            has_id,
            has_weight,
            has_created_at,
            extra_columns,
        } => edge_text_meta(
            edge,
            from_col,
            to_col,
            has_id,
            has_weight,
            has_created_at,
            extra_columns,
        ),
    }
}

fn edge_integer(edge: &str) -> String {
    format!(
        "CREATE TABLE IF NOT EXISTS {edge} (\n  \
            from_id INTEGER NOT NULL,\n  \
            to_id INTEGER NOT NULL,\n  \
            edge_type TEXT NOT NULL DEFAULT 'links',\n  \
            PRIMARY KEY(from_id, to_id, edge_type)\n\
         );\n\
         CREATE INDEX IF NOT EXISTS idx_{edge}_to ON {edge}(to_id);"
    )
}

/// Text-keyed edge DDL: `(src_path TEXT, dst_path TEXT, edge_type TEXT)`.
fn edge_text(edge: &str) -> String {
    format!(
        "CREATE TABLE IF NOT EXISTS {edge} (\n  \
            src_path TEXT NOT NULL,\n  \
            dst_path TEXT NOT NULL,\n  \
            edge_type TEXT NOT NULL DEFAULT 'links',\n  \
            PRIMARY KEY(src_path, dst_path, edge_type)\n\
         );\n\
         CREATE INDEX IF NOT EXISTS idx_{edge}_dst ON {edge}(dst_path);"
    )
}

/// Text-keyed edge DDL with optional metadata columns + caller-chosen
/// key column names + arbitrary extra columns.
fn edge_text_meta(
    edge: &str,
    from_col: &str,
    to_col: &str,
    has_id: bool,
    has_weight: bool,
    has_created_at: bool,
    extras: &[(&str, FieldKind)],
) -> String {
    let mut cols: Vec<String> = Vec::new();
    if has_id {
        cols.push("edge_id INTEGER PRIMARY KEY AUTOINCREMENT".to_string());
    }
    cols.push(format!("{from_col} TEXT NOT NULL"));
    cols.push(format!("{to_col} TEXT NOT NULL"));
    cols.push("edge_type TEXT NOT NULL DEFAULT 'links'".to_string());
    if has_weight {
        cols.push("weight REAL NOT NULL DEFAULT 1.0".to_string());
    }
    for (name, kind) in extras {
        cols.push(extra_column(name, *kind));
    }
    if has_created_at {
        cols.push("created_at INTEGER NOT NULL".to_string());
    }
    // Without an autoincrement PK we still want `INSERT OR IGNORE`
    // idempotent over the triple; with one we emit a UNIQUE instead.
    if has_id {
        cols.push(format!("UNIQUE({from_col}, {to_col}, edge_type)"));
    } else {
        cols.push(format!("PRIMARY KEY({from_col}, {to_col}, edge_type)"));
    }
    let body = cols.join(",\n  ");
    format!(
        "CREATE TABLE IF NOT EXISTS {edge} (\n  {body}\n);\n\
         CREATE INDEX IF NOT EXISTS idx_{edge}_dst ON {edge}({to_col});"
    )
}

/// DDL for one extra edge column. Limited subset of `FieldKind` — edge
/// extras can't be PKs, archive enums, or auto-stamped timestamps.
fn extra_column(name: &str, kind: FieldKind) -> String {
    match kind {
        FieldKind::Text => format!("{name} TEXT DEFAULT ''"),
        FieldKind::TextNotNull => format!("{name} TEXT NOT NULL"),
        FieldKind::Integer => format!("{name} INTEGER DEFAULT 0"),
        FieldKind::IntegerNotNull => format!("{name} INTEGER NOT NULL"),
        FieldKind::Real => format!("{name} REAL NOT NULL DEFAULT 0.0"),
        other => panic!(
            "edge extra_columns: unsupported FieldKind {other:?} for column '{name}'"
        ),
    }
}
