use camino::Utf8Path;
use scip::types::{Document, Occurrence, SymbolInformation};

use crate::{core::schema::SchemaModel, parse::ParseFailure};

use super::{
    super::line_index::LineIndex,
    super::signature::{
        build_column_sig, build_function_sig, build_index_sig, build_policy_sig, build_table_sig,
        build_trigger_sig, build_type_sig, build_view_sig,
    },
    super::symbol::{build_descriptor, to_scip_symbol},
    build_failure_occurrence, def_occurrence,
    relationships::{
        build_column_relationships, build_function_relationships, build_trigger_relationships,
        build_view_relationships, ref_rel,
    },
};

struct EmitCtx<'a> {
    model: &'a SchemaModel,
    line_index: &'a LineIndex,
    occurrences: &'a mut Vec<Occurrence>,
    symbols: &'a mut Vec<SymbolInformation>,
}

/// Build the schema SCIP document with definitions, documentation, and relationships.
pub fn build_schema_document(
    schema_path: &Utf8Path,
    schema_content: &str,
    failures: &[ParseFailure],
    model: &SchemaModel,
) -> Document {
    let line_index = LineIndex::new(schema_content);
    let mut occurrences = Vec::new();
    let mut symbols = Vec::new();

    {
        let mut ctx = EmitCtx {
            model,
            line_index: &line_index,
            occurrences: &mut occurrences,
            symbols: &mut symbols,
        };
        emit_table_symbols(&mut ctx);
        emit_function_symbols(&mut ctx);
        emit_view_symbols(&mut ctx);
        emit_type_symbols(&mut ctx);
        emit_trigger_symbols(&mut ctx);
        emit_policy_symbols(&mut ctx);
        emit_index_symbols(&mut ctx);
    }

    for failure in failures {
        occurrences.push(build_failure_occurrence(failure, &line_index));
    }

    let mut doc = Document::new();
    doc.relative_path = schema_path.as_str().into();
    doc.language = "SQL".into();
    doc.occurrences = occurrences;
    doc.symbols = symbols;
    doc
}

fn emit_table_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.tables {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let doc_text = def.doc.clone().unwrap_or_default();
        let sig = build_table_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym.clone(),
            vec![doc_text],
            String::new(),
            ctx.model,
            &qn.to_string(),
            vec![],
            sig,
        ));

        for (col_name, col) in &def.columns {
            let col_sym_input = format!("{qn}#{col_name}");
            let col_sym = to_scip_symbol(&col_sym_input, ctx.model);
            let col_doc = col.display_doc();
            let rels = build_column_relationships(col, ctx.model);
            let col_sig = build_column_sig(col, ctx.model);
            ctx.occurrences
                .push(def_occurrence(&col_sym, ctx.line_index, col.range));
            ctx.symbols.push(symbol_info(
                col_sym,
                vec![col_doc],
                sym.clone(),
                ctx.model,
                &col_sym_input,
                rels,
                col_sig,
            ));
        }
    }
}

fn emit_function_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.functions {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let docs = def
            .doc
            .as_deref()
            .map_or_else(Vec::new, |d| vec![d.to_owned()]);
        let rels = build_function_relationships(def, ctx.model);
        let sig = build_function_sig(qn, def, ctx.model);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym,
            docs,
            String::new(),
            ctx.model,
            &qn.to_string(),
            rels,
            sig,
        ));
    }
}

fn emit_view_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.views {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let docs = def
            .doc
            .as_deref()
            .map_or_else(Vec::new, |d| vec![d.to_owned()]);
        let rels = build_view_relationships(def, ctx.model);
        let sig = build_view_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym,
            docs,
            String::new(),
            ctx.model,
            &qn.to_string(),
            rels,
            sig,
        ));
    }
}

fn emit_type_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.types {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let docs = def
            .doc
            .as_deref()
            .map_or_else(Vec::new, |d| vec![d.to_owned()]);
        let sig = build_type_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym.clone(),
            docs,
            String::new(),
            ctx.model,
            &qn.to_string(),
            vec![],
            sig,
        ));

        if def.kind == crate::core::schema::TypeKind::Composite {
            emit_composite_field_symbols(qn, def, &sym, ctx);
        }
    }
}

