use scip::types::{Document, Occurrence, SyntaxKind};

use crate::core::schema::{
    ColumnDef, FunctionDef, IndexDef, PolicyDef, QualifiedName, SchemaModel, TableDef, TriggerDef,
    TypeDef, TypeKind, ViewDef,
};

use super::symbol::to_scip_symbol;

fn sql_qn(qn: &QualifiedName) -> String {
    format!("{}.{}", qn.schema, qn.name)
}

#[must_use]
pub(crate) fn build_table_sig(qn: &QualifiedName, _def: &TableDef) -> Document {
    make_sig(format!("TABLE {}", sql_qn(qn)))
}

#[must_use]
pub(crate) fn build_view_sig(qn: &QualifiedName, _def: &ViewDef) -> Document {
    make_sig(format!("VIEW {}", sql_qn(qn)))
}

#[must_use]
pub(crate) fn build_type_sig(qn: &QualifiedName, def: &TypeDef) -> Document {
    let kind_str = match def.kind {
        TypeKind::Enum => "ENUM",
        TypeKind::Composite => "COMPOSITE",
    };
    make_sig(format!("TYPE {} AS {kind_str}", sql_qn(qn)))
}

#[must_use]
pub(crate) fn build_trigger_sig(name: &QualifiedName, def: &TriggerDef) -> Document {
    make_sig(format!("TRIGGER {} ON {}", name.name, sql_qn(&def.target)))
}

#[must_use]
pub(crate) fn build_policy_sig(name: &QualifiedName, def: &PolicyDef) -> Document {
    make_sig(format!("POLICY {} ON {}", name.name, sql_qn(&def.target)))
}

#[must_use]
pub(crate) fn build_index_sig(name: &QualifiedName, def: &IndexDef) -> Document {
    make_sig(format!("INDEX {} ON {}", name.name, sql_qn(&def.target)))
}

// anonymous params (empty name) render as type only: `bigint, text`
fn render_params(params: &[(String, String)]) -> String {
    params
        .iter()
        .map(|(name, ty)| {
            if name.is_empty() {
                ty.clone()
            } else {
                format!("{name} {ty}")
            }
        })
        .collect::<Vec<_>>()
        .join(", ")
}

#[must_use]
pub(crate) fn build_function_sig(
    qn: &QualifiedName,
    def: &FunctionDef,
    model: &SchemaModel,
) -> Document {
    let qn_str = sql_qn(qn);
    let params_str = render_params(&def.params);
    match &def.return_type {
        Some(rt) => {
            let text = format!("FUNCTION {qn_str}({params_str}) RETURNS {}", rt.name);
            let prefix_len = format!("FUNCTION {qn_str}({params_str}) RETURNS ").len();
            let mut doc = make_sig(text);
            if model.types.contains_key(rt) {
                let sym = to_scip_symbol(&rt.to_string(), model);
                doc.occurrences
                    .push(type_ref_occ(sym, prefix_len, rt.name.len()));
            }
            doc
        }
        None => make_sig(format!("FUNCTION {qn_str}({params_str})")),
    }
}

#[must_use]
pub(crate) fn build_column_sig(col: &ColumnDef, model: &SchemaModel) -> Document {
    let text = col.display_doc();
    let type_name_len = col.ty.len();
    let mut doc = make_sig(text);
    if let Some(ref type_qn) = col.type_ref {
        if model.types.contains_key(type_qn) {
            let sym = to_scip_symbol(&type_qn.to_string(), model);
            // The type name starts at character 0 in display_doc().
            doc.occurrences.push(type_ref_occ(sym, 0, type_name_len));
        }
    }
    doc
}

// range format: [0, char_start, char_start+len] — line 0 in signature text
fn type_ref_occ(symbol: String, char_start: usize, len: usize) -> Occurrence {
    let start = i32::try_from(char_start).unwrap_or(0);
    let end = i32::try_from(char_start + len).unwrap_or(0);
    let mut occ = Occurrence::new();
    occ.symbol = symbol;
    occ.range = vec![0, start, end];
    occ.symbol_roles = 0;
    occ.syntax_kind = SyntaxKind::Identifier.into();
    occ
}

fn make_sig(text: String) -> Document {
    let mut doc = Document::new();
    doc.language = "SQL".into();
    doc.text = text;
    doc
}

#[cfg(test)]
mod tests {
    use indexmap::IndexMap;
    use text_size::TextRange;

    use super::*;
    use crate::core::schema::{FunctionDef, SchemaModel, TableDef, TypeDef};

    fn empty_model() -> SchemaModel {
        SchemaModel::default()
    }

    fn model_with_type(qn: QualifiedName) -> SchemaModel {
        use indexmap::IndexMap;
        let mut model = SchemaModel::default();
        model.types.insert(
            qn,
            TypeDef {
                kind: TypeKind::Enum,
                range: TextRange::default(),
                doc: None,
                fields: IndexMap::new(),
                labels: Vec::new(),
            },
        );
        model
    }

