//! Blade view and component symbol extraction plus Flux relation evidence.

use std::collections::BTreeMap;
use std::path::Path;
use std::sync::OnceLock;

use regex::Regex;
use serde::{Deserialize, Serialize};

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

use super::registry::SymbolLanguage;

pub(crate) fn collect_symbols_from_source(
    path: &Path,
    source: &str,
    symbols: &mut Vec<SymbolDefinition>,
) {
    let whole_file_span = source_span_from_offsets(source, 0, source.len());
    let file_anchor = usize::from(!source.is_empty());
    let module_name = blade_view_name_for_path(path).unwrap_or_else(|| {
        path.file_name()
            .and_then(|name| name.to_str())
            .map(strip_blade_suffix)
            .filter(|name| !name.is_empty())
            .unwrap_or_else(|| "blade".to_owned())
    });
    push_symbol_definition(
        symbols,
        SymbolLanguage::Blade,
        SymbolKind::Module,
        path,
        &module_name,
        whole_file_span.clone(),
    );

    if let Some(component_name) = blade_component_name_for_path(path) {
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            SymbolKind::Component,
            path,
            &component_name,
            source_span_from_offsets(source, file_anchor, file_anchor),
        );
    }

    for capture in blade_section_regex().captures_iter(source) {
        let Some(name) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            SymbolKind::Section,
            path,
            name.as_str().trim(),
            source_span_from_offsets(source, name.start(), name.end()),
        );
    }

    for capture in blade_livewire_directive_regex().captures_iter(source) {
        let Some(name) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        let normalized = format!("livewire:{}", name.as_str().trim());
        let Some(span) = capture.get(0) else {
            continue;
        };
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            SymbolKind::Component,
            path,
            &normalized,
            source_span_from_offsets(source, span.start(), span.end()),
        );
    }

    for capture in blade_tag_regex().captures_iter(source) {
        let Some(tag_name) = capture.get(1) else {
            continue;
        };
        let Some((kind, normalized_name)) = classify_tag_name(tag_name.as_str()) else {
            continue;
        };
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            kind,
            path,
            &normalized_name,
            source_span_from_offsets(source, tag_name.start(), tag_name.end()),
        );
    }

    for capture in blade_named_slot_tag_regex().captures_iter(source) {
        let Some(name) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            SymbolKind::Slot,
            path,
            name.as_str().trim(),
            source_span_from_offsets(source, name.start(), name.end()),
        );
    }

    for capture in blade_slot_directive_regex().captures_iter(source) {
        let Some(name) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        push_symbol_definition(
            symbols,
            SymbolLanguage::Blade,
            SymbolKind::Slot,
            path,
            name.as_str().trim(),
            source_span_from_offsets(source, name.start(), name.end()),
        );
    }

    collect_property_symbols(source, path, symbols, "@props");
    collect_property_symbols(source, path, symbols, "@aware");
}

/// Blade template relation categories used when resolving view and component graph edges.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum BladeRelationKind {
    Extends,
    Include,
    Component,
    Yield,
    DynamicComponent,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct BladeRelationEvidence {
    pub(crate) owner_symbol_id: Option<String>,
    pub(crate) kind: BladeRelationKind,
    pub(crate) target_name: String,
    pub(crate) target_symbol_kind: SymbolKind,
    pub(crate) line: usize,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct FluxComponentHint {
    pub(crate) props: Vec<String>,
    pub(crate) slots: Vec<String>,
    pub(crate) variant_values: Vec<String>,
    pub(crate) size_values: Vec<String>,
    pub(crate) local_overlay: bool,
}

/// Blade template evidence bundle covering relations, Livewire usage, and Flux component hints.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct BladeSourceEvidence {
    pub(crate) relations: Vec<BladeRelationEvidence>,
    pub(crate) livewire_components: Vec<String>,
    pub(crate) wire_directives: Vec<String>,
    pub(crate) flux_components: Vec<String>,
    pub(crate) flux_hints: BTreeMap<String, FluxComponentHint>,
}

pub(crate) const FLUX_REGISTRY_VERSION: &str = "2026-03-08-mvp";

