use glua_parser::{
    LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaDocDescriptionOwner, LuaDocTagAs, LuaDocTagCast,
    LuaDocTagModule, LuaDocTagOther, LuaDocTagOutparam, LuaDocTagOverload, LuaDocTagParam,
    LuaDocTagReturn, LuaDocTagReturnCast, LuaDocTagSchema, LuaDocTagSee, LuaDocTagType,
    LuaDocTypeFlag, LuaExpr, LuaIndexKey, LuaLocalName, LuaTokenKind, LuaVarExpr,
};

use super::{
    DocAnalyzer, apply_nullable_doc_default, convert_doc_default_value,
    infer_type::infer_type,
    preprocess_description,
    tags::{find_owner_closure, get_owner_id_or_report},
};
use crate::{
    AnalyzeError, DiagnosticCode, LuaAttributeUse,
    compilation::analyzer::doc::{
        attribute_tags::{find_attach_attribute, infer_attribute_uses},
        tags::{find_owner_closure_or_report, get_owner_id, report_orphan_tag},
    },
};
use crate::{
    InFiled, JsonSchemaFile, LuaOperatorMetaMethod, LuaTypeCache, LuaTypeOwner, OperatorFunction,
    ReturnTypeKind, SignatureReturnStatus,
    compilation::analyzer::common::bind_type,
    db_index::{
        AccessorFuncAnnotation, LuaDeclId, LuaDocParamInfo, LuaDocReturnInfo, LuaInstanceType,
        LuaMemberId, LuaOperator, LuaOutParamInfo, LuaSemanticDeclId, LuaSignatureId, LuaType,
    },
};

pub fn analyze_type(analyzer: &mut DocAnalyzer, tag: LuaDocTagType) -> Option<()> {
    let description = tag
        .get_description()
        .map(|des| preprocess_description(&des.get_description_text(), None));

    let type_kind = get_return_type_kind(tag.get_type_flag());

    let mut type_list = Vec::new();
    for lua_doc_type in tag.get_type_list() {
        let mut type_ref = infer_type(analyzer, lua_doc_type);
        match type_kind {
            ReturnTypeKind::Instance => {
                let range = InFiled::new(analyzer.file_id, tag.syntax().text_range());
                type_ref = LuaType::Instance(LuaInstanceType::new(type_ref, range).into());
            }
            ReturnTypeKind::Definition => {
                if let LuaType::Ref(ref_id) = type_ref {
                    type_ref = LuaType::Def(ref_id);
                }
            }
            ReturnTypeKind::Reference => {}
        }
        type_list.push(type_ref);
    }

    // bind ref type
    bind_type_to_owner(analyzer, &tag, &type_list, description);
    Some(())
}