    #[test]
    fn table_sig_text_and_language() {
        let qn = QualifiedName::new(Some("public"), "users");
        let def = TableDef {
            columns: IndexMap::new(),
            range: TextRange::default(),
            doc: None,
        };
        let doc = build_table_sig(&qn, &def);
        assert_eq!(doc.language, "SQL");
        assert_eq!(doc.text, "TABLE public.users");
    }

    #[test]
    fn function_sig_with_return_type_has_inner_occ() {
        let ret_qn = QualifiedName::new(Some("public"), "item_status");
        let mut model = model_with_type(ret_qn.clone());
        let fn_qn = QualifiedName::new(Some("public"), "get_status");
        let def = FunctionDef {
            range: TextRange::default(),
            doc: None,
            return_type: Some(ret_qn.clone()),
            param_types: Vec::new(),
            params: Vec::new(),
        };
        model.functions.insert(fn_qn.clone(), def);

        let def_ref = &model.functions[&fn_qn];
        let doc = build_function_sig(&fn_qn, def_ref, &model);
        assert_eq!(doc.language, "SQL");
        assert!(doc.text.contains("FUNCTION"), "must contain FUNCTION");
        assert!(doc.text.contains("RETURNS"), "must contain RETURNS");
        assert_eq!(doc.occurrences.len(), 1, "must have one inner occurrence");
    }

    #[test]
    fn column_sig_with_type_ref_has_inner_occ() {
        let type_qn = QualifiedName::new(Some("public"), "item_status");
        let model = model_with_type(type_qn.clone());
        let col = ColumnDef {
            ty: "item_status".to_owned(),
            nullable: true,
            range: TextRange::default(),
            doc: None,
            type_ref: Some(type_qn),
            fk_target: None,
            default_expr: None,
        };
        let doc = build_column_sig(&col, &model);
        assert_eq!(doc.language, "SQL");
        assert_eq!(
            doc.occurrences.len(),
            1,
            "must have one inner occurrence for type ref"
        );
    }

    #[test]
    fn trigger_sig_text() {
        let name = QualifiedName::new(Some("public"), "trg_audit");
        let def = TriggerDef {
            target: QualifiedName::new(Some("public"), "items"),
            range: TextRange::default(),
            func: None,
        };
        let doc = build_trigger_sig(&name, &def);
        assert_eq!(doc.language, "SQL");
        assert!(
            doc.text.starts_with("TRIGGER trg_audit ON"),
            "got: {}",
            doc.text
        );
    }

    #[test]
    fn column_sig_no_type_ref_no_inner_occ() {
        let col = ColumnDef {
            ty: "bigint".to_owned(),
            nullable: false,
            range: TextRange::default(),
            doc: None,
            type_ref: None,
            fk_target: None,
            default_expr: None,
        };
        let doc = build_column_sig(&col, &empty_model());
        assert!(
            doc.occurrences.is_empty(),
            "no type_ref means no inner occurrences"
        );
    }

    #[test]
    fn function_sig_renders_param_names_and_types() {
        let fn_qn = QualifiedName::new(Some("public"), "f");
        let def = FunctionDef {
            range: TextRange::default(),
            doc: None,
            return_type: None,
            param_types: Vec::new(),
            params: vec![
                ("p_id".to_owned(), "bigint".to_owned()),
                ("p_name".to_owned(), "text".to_owned()),
            ],
        };
        let model = empty_model();
        let doc = build_function_sig(&fn_qn, &def, &model);
        assert!(
            doc.text.contains("p_id bigint"),
            "must contain named param; got: {}",
            doc.text
        );
        assert!(
            doc.text.contains("p_name text"),
            "must contain named param; got: {}",
            doc.text
        );
    }

    #[test]
    fn function_sig_with_no_params_renders_empty_parens() {
        let fn_qn = QualifiedName::new(Some("public"), "f");
        let def = FunctionDef {
            range: TextRange::default(),
            doc: None,
            return_type: None,
            param_types: Vec::new(),
            params: Vec::new(),
        };
        let doc = build_function_sig(&fn_qn, &def, &empty_model());
        assert!(
            doc.text.contains("()"),
            "zero-param function must contain empty parens; got: {}",
            doc.text,
        );
    }

    #[test]
    fn function_sig_anonymous_param_renders_type_only() {
        let fn_qn = QualifiedName::new(Some("public"), "f");
        let def = FunctionDef {
            range: TextRange::default(),
            doc: None,
            return_type: None,
            param_types: Vec::new(),
            params: vec![("".to_owned(), "bigint".to_owned())],
        };
        let doc = build_function_sig(&fn_qn, &def, &empty_model());
        assert!(
            doc.text.contains("(bigint)"),
            "anonymous param must render as type only; got: {}",
            doc.text,
        );
    }
}
