//! PHP namespace and use-alias resolution for class-like symbol names.
//!
//! Builds namespace and use-alias context so PHP declaration and graph edges reference
//! canonical class-like names, not raw source tokens.

use std::collections::{BTreeMap, BTreeSet};

use tree_sitter::Node;

use crate::graph::RelationKind;
use crate::indexer::{SymbolDefinition, SymbolKind};

use super::super::registry::{SymbolLanguage, node_field_text};
use super::declarations::PhpDeclarationRelation;

/// Namespace and `use` alias state used to canonicalize PHP class-like names during graph analysis.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct PhpNameResolutionContext {
    pub(crate) namespace: Option<String>,
    pub(crate) class_like_aliases: BTreeMap<String, String>,
}

impl PhpNameResolutionContext {
    pub(crate) fn resolve_class_like_name(
        &self,
        raw_name: &str,
        current_class_canonical_name: Option<&str>,
    ) -> Option<String> {
        let trimmed = raw_name.trim();
        if trimmed.is_empty() {
            return None;
        }

        let optional_trimmed = trimmed.strip_prefix('?').unwrap_or(trimmed);
        let normalized = optional_trimmed.trim_start_matches('\\').trim();
        if normalized.is_empty() {
            return None;
        }

        match normalized.to_ascii_lowercase().as_str() {
            "self" | "static" => {
                return current_class_canonical_name
                    .map(str::trim)
                    .filter(|name| !name.is_empty())
                    .map(ToOwned::to_owned);
            }
            "parent" => return None,
            _ if php_is_builtin_type(normalized) => return None,
            _ => {}
        }

        if optional_trimmed.starts_with('\\') {
            return Some(normalized.to_owned());
        }

        if let Some(relative) = normalized
            .strip_prefix("namespace\\")
            .or_else(|| normalized.strip_prefix("namespace/"))
        {
            return self.namespace.as_ref().map(|namespace| {
                if relative.is_empty() {
                    namespace.clone()
                } else {
                    format!("{namespace}\\{relative}")
                }
            });
        }

        let mut segments = normalized.splitn(2, '\\');
        let first_segment = segments.next().unwrap_or_default().trim();
        let remainder = segments.next().map(str::trim).unwrap_or_default();
        if first_segment.is_empty() {
            return None;
        }

        if let Some(alias_target) = self
            .class_like_aliases
            .get(&first_segment.to_ascii_lowercase())
            .filter(|target| !target.trim().is_empty())
        {
            return Some(if remainder.is_empty() {
                alias_target.clone()
            } else {
                format!("{alias_target}\\{remainder}")
            });
        }

        if let Some(namespace) = &self.namespace {
            return Some(format!("{namespace}\\{normalized}"));
        }

        Some(normalized.to_owned())
    }
}

pub(crate) fn php_name_resolution_context_from_root(
    source: &str,
    root: Node<'_>,
) -> PhpNameResolutionContext {
    let mut context = PhpNameResolutionContext::default();
    let mut cursor = root.walk();
    for child in root.children(&mut cursor).filter(|child| child.is_named()) {
        match child.kind() {
            "namespace_definition" => {
                context.namespace = node_field_text(child, source, "name");
                if let Some(body) = child.child_by_field_name("body") {
                    let mut body_cursor = body.walk();
                    for body_child in body
                        .children(&mut body_cursor)
                        .filter(|node| node.is_named())
                    {
                        if body_child.kind() == "namespace_use_declaration" {
                            collect_php_namespace_use_declaration(source, body_child, &mut context);
                        }
                    }
                }
            }
            "namespace_use_declaration" => {
                collect_php_namespace_use_declaration(source, child, &mut context);
            }
            _ => {}
        }
    }
    context
}

pub(crate) fn php_name_resolution_context_for_namespace_node(
    source: &str,
    namespace_node: Node<'_>,
) -> PhpNameResolutionContext {
    let mut context = PhpNameResolutionContext {
        namespace: node_field_text(namespace_node, source, "name"),
        ..PhpNameResolutionContext::default()
    };
    if let Some(body) = namespace_node.child_by_field_name("body") {
        let mut body_cursor = body.walk();
        for body_child in body
            .children(&mut body_cursor)
            .filter(|node| node.is_named())
        {
            if body_child.kind() == "namespace_use_declaration" {
                collect_php_namespace_use_declaration(source, body_child, &mut context);
            }
        }
    }
    context
}

fn php_is_builtin_type(raw_name: &str) -> bool {
    matches!(
        raw_name.trim().to_ascii_lowercase().as_str(),
        "array"
            | "bool"
            | "callable"
            | "false"
            | "float"
            | "int"
            | "iterable"
            | "mixed"
            | "never"
            | "null"
            | "object"
            | "resource"
            | "string"
            | "true"
            | "void"
    )
}