/// Extracts Blade relation and component evidence from template source and module symbols.
pub(crate) fn extract_source_evidence_from_source(
    source: &str,
    file_symbols: &[SymbolDefinition],
) -> BladeSourceEvidence {
    let owner_symbol_id = file_symbols
        .iter()
        .find(|symbol| {
            symbol.language == SymbolLanguage::Blade && symbol.kind == SymbolKind::Module
        })
        .map(|symbol| symbol.stable_id.clone());
    let mut evidence = BladeSourceEvidence::default();

    for capture in blade_view_relation_regex().captures_iter(source) {
        let Some(directive) = capture.name("directive").map(|value| value.as_str()) else {
            continue;
        };
        let Some(target) = capture.get(2).or_else(|| capture.get(3)) else {
            continue;
        };
        let target_name = target.as_str().trim();
        if target_name.is_empty() {
            continue;
        }
        let (kind, target_symbol_kind) = match directive {
            "extends" => (BladeRelationKind::Extends, SymbolKind::Module),
            "component" => (BladeRelationKind::Component, SymbolKind::Module),
            "yield" => (BladeRelationKind::Yield, SymbolKind::Section),
            _ => (BladeRelationKind::Include, SymbolKind::Module),
        };
        evidence.relations.push(BladeRelationEvidence {
            owner_symbol_id: owner_symbol_id.clone(),
            kind,
            target_name: target_name.to_owned(),
            target_symbol_kind,
            line: line_column_for_offset(source, target.start()).0,
        });
    }

    for capture in blade_dynamic_component_regex().captures_iter(source) {
        let Some(target) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        let target_name = normalize_dynamic_component_target(target.as_str());
        if target_name.is_empty() {
            continue;
        }
        evidence.relations.push(BladeRelationEvidence {
            owner_symbol_id: owner_symbol_id.clone(),
            kind: BladeRelationKind::DynamicComponent,
            target_name,
            target_symbol_kind: SymbolKind::Component,
            line: line_column_for_offset(source, target.start()).0,
        });
    }

    for capture in blade_tag_regex().captures_iter(source) {
        let Some(tag_name) = capture.get(1) else {
            continue;
        };
        let normalized = tag_name.as_str().trim();
        if let Some(component_name) = normalized.strip_prefix("livewire:") {
            insert_sorted_unique_owned(
                &mut evidence.livewire_components,
                component_name.to_owned(),
            );
            continue;
        }
        if normalized.starts_with("flux:") {
            insert_sorted_unique_owned(&mut evidence.flux_components, normalized.to_owned());
            if let Some(hint) = flux_registry_hint(normalized) {
                evidence
                    .flux_hints
                    .entry(normalized.to_owned())
                    .or_insert(hint);
            }
            continue;
        }
        if let Some((SymbolKind::Component, component_name)) = classify_tag_name(normalized) {
            evidence.relations.push(BladeRelationEvidence {
                owner_symbol_id: owner_symbol_id.clone(),
                kind: BladeRelationKind::Component,
                target_name: component_name,
                target_symbol_kind: SymbolKind::Component,
                line: line_column_for_offset(source, tag_name.start()).0,
            });
        }
    }

    for capture in blade_livewire_directive_regex().captures_iter(source) {
        let Some(name) = capture.get(1).or_else(|| capture.get(2)) else {
            continue;
        };
        insert_sorted_unique_owned(
            &mut evidence.livewire_components,
            name.as_str().trim().to_owned(),
        );
    }

    for capture in blade_wire_directive_regex().captures_iter(source) {
        let Some(name) = capture.get(1) else {
            continue;
        };
        insert_sorted_unique_owned(
            &mut evidence.wire_directives,
            name.as_str().trim().to_owned(),
        );
    }

    normalize_source_evidence(&mut evidence);
    evidence
}

pub(crate) fn mark_local_flux_overlays(
    evidence: &mut BladeSourceEvidence,
    symbols: &[SymbolDefinition],
    symbol_indices_by_name: &BTreeMap<String, Vec<usize>>,
) {
    for component_name in &evidence.flux_components {
        let local_component_name = component_name.replacen("flux:", "flux.", 1);
        let local_overlay = symbol_indices_by_name
            .get(&local_component_name)
            .into_iter()
            .flatten()
            .any(|index| {
                let symbol = &symbols[*index];
                symbol.language == SymbolLanguage::Blade && symbol.kind == SymbolKind::Component
            });
        if !local_overlay {
            continue;
        }
        evidence
            .flux_hints
            .entry(component_name.clone())
            .or_default()
            .local_overlay = true;
    }
}