fn bind_type_to_owner(
    analyzer: &mut DocAnalyzer,
    tag: &impl LuaAstNode,
    type_list: &[LuaType],
    description: Option<String>,
) -> Option<()> {
    let Some(owner) = analyzer.comment.get_owner() else {
        report_orphan_tag(analyzer, tag);
        return None;
    };
    match owner {
        LuaAst::LuaAssignStat(assign_stat) => {
            let (vars, _) = assign_stat.get_var_and_expr_list();
            let min_len = vars.len().min(type_list.len());
            for i in 0..min_len {
                let var_expr = vars.get(i)?;
                let type_ref = type_list.get(i)?;
                match var_expr {
                    LuaVarExpr::NameExpr(name_expr) => {
                        let name_token = name_expr.get_name_token()?;
                        let position = name_token.get_position();
                        let file_id = analyzer.file_id;
                        let decl_id = LuaDeclId::new(file_id, position);
                        analyzer
                            .db
                            .get_type_index_mut()
                            .bind_type(decl_id.into(), LuaTypeCache::DocType(type_ref.clone()));

                        // bind description
                        if let Some(ref desc) = description
                            && !desc.is_empty()
                        {
                            analyzer.db.get_property_index_mut().add_description(
                                analyzer.file_id,
                                LuaSemanticDeclId::LuaDecl(decl_id),
                                desc.clone(),
                            );
                        }
                    }
                    LuaVarExpr::IndexExpr(index_expr) => {
                        let member_id =
                            LuaMemberId::new(index_expr.get_syntax_id(), analyzer.file_id);
                        analyzer
                            .db
                            .get_type_index_mut()
                            .bind_type(member_id.into(), LuaTypeCache::DocType(type_ref.clone()));

                        // bind description
                        if let Some(ref desc) = description
                            && !desc.is_empty()
                        {
                            analyzer.db.get_property_index_mut().add_description(
                                analyzer.file_id,
                                LuaSemanticDeclId::Member(member_id),
                                desc.clone(),
                            );
                        }
                    }
                }
            }
        }
        LuaAst::LuaLocalStat(local_assign_stat) => {
            let local_list: Vec<LuaLocalName> = local_assign_stat.get_local_name_list().collect();
            let min_len = local_list.len().min(type_list.len());
            for i in 0..min_len {
                let local_name = local_list.get(i)?;
                let type_ref = type_list.get(i)?;
                let name_token = local_name.get_name_token()?;
                let position = name_token.get_position();
                let file_id = analyzer.file_id;
                let decl_id = LuaDeclId::new(file_id, position);

                analyzer
                    .db
                    .get_type_index_mut()
                    .bind_type(decl_id.into(), LuaTypeCache::DocType(type_ref.clone()));

                // bind description
                if let Some(ref desc) = description
                    && !desc.is_empty()
                {
                    analyzer.db.get_property_index_mut().add_description(
                        analyzer.file_id,
                        LuaSemanticDeclId::LuaDecl(decl_id),
                        desc.clone(),
                    );
                }
            }
        }
        LuaAst::LuaTableField(table_field) => {
            if let Some(first_type) = type_list.first() {
                let member_id = LuaMemberId::new(table_field.get_syntax_id(), analyzer.file_id);

                analyzer
                    .db
                    .get_type_index_mut()
                    .bind_type(member_id.into(), LuaTypeCache::DocType(first_type.clone()));

                // bind description
                if let Some(ref desc) = description
                    && !desc.is_empty()
                {
                    analyzer.db.get_property_index_mut().add_description(
                        analyzer.file_id,
                        LuaSemanticDeclId::Member(member_id),
                        desc.clone(),
                    );
                }
            }
        }
        LuaAst::LuaReturnStat(return_stat) => {
            if let Some(first_type) = type_list.first() {
                let file_id = analyzer.file_id;
                let syntax_id = return_stat.get_syntax_id();
                let in_file_syntax_id = InFiled::new(file_id, syntax_id);
                analyzer.db.get_type_index_mut().bind_type(
                    in_file_syntax_id.into(),
                    LuaTypeCache::DocType(first_type.clone()),
                );
            }
        }
        _ => {
            report_orphan_tag(analyzer, tag);
        }
    }

    Some(())
}

pub fn analyze_param(analyzer: &mut DocAnalyzer, tag: LuaDocTagParam) -> Option<()> {
    let name = if let Some(name) = tag.get_name_token() {
        name.get_name_text().to_string()
    } else if tag.is_vararg() {
        "...".to_string()
    } else {
        return None;
    };

    let nullable = tag.is_nullable();
    let type_ref = if let Some(lua_doc_type) = tag.get_type() {
        infer_type(analyzer, lua_doc_type)
    } else {
        return None;
    };
    let default_value = tag.get_default_value().map(convert_doc_default_value);
    let type_ref = apply_nullable_doc_default(analyzer, type_ref, nullable, default_value.as_ref());

    let description = tag
        .get_description()
        .map(|des| preprocess_description(&des.get_description_text(), None));

    // bind type ref to signature and param
    if let Some(closure) = find_owner_closure(analyzer) {
        let id = LuaSignatureId::from_closure(analyzer.file_id, &closure);
        // 绑定`attribute`标记
        let attributes =
            find_attach_attribute(LuaAst::LuaDocTagParam(tag)).and_then(|tag_attribute_uses| {
                let result: Vec<LuaAttributeUse> = tag_attribute_uses
                    .into_iter()
                    .filter_map(|tag_use| infer_attribute_uses(analyzer, tag_use))
                    .flatten()
                    .collect();
                (!result.is_empty()).then_some(result)
            });

        let signature = analyzer.db.get_signature_index_mut().get_or_create(id);
        let param_info = LuaDocParamInfo {
            name: name.clone(),
            type_ref: type_ref.clone(),
            default_value,
            nullable,
            description,
            attributes,
        };

        let idx = signature.find_param_idx(&name)?;

        signature.param_docs.insert(idx, param_info);
    } else if let Some(LuaAst::LuaForRangeStat(for_range)) = analyzer.comment.get_owner() {
        // for in 支持 @param 语法
        for it_name_token in for_range.get_var_name_list() {
            let it_name = it_name_token.get_name_text();
            if it_name == name {
                let decl_id = LuaDeclId::new(analyzer.file_id, it_name_token.get_position());

                analyzer
                    .db
                    .get_type_index_mut()
                    .bind_type(decl_id.into(), LuaTypeCache::DocType(type_ref));
                break;
            }
        }
    } else {
        report_orphan_tag(analyzer, &tag);
    }

    Some(())
}