#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn php_class_like_name_candidates(
    context: Option<&PhpNameResolutionContext>,
    raw_target_name: &str,
    current_class_canonical_name: Option<&str>,
) -> Vec<String> {
    let mut candidates = Vec::new();
    if let Some(canonical) = context.and_then(|context| {
        context.resolve_class_like_name(raw_target_name, current_class_canonical_name)
    }) {
        candidates.push(canonical);
    }
    for candidate in php_reference_name_candidates(raw_target_name) {
        if !candidates.iter().any(|existing| existing == &candidate) {
            candidates.push(candidate);
        }
    }
    candidates
}

fn collect_php_namespace_use_declaration(
    source: &str,
    declaration: Node<'_>,
    context: &mut PhpNameResolutionContext,
) {
    let declaration_type = declaration
        .child_by_field_name("type")
        .map(|node| node.kind());
    let body_id = declaration
        .child_by_field_name("body")
        .map(|node| node.id());
    let grouped_prefix = if body_id.is_some() {
        let mut cursor = declaration.walk();
        declaration
            .children(&mut cursor)
            .filter(|child| child.is_named())
            .find(|child| child.kind() == "namespace_name")
            .and_then(|child| child.utf8_text(source.as_bytes()).ok())
            .map(str::trim)
            .filter(|text| !text.is_empty())
            .map(ToOwned::to_owned)
    } else {
        None
    };

    let mut cursor = declaration.walk();
    for child in declaration
        .children(&mut cursor)
        .filter(|child| child.is_named())
    {
        if Some(child.id()) == body_id {
            let mut group_cursor = child.walk();
            for clause in child
                .children(&mut group_cursor)
                .filter(|node| node.is_named())
            {
                if clause.kind() == "namespace_use_clause" {
                    collect_php_namespace_use_clause(
                        source,
                        clause,
                        grouped_prefix.as_deref(),
                        declaration_type,
                        context,
                    );
                }
            }
            continue;
        }
        if child.kind() == "namespace_use_clause" {
            collect_php_namespace_use_clause(source, child, None, declaration_type, context);
        }
    }
}

fn collect_php_namespace_use_clause(
    source: &str,
    clause: Node<'_>,
    grouped_prefix: Option<&str>,
    declaration_type: Option<&str>,
    context: &mut PhpNameResolutionContext,
) {
    let clause_type = clause
        .child_by_field_name("type")
        .map(|node| node.kind())
        .or(declaration_type);
    if matches!(clause_type, Some("function" | "const")) {
        return;
    }

    let alias_node = clause.child_by_field_name("alias");
    let alias_node_id = alias_node.as_ref().map(Node::id);
    let alias = alias_node
        .and_then(|node| node.utf8_text(source.as_bytes()).ok())
        .map(str::trim)
        .filter(|text| !text.is_empty())
        .map(ToOwned::to_owned);

    let mut referenced_name = None;
    let mut cursor = clause.walk();
    for child in clause
        .children(&mut cursor)
        .filter(|child| child.is_named())
    {
        if Some(child.id()) == alias_node_id {
            continue;
        }
        referenced_name = child
            .utf8_text(source.as_bytes())
            .ok()
            .map(str::trim)
            .filter(|text| !text.is_empty())
            .map(ToOwned::to_owned);
        if referenced_name.is_some() {
            break;
        }
    }

    let Some(referenced_name) = referenced_name else {
        return;
    };
    let canonical = grouped_prefix
        .filter(|prefix| !prefix.is_empty())
        .map(|prefix| format!("{prefix}\\{referenced_name}"))
        .unwrap_or_else(|| referenced_name.clone());
    let alias = alias.unwrap_or_else(|| {
        referenced_name
            .rsplit('\\')
            .next()
            .unwrap_or(&referenced_name)
            .to_owned()
    });
    if alias.trim().is_empty() || canonical.trim().is_empty() {
        return;
    }

    context
        .class_like_aliases
        .insert(alias.to_ascii_lowercase(), canonical);
}

pub(crate) struct PhpSymbolLookup<'a> {
    pub(crate) symbols: &'a [SymbolDefinition],
    pub(crate) symbols_by_relative_path: &'a BTreeMap<String, Vec<usize>>,
    pub(crate) symbol_indices_by_name: &'a BTreeMap<String, Vec<usize>>,
    pub(crate) symbol_indices_by_lower_name: &'a BTreeMap<String, Vec<usize>>,
}

pub(crate) fn resolve_php_declaration_relation_indices(
    lookup: &PhpSymbolLookup<'_>,
    relative_path: &str,
    relation: &PhpDeclarationRelation,
) -> Option<(usize, usize)> {
    let source_symbol_index = resolve_php_relation_source_symbol(lookup, relative_path, relation)?;
    let target_symbol_index = resolve_php_relation_target_symbol(lookup, relation)?;
    let source_symbol = &lookup.symbols[source_symbol_index];
    let target_symbol = &lookup.symbols[target_symbol_index];
    if source_symbol.stable_id == target_symbol.stable_id {
        return None;
    }
    Some((source_symbol_index, target_symbol_index))
}