/// Resolves Blade relation evidence into graph edges between indexed symbols.
pub(crate) fn resolve_relation_evidence_edges(
    symbols: &[SymbolDefinition],
    symbol_index_by_stable_id: &BTreeMap<String, usize>,
    symbol_indices_by_name: &BTreeMap<String, Vec<usize>>,
    symbol_indices_by_lower_name: &BTreeMap<String, Vec<usize>>,
    evidence: &BladeSourceEvidence,
) -> Vec<(usize, usize, RelationKind)> {
    let mut edges = Vec::new();
    for relation in &evidence.relations {
        let Some(source_symbol_id) = relation.owner_symbol_id.as_ref() else {
            continue;
        };
        let Some(source_symbol_index) = symbol_index_by_stable_id.get(source_symbol_id).copied()
        else {
            continue;
        };
        let Some(target_symbol_index) = resolve_relation_target_symbol_index(
            symbols,
            symbol_indices_by_name,
            symbol_indices_by_lower_name,
            relation,
        ) else {
            continue;
        };
        if source_symbol_index == target_symbol_index {
            continue;
        }
        edges.push((
            source_symbol_index,
            target_symbol_index,
            RelationKind::RefersTo,
        ));
    }
    edges.sort_by(|left, right| {
        left.0
            .cmp(&right.0)
            .then(left.1.cmp(&right.1))
            .then(left.2.cmp(&right.2))
    });
    edges.dedup();
    edges
}

pub(crate) fn is_blade_path(path: &Path) -> bool {
    path.file_name()
        .and_then(|file_name| file_name.to_str())
        .is_some_and(|file_name| file_name.ends_with(".blade.php"))
}

pub(crate) fn blade_view_name_for_path(path: &Path) -> Option<String> {
    let normalized = normalize_path_components(path);
    let blade_index = normalized
        .iter()
        .position(|component| component == "views")?;
    let tail = normalized.get(blade_index + 1..)?;
    let segments = tail
        .iter()
        .map(|segment| strip_blade_suffix(segment))
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if segments.is_empty() {
        return None;
    }
    Some(segments.join("."))
}

pub(crate) fn blade_component_name_for_path(path: &Path) -> Option<String> {
    let normalized = normalize_path_components(path);
    let components_index = normalized
        .iter()
        .position(|component| component == "components")?;
    let tail = normalized.get(components_index + 1..)?;
    let segments = tail
        .iter()
        .map(|segment| strip_blade_suffix(segment))
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if segments.is_empty() {
        return None;
    }
    Some(segments.join("."))
}

pub(crate) fn classify_tag_name(raw_tag_name: &str) -> Option<(SymbolKind, String)> {
    let normalized = raw_tag_name.trim();
    if normalized.is_empty() {
        return None;
    }
    if let Some(slot_name) = normalized
        .strip_prefix("x-slot:")
        .or_else(|| normalized.strip_prefix("x-slot."))
    {
        let slot_name = slot_name.trim();
        return (!slot_name.is_empty()).then(|| (SymbolKind::Slot, slot_name.to_owned()));
    }
    if let Some(component_name) = normalized.strip_prefix("x-") {
        if component_name == "dynamic-component" {
            return None;
        }
        let component_name = component_name.trim();
        return (!component_name.is_empty())
            .then(|| (SymbolKind::Component, component_name.to_owned()));
    }
    if normalized.starts_with("livewire:") || normalized.starts_with("flux:") {
        return Some((SymbolKind::Component, normalized.to_owned()));
    }
    None
}

