use emmylua_parser::{LuaAstNode, LuaDocGenericDeclList, LuaDocType};
use smol_str::SmolStr;

use crate::{
    FileId, GenericParam,
    compilation::analyzer::AnalyzeContext,
    db_index::{DbIndex, LuaType, WorkspaceId},
    semantic::complete_type_generic_args_in_type,
};

use super::{
    file_generic_index::FileGenericIndex,
    infer_type::{DocTypeAnalyzeContext, DocTypeAnalyzeOptions, infer_type},
};

pub(super) fn preprocess_type_generic_headers(db: &mut DbIndex, context: &mut AnalyzeContext) {
    let pending_headers = context.take_pending_type_generic_headers();
    if pending_headers.is_empty() {
        return;
    }

    let workspace_id = context.workspace_id.unwrap_or(WorkspaceId::MAIN);
    let mut resolved_headers = Vec::new();
    for pending_header in pending_headers {
        let type_id = pending_header.type_id;
        let params = resolve_generic_params(
            db,
            pending_header.file_id,
            workspace_id,
            pending_header.generic_decl_list,
        );

        resolved_headers.push((type_id, params));
    }

    // 先写入 raw metadata, 让后续 normalize 能跨类型看到所有泛型默认值.
    for (type_id, params) in &resolved_headers {
        if !params.is_empty() {
            db.get_type_index_mut()
                .add_generic_params(type_id.clone(), params.clone());
        }
    }

    // 再补齐 constraint/default 类型表达式里的泛型实参并覆盖 raw metadata.
    for (type_id, params) in resolved_headers {
        let normalized_params = normalize_generic_params(db, &params);
        if !normalized_params.is_empty() {
            db.get_type_index_mut()
                .add_generic_params(type_id, normalized_params);
        }
    }
}

fn normalize_generic_params(db: &DbIndex, params: &[GenericParam]) -> Vec<GenericParam> {
    params
        .iter()
        .map(|param| {
            GenericParam::new(
                param.name.clone(),
                param
                    .constraint
                    .as_ref()
                    .map(|ty| complete_type_generic_args_in_type(db, ty)),
                param
                    .default
                    .as_ref()
                    .map(|ty| complete_type_generic_args_in_type(db, ty)),
                param.is_const,
                param.attributes.clone(),
            )
        })
        .collect()
}

fn resolve_generic_params(
    db: &mut DbIndex,
    file_id: FileId,
    workspace_id: WorkspaceId,
    generic_decl_list: LuaDocGenericDeclList,
) -> Vec<GenericParam> {
    let mut generic_index = FileGenericIndex::new();
    let scope_id = generic_index.add_generic_scope(vec![generic_decl_list.get_range()], false);
    let mut declared_params = Vec::new();

    for generic_decl in generic_decl_list.get_generic_decl() {
        let Some(name_token) = generic_decl.get_name_token() else {
            continue;
        };

        let name = SmolStr::new(name_token.get_name_text());
        let placeholder = GenericParam::new(
            name.clone(),
            None,
            None,
            generic_decl.has_const_modifier(),
            None,
        );
        if let Some(tpl_id) = generic_index.append_generic_param(scope_id, placeholder) {
            declared_params.push((tpl_id, generic_decl, name));
        }
    }

    let mut params = Vec::new();

    for (tpl_id, generic_decl, name) in declared_params {
        let constraint = generic_decl.get_constraint_type().map(|type_ref| {
            infer_header_type(db, file_id, workspace_id, &mut generic_index, type_ref)
        });

        let default_type = generic_decl.get_default_type().map(|type_ref| {
            infer_header_type(db, file_id, workspace_id, &mut generic_index, type_ref)
        });

        let param = GenericParam::new(
            name,
            constraint,
            default_type,
            generic_decl.has_const_modifier(),
            None,
        );
        let _ = generic_index.update_generic_param(tpl_id, param.clone());
        params.push(param);
    }

    params
}

fn infer_header_type(
    db: &mut DbIndex,
    file_id: FileId,
    workspace_id: WorkspaceId,
    generic_index: &mut FileGenericIndex,
    type_ref: LuaDocType,
) -> LuaType {
    let mut context = DocTypeAnalyzeContext::new(db, file_id, generic_index, workspace_id)
        .with_options(DocTypeAnalyzeOptions::header_preprocess());
    infer_type(&mut context, type_ref)
}