pub(crate) fn php_relation_targets_symbol_name(
    relation: &PhpDeclarationRelation,
    target_symbol: &SymbolDefinition,
) -> bool {
    let target_name = target_symbol.name.trim();
    if target_name.is_empty() {
        return false;
    }
    (match target_symbol.kind {
        SymbolKind::Interface => matches!(
            relation.relation,
            RelationKind::Implements | RelationKind::Extends
        ),
        SymbolKind::Class => relation.relation == RelationKind::Extends,
        _ => false,
    }) && php_reference_name_candidates(&relation.target_name)
        .iter()
        .any(|candidate| candidate.eq_ignore_ascii_case(target_name))
}

fn resolve_php_relation_source_symbol(
    lookup: &PhpSymbolLookup<'_>,
    relative_path: &str,
    relation: &PhpDeclarationRelation,
) -> Option<usize> {
    let matches = lookup
        .symbols_by_relative_path
        .get(relative_path)?
        .iter()
        .copied()
        .filter(|index| {
            let symbol = &lookup.symbols[*index];
            symbol.language == SymbolLanguage::Php
                && symbol.kind == relation.source_kind
                && symbol.line == relation.source_line
                && symbol.name == relation.source_name
        })
        .collect::<Vec<_>>();
    if matches.len() == 1 {
        matches.first().copied()
    } else {
        None
    }
}

fn resolve_php_relation_target_symbol(
    lookup: &PhpSymbolLookup<'_>,
    relation: &PhpDeclarationRelation,
) -> Option<usize> {
    let allowed_kinds = php_allowed_target_kinds(relation.source_kind, relation.relation);
    if allowed_kinds.is_empty() {
        return None;
    }

    let candidates = php_reference_name_candidates(&relation.target_name);
    if candidates.is_empty() {
        return None;
    }

    let mut exact_matches = BTreeSet::new();
    for candidate in &candidates {
        if let Some(indices) = lookup.symbol_indices_by_name.get(candidate) {
            for index in indices {
                let symbol = &lookup.symbols[*index];
                if symbol.language == SymbolLanguage::Php && allowed_kinds.contains(&symbol.kind) {
                    exact_matches.insert(*index);
                }
            }
        }
    }
    if exact_matches.len() == 1 {
        return exact_matches.iter().next().copied();
    }
    if !exact_matches.is_empty() {
        return None;
    }

    let mut case_insensitive_matches = BTreeSet::new();
    for candidate in &candidates {
        let lower = candidate.to_ascii_lowercase();
        if let Some(indices) = lookup.symbol_indices_by_lower_name.get(&lower) {
            for index in indices {
                let symbol = &lookup.symbols[*index];
                if symbol.language == SymbolLanguage::Php && allowed_kinds.contains(&symbol.kind) {
                    case_insensitive_matches.insert(*index);
                }
            }
        }
    }
    if case_insensitive_matches.len() == 1 {
        case_insensitive_matches.iter().next().copied()
    } else {
        None
    }
}

fn php_allowed_target_kinds(
    source_kind: SymbolKind,
    relation: RelationKind,
) -> &'static [SymbolKind] {
    const CLASS_ONLY: &[SymbolKind] = &[SymbolKind::Class];
    const INTERFACE_ONLY: &[SymbolKind] = &[SymbolKind::Interface];
    const NONE: &[SymbolKind] = &[];

    match relation {
        RelationKind::Implements => INTERFACE_ONLY,
        RelationKind::Extends => match source_kind {
            SymbolKind::Class => CLASS_ONLY,
            SymbolKind::Interface => INTERFACE_ONLY,
            _ => NONE,
        },
        _ => NONE,
    }
}

pub(super) fn php_reference_name_candidates(raw_target_name: &str) -> Vec<String> {
    let mut candidates = Vec::new();
    let trimmed = raw_target_name.trim().trim_start_matches('\\');
    if trimmed.is_empty() {
        return candidates;
    }

    for candidate in [
        Some(trimmed),
        trimmed.rsplit('\\').next(),
        trimmed.rsplit(':').next(),
    ]
    .into_iter()
    .flatten()
    .map(str::trim)
    .filter(|candidate| !candidate.is_empty())
    {
        if matches!(
            candidate.to_ascii_lowercase().as_str(),
            "self" | "static" | "parent"
        ) {
            continue;
        }
        if !candidates.iter().any(|existing| existing == candidate) {
            candidates.push(candidate.to_owned());
        }
    }

    candidates
}