fn collect_property_symbols(
    source: &str,
    path: &Path,
    symbols: &mut Vec<SymbolDefinition>,
    directive: &str,
) {
    let bytes = source.as_bytes();
    let mut cursor = 0usize;
    while let Some(relative) = source[cursor..].find(directive) {
        let start = cursor + relative;
        let mut offset = start + directive.len();
        while offset < bytes.len() && bytes[offset].is_ascii_whitespace() {
            offset += 1;
        }
        if offset >= bytes.len() || bytes[offset] != b'(' {
            cursor = start + directive.len();
            continue;
        }
        let Some((parameter_start, parameter_end)) = directive_parameter_bounds(source, offset)
        else {
            cursor = start + directive.len();
            continue;
        };
        let parameter_source = &source[parameter_start..parameter_end];
        for capture in blade_property_key_regex().captures_iter(parameter_source) {
            let Some(name_match) = capture.get(1).or_else(|| capture.get(2)) else {
                continue;
            };
            let normalized_name = format!("${}", name_match.as_str().trim());
            push_symbol_definition(
                symbols,
                SymbolLanguage::Blade,
                SymbolKind::Property,
                path,
                &normalized_name,
                source_span_from_offsets(
                    source,
                    parameter_start + name_match.start(),
                    parameter_start + name_match.end(),
                ),
            );
        }
        cursor = parameter_end.saturating_add(1);
    }
}

fn resolve_relation_target_symbol_index(
    symbols: &[SymbolDefinition],
    symbol_indices_by_name: &BTreeMap<String, Vec<usize>>,
    symbol_indices_by_lower_name: &BTreeMap<String, Vec<usize>>,
    relation: &BladeRelationEvidence,
) -> Option<usize> {
    if let Some(indices) = symbol_indices_by_name.get(&relation.target_name) {
        let matches = indices
            .iter()
            .copied()
            .filter(|index| symbols[*index].kind == relation.target_symbol_kind)
            .collect::<Vec<_>>();
        if matches.len() == 1 {
            return matches.first().copied();
        }
        if !matches.is_empty() {
            return None;
        }
    }

    let matches = symbol_indices_by_lower_name
        .get(&relation.target_name.to_ascii_lowercase())
        .into_iter()
        .flatten()
        .copied()
        .filter(|index| symbols[*index].kind == relation.target_symbol_kind)
        .collect::<Vec<_>>();
    if matches.len() == 1 {
        matches.first().copied()
    } else {
        None
    }
}

fn normalize_source_evidence(evidence: &mut BladeSourceEvidence) {
    evidence.relations.sort();
    evidence.relations.dedup();
    evidence.livewire_components.sort();
    evidence.livewire_components.dedup();
    evidence.wire_directives.sort();
    evidence.wire_directives.dedup();
    evidence.flux_components.sort();
    evidence.flux_components.dedup();
    for hint in evidence.flux_hints.values_mut() {
        hint.props.sort();
        hint.props.dedup();
        hint.slots.sort();
        hint.slots.dedup();
        hint.variant_values.sort();
        hint.variant_values.dedup();
        hint.size_values.sort();
        hint.size_values.dedup();
    }
}

fn insert_sorted_unique_owned(values: &mut Vec<String>, value: String) {
    match values.binary_search(&value) {
        Ok(_) => {}
        Err(index) => values.insert(index, value),
    }
}

fn flux_registry_hint(component_name: &str) -> Option<FluxComponentHint> {
    match component_name {
        "flux:button" => Some(FluxComponentHint {
            props: vec!["icon".to_owned(), "size".to_owned(), "variant".to_owned()],
            slots: vec!["default".to_owned()],
            variant_values: vec![
                "danger".to_owned(),
                "ghost".to_owned(),
                "primary".to_owned(),
                "subtle".to_owned(),
            ],
            size_values: vec!["sm".to_owned(), "base".to_owned(), "lg".to_owned()],
            local_overlay: false,
        }),
        "flux:input" => Some(FluxComponentHint {
            props: vec!["size".to_owned(), "type".to_owned()],
            slots: Vec::new(),
            variant_values: Vec::new(),
            size_values: vec!["sm".to_owned(), "base".to_owned(), "lg".to_owned()],
            local_overlay: false,
        }),
        "flux:modal" => Some(FluxComponentHint {
            props: vec!["name".to_owned(), "variant".to_owned()],
            slots: vec![
                "default".to_owned(),
                "footer".to_owned(),
                "heading".to_owned(),
            ],
            variant_values: vec!["danger".to_owned(), "default".to_owned()],
            size_values: Vec::new(),
            local_overlay: false,
        }),
        "flux:dropdown" => Some(FluxComponentHint {
            props: vec!["align".to_owned(), "position".to_owned()],
            slots: vec!["default".to_owned(), "trigger".to_owned()],
            variant_values: Vec::new(),
            size_values: Vec::new(),
            local_overlay: false,
        }),
        "flux:select" => Some(FluxComponentHint {
            props: vec!["multiple".to_owned(), "size".to_owned()],
            slots: vec!["default".to_owned()],
            variant_values: Vec::new(),
            size_values: vec!["sm".to_owned(), "base".to_owned(), "lg".to_owned()],
            local_overlay: false,
        }),
        _ => None,
    }
}