pub fn analyze_outparam(analyzer: &mut DocAnalyzer, tag: LuaDocTagOutparam) -> Option<()> {
    let path = tag.get_path()?;
    let mut path_segments = path
        .split('.')
        .filter(|segment| !segment.is_empty())
        .map(ToString::to_string);
    let Some(param_name) = path_segments.next() else {
        report_invalid_outparam(
            analyzer,
            &tag,
            "outparam path must start with a parameter name",
        );
        return None;
    };
    let field_path = path_segments.collect::<Vec<_>>();
    if field_path.is_empty() {
        report_invalid_outparam(
            analyzer,
            &tag,
            format!("outparam `{path}` must target at least one field"),
        );
        return None;
    }

    let type_ref = tag
        .get_type()
        .map(|lua_doc_type| infer_type(analyzer, lua_doc_type))?;
    let closure = find_owner_closure_or_report(analyzer, &tag)?;
    let signature_id = LuaSignatureId::from_closure(analyzer.file_id, &closure);
    let signature = analyzer
        .db
        .get_signature_index_mut()
        .get_or_create(signature_id);
    let Some(param_idx) = signature.find_param_idx(&param_name) else {
        report_invalid_outparam(
            analyzer,
            &tag,
            format!("outparam root `{param_name}` does not match any parameter"),
        );
        return None;
    };

    if let Some(existing) = signature
        .out_params
        .iter_mut()
        .find(|p| p.param_idx == param_idx && p.field_path == field_path)
    {
        existing.type_ref = type_ref;
    } else {
        signature.out_params.push(LuaOutParamInfo {
            param_idx,
            field_path,
            type_ref,
        });
    }
    Some(())
}

fn report_invalid_outparam(
    analyzer: &mut DocAnalyzer,
    tag: &LuaDocTagOutparam,
    message: impl Into<String>,
) {
    analyzer.db.get_diagnostic_index_mut().add_diagnostic(
        analyzer.file_id,
        AnalyzeError {
            kind: DiagnosticCode::AnnotationUsageError,
            message: message.into(),
            range: tag.get_range(),
        },
    );
}

fn get_return_type_kind(flag: Option<LuaDocTypeFlag>) -> ReturnTypeKind {
    if let Some(flag) = flag {
        for token in flag.get_attrib_tokens() {
            match token.get_name_text() {
                "instance" => return ReturnTypeKind::Instance,
                "definition" => return ReturnTypeKind::Definition,
                _ => {}
            }
        }
    }
    ReturnTypeKind::Reference
}

pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Option<()> {
    let description = tag
        .get_description()
        .map(|des| preprocess_description(&des.get_description_text(), None));

    let return_kind = get_return_type_kind(tag.get_type_flag());

    if let Some(closure) = find_owner_closure_or_report(analyzer, &tag) {
        let signature_id = LuaSignatureId::from_closure(analyzer.file_id, &closure);
        let returns = tag.get_info_list_with_default();
        for (doc_type, name_token, default) in returns {
            let name = name_token.map(|name| name.get_name_text().to_string());

            let default_value = default.map(convert_doc_default_value);
            let inferred_type = infer_type(analyzer, doc_type);
            let mut type_ref =
                apply_nullable_doc_default(analyzer, inferred_type, false, default_value.as_ref());
            match return_kind {
                ReturnTypeKind::Instance => {
                    let range = InFiled::new(analyzer.file_id, tag.syntax().text_range());
                    type_ref = LuaType::Instance(LuaInstanceType::new(type_ref, range).into());
                }
                ReturnTypeKind::Definition => {
                    if let LuaType::Ref(ref_id) = type_ref {
                        type_ref = LuaType::Def(ref_id);
                    }
                }
                ReturnTypeKind::Reference => {}
            }
            let return_info = LuaDocReturnInfo {
                name,
                type_ref,
                default_value,
                description: description.clone(),
                attributes: None,
                return_kind,
            };

            let signature = analyzer
                .db
                .get_signature_index_mut()
                .get_or_create(signature_id);
            signature.return_docs.push(return_info);
            signature.resolve_return = SignatureReturnStatus::DocResolve;
        }
    }
    Some(())
}