fn emit_composite_field_symbols(
    type_qn: &crate::core::schema::QualifiedName,
    def: &crate::core::schema::TypeDef,
    type_sym: &str,
    ctx: &mut EmitCtx<'_>,
) {
    for (field_name, col) in &def.fields {
        let field_input = format!("{type_qn}#{field_name}");
        let field_sym = to_scip_symbol(&field_input, ctx.model);
        let field_doc = col.display_doc();
        let rels = build_column_relationships(col, ctx.model);
        let field_sig = build_column_sig(col, ctx.model);
        ctx.occurrences
            .push(def_occurrence(&field_sym, ctx.line_index, col.range));
        ctx.symbols.push(symbol_info(
            field_sym,
            vec![field_doc],
            type_sym.to_owned(),
            ctx.model,
            &field_input,
            rels,
            field_sig,
        ));
    }
}

fn emit_trigger_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.triggers {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let rels = build_trigger_relationships(def, ctx.model);
        let sig = build_trigger_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym,
            vec![],
            String::new(),
            ctx.model,
            &qn.to_string(),
            rels,
            sig,
        ));
    }
}

fn emit_policy_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.policies {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let rels = vec![ref_rel(
            to_scip_symbol(&def.target.to_string(), ctx.model),
            false,
        )];
        let sig = build_policy_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym,
            vec![],
            String::new(),
            ctx.model,
            &qn.to_string(),
            rels,
            sig,
        ));
    }
}

fn emit_index_symbols(ctx: &mut EmitCtx<'_>) {
    for (qn, def) in &ctx.model.indexes {
        let sym = to_scip_symbol(&qn.to_string(), ctx.model);
        let rels = vec![ref_rel(
            to_scip_symbol(&def.target.to_string(), ctx.model),
            false,
        )];
        let sig = build_index_sig(qn, def);
        ctx.occurrences
            .push(def_occurrence(&sym, ctx.line_index, def.range));
        ctx.symbols.push(symbol_info(
            sym,
            vec![],
            String::new(),
            ctx.model,
            &qn.to_string(),
            rels,
            sig,
        ));
    }
}

fn symbol_info(
    sym: String,
    docs: Vec<String>,
    enclosing: String,
    model: &SchemaModel,
    input: &str,
    relationships: Vec<scip::types::Relationship>,
    sig_doc: Document,
) -> SymbolInformation {
    let meta = build_descriptor(input, model);
    let mut si = SymbolInformation::new();
    si.symbol = sym;
    si.documentation = docs;
    si.enclosing_symbol = enclosing;
    si.display_name = meta.display_name;
    si.kind = meta.kind.into();
    si.relationships = relationships;
    si.signature_documentation = Some(sig_doc).into();
    si
}

#[cfg(test)]
mod tests {
    use camino::Utf8Path;

    use crate::{
        core::{ddl::walk_ddl, schema::SchemaModel},
        parse::split_and_parse,
    };

    use super::build_schema_document;

    fn build_model(sql: &str) -> SchemaModel {
        let (stmts, _) = split_and_parse(sql).expect("must parse");
        let mut model = SchemaModel::default();
        walk_ddl(&stmts, &mut model);
        model
    }

    #[test]
    fn composite_type_field_symbols_are_emitted() {
        let sql = concat!(
            "CREATE TYPE public.address AS (\n",
            "    street text,\n",
            "    city text,\n",
            "    zip text\n",
            ");\n",
        );
        let model = build_model(sql);
        let doc = build_schema_document(Utf8Path::new("schema.sql"), sql, &[], &model);

        let field_syms: Vec<_> = doc
            .symbols
            .iter()
            .filter(|si| {
                si.symbol.contains("public/address#")
                    && !si.symbol.trim_end_matches('.').ends_with('#')
            })
            .collect();

        assert_eq!(
            field_syms.len(),
            3,
            "composite type must emit one symbol per field; got: {:?}",
            field_syms.iter().map(|s| &s.symbol).collect::<Vec<_>>(),
        );
    }

    #[test]
    fn trigger_symbol_has_function_relationship() {
        let sql = concat!(
            "CREATE TABLE public.items (id bigint NOT NULL);\n",
            "CREATE FUNCTION public.set_ts() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$;\n",
            "CREATE TRIGGER items_set_ts BEFORE UPDATE ON public.items FOR EACH ROW EXECUTE FUNCTION public.set_ts();\n",
        );
        let model = build_model(sql);
        let doc = build_schema_document(Utf8Path::new("schema.sql"), sql, &[], &model);

        let trigger_sym = doc
            .symbols
            .iter()
            .find(|si| si.display_name == "items_set_ts")
            .expect("trigger symbol must exist in document");

        let has_func_rel = trigger_sym
            .relationships
            .iter()
            .any(|r| r.is_reference && r.symbol.contains("set_ts"));
        assert!(
            has_func_rel,
            "trigger symbol must have reference relationship to EXECUTE FUNCTION target; \
             got relationships: {:?}",
            trigger_sym.relationships
        );
    }
}