fn normalize_path_components(path: &Path) -> Vec<String> {
    path.components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>()
}

fn strip_blade_suffix(segment: &str) -> String {
    segment
        .strip_suffix(".blade.php")
        .or_else(|| segment.strip_suffix(".php"))
        .unwrap_or(segment)
        .to_owned()
}

fn directive_parameter_bounds(source: &str, open_paren_index: usize) -> Option<(usize, usize)> {
    let bytes = source.as_bytes();
    if bytes.get(open_paren_index) != Some(&b'(') {
        return None;
    }
    let mut depth = 0usize;
    let mut in_single_quote = false;
    let mut in_double_quote = false;
    let mut escaped = false;
    for (offset, byte) in bytes[open_paren_index..].iter().copied().enumerate() {
        let index = open_paren_index + offset;
        if escaped {
            escaped = false;
            continue;
        }
        match byte {
            b'\\' if in_single_quote || in_double_quote => escaped = true,
            b'\'' if !in_double_quote => in_single_quote = !in_single_quote,
            b'"' if !in_single_quote => in_double_quote = !in_double_quote,
            b'(' if !in_single_quote && !in_double_quote => depth += 1,
            b')' if !in_single_quote && !in_double_quote => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    return Some((open_paren_index + 1, index));
                }
            }
            _ => {}
        }
    }
    None
}

fn blade_section_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"@(?:section|yield)\s*\(\s*(?:"([^"]+)"|'([^']+)')"#)
            .expect("blade section regex must compile")
    })
}

fn blade_livewire_directive_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"@livewire\s*\(\s*(?:"([^"]+)"|'([^']+)')"#)
            .expect("blade livewire directive regex must compile")
    })
}

fn blade_view_relation_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(
            r#"@(?P<directive>extends|component|include(?:If|When|Unless|First)?|yield)\s*\(\s*(?:"([^"]+)"|'([^']+)')"#,
        )
        .expect("blade view relation regex must compile")
    })
}

fn blade_dynamic_component_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(
            r#"<\s*x-dynamic-component\b[^>]*\b(?::component|component)\s*=\s*(?:"([^"]+)"|'([^']+)')"#,
        )
        .expect("blade dynamic component regex must compile")
    })
}

fn blade_tag_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(
            r#"<\s*(x-[A-Za-z0-9_.:-]+|livewire:[A-Za-z0-9_.:-]+|flux:[A-Za-z0-9_.:-]+)(?:[\s>/])"#,
        )
        .expect("blade tag regex must compile")
    })
}

fn blade_named_slot_tag_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"<\s*x-slot\b[^>]*\bname\s*=\s*(?:"([^"]+)"|'([^']+)')"#)
            .expect("blade named slot regex must compile")
    })
}

fn blade_slot_directive_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"@slot\s*\(\s*(?:"([^"]+)"|'([^']+)')"#)
            .expect("blade slot directive regex must compile")
    })
}

fn blade_wire_directive_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"\b(wire:[A-Za-z0-9_.-]+)\s*="#)
            .expect("blade wire directive regex must compile")
    })
}

fn normalize_dynamic_component_target(raw_target: &str) -> String {
    raw_target
        .trim()
        .trim_matches(['"', '\''])
        .trim()
        .to_owned()
}

fn blade_property_key_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(
            r#"(?x)
            (?:^|[\[,])\s*"([^"]+)"(?:\s*=>)?
            |
            (?:^|[\[,])\s*'([^']+)'(?:\s*=>)?
        "#,
        )
        .expect("blade property key regex must compile")
    })
}