pub fn analyze_return_cast(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturnCast) -> Option<()> {
    if let Some(LuaSemanticDeclId::Signature(signature_id)) = get_owner_id(analyzer, None, false) {
        let name_token = tag.get_name_token()?;
        let name = name_token.get_name_text();

        let op_types: Vec<_> = tag.get_op_types().collect();
        let cast_op_type = op_types.first()?;

        // Bind the true condition type
        if let Some(node_type) = cast_op_type.get_type() {
            let typ = infer_type(analyzer, node_type.clone());
            let infiled_syntax_id = InFiled::new(analyzer.file_id, node_type.get_syntax_id());
            let type_owner = LuaTypeOwner::SyntaxId(infiled_syntax_id);
            bind_type(analyzer.db, type_owner, LuaTypeCache::DocType(typ));
        };

        // Bind the false condition type if present
        let fallback_cast = if op_types.len() > 1 {
            let fallback_op_type = &op_types[1];
            if let Some(node_type) = fallback_op_type.get_type() {
                let typ = infer_type(analyzer, node_type.clone());
                let infiled_syntax_id = InFiled::new(analyzer.file_id, node_type.get_syntax_id());
                let type_owner = LuaTypeOwner::SyntaxId(infiled_syntax_id);
                bind_type(analyzer.db, type_owner, LuaTypeCache::DocType(typ));
            }
            Some(fallback_op_type.to_ptr())
        } else {
            None
        };

        analyzer.db.get_flow_index_mut().add_signature_cast(
            analyzer.file_id,
            signature_id,
            name.to_string(),
            cast_op_type.to_ptr(),
            fallback_cast,
        );
    } else {
        report_orphan_tag(analyzer, &tag);
    }

    Some(())
}

pub fn analyze_overload(analyzer: &mut DocAnalyzer, tag: LuaDocTagOverload) -> Option<()> {
    if let Some(decl_id) = analyzer.current_type_id.clone() {
        let type_ref = infer_type(analyzer, tag.get_type()?);
        if let LuaType::DocFunction(func) = type_ref {
            let operator = LuaOperator::new(
                decl_id.clone().into(),
                LuaOperatorMetaMethod::Call,
                analyzer.file_id,
                tag.get_range(),
                OperatorFunction::Func(func.clone()),
            );
            analyzer.db.get_operator_index_mut().add_operator(operator);
        }
    } else if let Some(closure) = find_owner_closure_or_report(analyzer, &tag) {
        let type_ref = infer_type(analyzer, tag.get_type()?);
        if let LuaType::DocFunction(func) = type_ref {
            let id = LuaSignatureId::from_closure(analyzer.file_id, &closure);
            let signature = analyzer.db.get_signature_index_mut().get_or_create(id);
            signature.overloads.push(func);
        }
    }
    Some(())
}

pub fn analyze_module(analyzer: &mut DocAnalyzer, tag: LuaDocTagModule) -> Option<()> {
    let module_path = tag.get_module_path()?;
    let module_info = analyzer
        .db
        .get_module_index()
        .find_module_for_file(&module_path, analyzer.file_id)?;
    let module_file_id = module_info.file_id;
    let owner_id = get_owner_id_or_report(analyzer, &tag)?;
    let module_ref = LuaType::ModuleRef(module_file_id);
    match &owner_id {
        LuaSemanticDeclId::LuaDecl(decl_id) => {
            analyzer
                .db
                .get_type_index_mut()
                .bind_type((*decl_id).into(), LuaTypeCache::DocType(module_ref));
        }
        LuaSemanticDeclId::Member(member_id) => {
            analyzer
                .db
                .get_type_index_mut()
                .bind_type((*member_id).into(), LuaTypeCache::DocType(module_ref));
        }
        _ => {}
    }

    Some(())
}

pub fn analyze_as(analyzer: &mut DocAnalyzer, tag: LuaDocTagAs) -> Option<()> {
    let as_type = tag.get_type()?;
    let type_ref = infer_type(analyzer, as_type);
    let comment = analyzer.comment.clone();
    let mut left_token = comment.syntax().first_token()?.prev_token()?;
    if left_token.kind() == LuaTokenKind::TkWhitespace.into() {
        left_token = left_token.prev_token()?;
    }

    let mut ast_node = left_token.parent()?;
    loop {
        if LuaExpr::can_cast(ast_node.kind().into()) {
            break;
        } else if LuaBlock::can_cast(ast_node.kind().into()) {
            return None;
        }
        ast_node = ast_node.parent()?;
    }
    let expr = LuaExpr::cast(ast_node)?;

    let file_id = analyzer.file_id;
    let in_filed_syntax_id = InFiled::new(file_id, expr.get_syntax_id());
    bind_type(
        analyzer.db,
        in_filed_syntax_id.into(),
        LuaTypeCache::DocType(type_ref),
    );

    Some(())
}

pub fn analyze_cast(analyzer: &mut DocAnalyzer, tag: LuaDocTagCast) -> Option<()> {
    for op in tag.get_op_types() {
        if let Some(doc_type) = op.get_type() {
            let typ = infer_type(analyzer, doc_type.clone());
            let type_owner =
                LuaTypeOwner::SyntaxId(InFiled::new(analyzer.file_id, doc_type.get_syntax_id()));
            analyzer
                .db
                .get_type_index_mut()
                .bind_type(type_owner, LuaTypeCache::DocType(typ));
        }
    }
    Some(())
}

pub fn analyze_see(analyzer: &mut DocAnalyzer, tag: LuaDocTagSee) -> Option<()> {
    let owner = get_owner_id_or_report(analyzer, &tag)?;
    let content = tag.get_see_content()?;
    let text = content.get_text();
    let descriptions = tag
        .get_description()
        .map(|description| description.get_description_text());

    analyzer.db.get_property_index_mut().add_see(
        analyzer.file_id,
        owner,
        text.to_string(),
        descriptions,
    );

    Some(())
}

pub fn analyze_other(analyzer: &mut DocAnalyzer, other: LuaDocTagOther) -> Option<()> {
    let owner = get_owner_id(analyzer, None, false)?;
    let tag_name = other.get_tag_name()?;
    let description = if let Some(des) = other.get_description() {
        preprocess_description(&des.get_description_text(), None)
    } else {
        "".to_string()
    };

    if tag_name == "accessorfunc"
        && let Some(owner_ast) = analyzer.comment.get_owner()
        && let Some(name) = extract_func_name_from_ast(&owner_ast)
    {
        let name_param_index = parse_accessorfunc_param_index(&description);
        analyzer.db.get_accessor_func_index_mut().register(
            name.into(),
            AccessorFuncAnnotation {
                name_param_index,
                file_id: analyzer.file_id,
            },
        );
    }

    analyzer
        .db
        .get_property_index_mut()
        .add_other(analyzer.file_id, owner, tag_name, description);

    Some(())
}

fn extract_func_name_from_ast(ast: &LuaAst) -> Option<String> {
    match ast {
        LuaAst::LuaFuncStat(func) => {
            let var_expr = func.get_func_name()?;
            match var_expr {
                LuaVarExpr::IndexExpr(index_expr) => match index_expr.get_index_key()? {
                    LuaIndexKey::Name(name) => Some(name.get_name_text().to_string()),
                    LuaIndexKey::String(string_token) => Some(string_token.get_value()),
                    _ => None,
                },
                LuaVarExpr::NameExpr(name_expr) => name_expr.get_name_text(),
            }
        }
        LuaAst::LuaLocalFuncStat(local_func) => local_func
            .get_local_name()?
            .get_name_token()
            .map(|token| token.get_name_text().to_string()),
        _ => None,
    }
}

fn parse_accessorfunc_param_index(description: &str) -> usize {
    let trimmed = description.trim();
    if trimmed.is_empty() {
        return 0;
    }

    trimmed
        .parse::<usize>()
        .ok()
        .and_then(|n| n.checked_sub(1))
        .unwrap_or(0)
}

pub fn analyze_doc_tag_schema(analyzer: &mut DocAnalyzer, tag: LuaDocTagSchema) -> Option<()> {
    if analyzer.is_meta
        || !analyzer
            .db
            .get_module_index()
            .is_main_workspace_id(analyzer.workspace_id)
    {
        return Some(());
    }

    let path = tag.get_path_token()?;
    let path_str = path.get_path();
    if !path_str.ends_with(".json") {
        return Some(());
    }

    let url = match url::Url::parse(path_str) {
        Ok(url) => url,
        Err(e) => {
            log::error!("Invalid URL in @schema tag: {}, error: {}", path_str, e);
            return Some(());
        }
    };

    let schema_index = analyzer.db.get_json_schema_index_mut();
    if let Some(schema_file) = schema_index.get_schema_file(&url) {
        if let JsonSchemaFile::Resolved(type_id) = schema_file {
            let types = vec![LuaType::Ref(type_id.clone())];
            bind_type_to_owner(analyzer, &tag, &types, None);
        }
    } else {
        schema_index.add_schema_file(url, JsonSchemaFile::NeedResolve);
    }

    Some(())
}
