"""Cross-reference discovery engine — Phase 3 conformance mapping."""
from __future__ import annotations

import re
from collections import defaultdict
from difflib import SequenceMatcher
from typing import Any, Dict, List, Set, Tuple

from .constants import MappingType, XREF_AUTO_PROMOTE_THRESHOLD
from .contracts import (
    CrossReferenceMapping,
    CrossReferenceModel,
    Dependency,
    GrainColumn,
    LogicArtifact,
    Measure,
)


def _normalize_col(name: str) -> str:
    """Normalize a column name for fuzzy comparison."""
    s = name.lower().strip()
    s = re.sub(r"[^a-z0-9]", "", s)
    return s


def _similarity(a: str, b: str) -> float:
    """String similarity ratio between two normalized names."""
    na, nb = _normalize_col(a), _normalize_col(b)
    if not na or not nb:
        return 0.0
    if na == nb:
        return 1.0
    return SequenceMatcher(None, na, nb).ratio()


class CrossReferenceDiscovery:
    """Discover cross-system entity mappings from logic artifacts.

    Compares column names, grain patterns, and measure definitions
    across artifacts from different source systems to propose
    CrossReferenceMapping entries.
    """

    def __init__(self, similarity_threshold: float = 0.75):
        self.similarity_threshold = similarity_threshold

    def discover(
        self,
        artifacts: List[LogicArtifact],
        system_labels: Dict[str, str] | None = None,
    ) -> CrossReferenceModel:
        """Run cross-reference discovery across all artifacts.

        system_labels: optional map of artifact_id -> system name.
        If not provided, system names are inferred from source_name/source_path.
        """
        if not system_labels:
            system_labels = {}
            for art in artifacts:
                label = art.source_name or art.source_path or art.artifact_id
                system_labels[art.artifact_id] = label

        mappings: List[CrossReferenceMapping] = []

        # 1. Column-name based matching
        mappings.extend(self._match_columns(artifacts, system_labels))

        # 2. Grain-based matching
        mappings.extend(self._match_grains(artifacts, system_labels))

        # 3. Measure-expression matching
        mappings.extend(self._match_measures(artifacts, system_labels))

        # Deduplicate
        mappings = self._deduplicate(mappings)

        # Collect metadata
        domains = list(dict.fromkeys(m.domain for m in mappings if m.domain))
        systems = list(dict.fromkeys(
            s for m in mappings for s in [m.source_system, m.target_system] if s
        ))

        return CrossReferenceModel(
            mappings=mappings,
            domains=domains,
            source_systems=systems,
        )

    def _match_columns(
        self,
        artifacts: List[LogicArtifact],
        labels: Dict[str, str],
    ) -> List[CrossReferenceMapping]:
        """Match columns across artifacts by name similarity."""
        mappings: List[CrossReferenceMapping] = []

        # Collect all columns per artifact
        art_cols: List[Tuple[str, str, Set[str]]] = []
        for art in artifacts:
            cols: Set[str] = set()
            for m in art.objects.measures:
                cols.update(m.source_columns)
            for g in art.objects.grain_columns:
                if g.column:
                    cols.add(g.column)
            for f in art.objects.filters:
                col = f.column.split(".")[-1] if f.column else ""
                if col:
                    cols.add(col)
            if cols:
                art_cols.append((art.artifact_id, labels.get(art.artifact_id, ""), cols))

        # Pairwise comparison
        for i in range(len(art_cols)):
            for j in range(i + 1, len(art_cols)):
                aid_a, sys_a, cols_a = art_cols[i]
                aid_b, sys_b, cols_b = art_cols[j]

                for ca in cols_a:
                    for cb in cols_b:
                        sim = _similarity(ca, cb)
                        if sim >= self.similarity_threshold:
                            domain = self._infer_domain(ca, cb)
                            mappings.append(CrossReferenceMapping(
                                domain=domain,
                                source_system=sys_a,
                                source_key=ca,
                                target_system=sys_b,
                                target_key=cb,
                                mapping_type=MappingType.DIRECT,
                                confidence=round(sim, 2),
                                rationale=f"Column name similarity: {sim:.0%}",
                            ))

        return mappings

    def _match_grains(
        self,
        artifacts: List[LogicArtifact],
        labels: Dict[str, str],
    ) -> List[CrossReferenceMapping]:
        """Match grain columns across artifacts."""
        mappings: List[CrossReferenceMapping] = []

        grains: List[Tuple[str, str, List[GrainColumn]]] = []
        for art in artifacts:
            if art.objects.grain_columns:
                grains.append((
                    art.artifact_id,
                    labels.get(art.artifact_id, ""),
                    art.objects.grain_columns,
                ))

        for i in range(len(grains)):
            for j in range(i + 1, len(grains)):
                aid_a, sys_a, ga = grains[i]
                aid_b, sys_b, gb = grains[j]

                for gca in ga:
                    for gcb in gb:
                        sim = _similarity(gca.column, gcb.column)
                        if sim >= self.similarity_threshold:
                            # Grain matches get a confidence boost
                            conf = min(sim + 0.05, 1.0)
                            mappings.append(CrossReferenceMapping(
                                domain=self._infer_domain(gca.column, gcb.column),
                                source_system=sys_a,
                                source_key=gca.column,
                                source_grain=gca.column,
                                target_system=sys_b,
                                target_key=gcb.column,
                                target_grain=gcb.column,
                                mapping_type=MappingType.DIRECT,
                                confidence=round(conf, 2),
                                rationale=f"Grain column match: {sim:.0%}",
                            ))

        return mappings

    def _match_measures(
        self,
        artifacts: List[LogicArtifact],
        labels: Dict[str, str],
    ) -> List[CrossReferenceMapping]:
        """Match measures with similar names/expressions across systems."""
        mappings: List[CrossReferenceMapping] = []

        art_measures: List[Tuple[str, str, List[Measure]]] = []
        for art in artifacts:
            if art.objects.measures:
                art_measures.append((
                    art.artifact_id,
                    labels.get(art.artifact_id, ""),
                    art.objects.measures,
                ))

        for i in range(len(art_measures)):
            for j in range(i + 1, len(art_measures)):
                aid_a, sys_a, ma = art_measures[i]
                aid_b, sys_b, mb = art_measures[j]

                for m_a in ma:
                    name_a = m_a.alias or m_a.name
                    if not name_a:
                        continue
                    for m_b in mb:
                        name_b = m_b.alias or m_b.name
                        if not name_b:
                            continue
                        sim = _similarity(name_a, name_b)
                        if sim >= self.similarity_threshold:
                            # Same aggregation type boosts confidence
                            conf = sim
                            if m_a.aggregation == m_b.aggregation:
                                conf = min(conf + 0.1, 1.0)
                            mappings.append(CrossReferenceMapping(
                                domain="measure",
                                source_system=sys_a,
                                source_key=name_a,
                                target_system=sys_b,
                                target_key=name_b,
                                mapping_type=MappingType.DIRECT,
                                confidence=round(conf, 2),
                                rationale=f"Measure name match: {sim:.0%}, agg: {m_a.aggregation.value}",
                            ))

        return mappings

    def propose_conformed_dimensions(
        self, model: CrossReferenceModel
    ) -> List[Dict[str, Any]]:
        """Propose conformed dimensions from high-confidence cross-references.

        Returns a list of proposed conformed dimension descriptions.
        """
        # Group mappings by domain
        domain_mappings: Dict[str, List[CrossReferenceMapping]] = defaultdict(list)
        for m in model.mappings:
            if m.domain and m.domain != "measure":
                domain_mappings[m.domain].append(m)

        proposals: List[Dict[str, Any]] = []
        for domain, maps in domain_mappings.items():
            high_conf = [m for m in maps if m.confidence >= XREF_AUTO_PROMOTE_THRESHOLD]
            if not high_conf:
                continue

            systems = list(dict.fromkeys(
                s for m in high_conf for s in [m.source_system, m.target_system]
            ))
            keys = list(dict.fromkeys(
                k for m in high_conf for k in [m.source_key, m.target_key]
            ))
            avg_conf = sum(m.confidence for m in high_conf) / len(high_conf)

            proposals.append({
                "domain": domain,
                "conformed_name": f"dim_{domain}",
                "source_systems": systems,
                "source_keys": keys,
                "mapping_count": len(high_conf),
                "avg_confidence": round(avg_conf, 2),
                "auto_promoted": avg_conf >= XREF_AUTO_PROMOTE_THRESHOLD,
            })

        proposals.sort(key=lambda p: -p["avg_confidence"])
        return proposals

    def validate_mapping(
        self, mapping: CrossReferenceMapping
    ) -> Dict[str, Any]:
        """Validate a single cross-reference mapping.

        Returns validation result with pass/fail and details.
        """
        issues: List[str] = []

        if not mapping.source_key:
            issues.append("Missing source_key")
        if not mapping.target_key:
            issues.append("Missing target_key")
        if not mapping.source_system:
            issues.append("Missing source_system")
        if not mapping.target_system:
            issues.append("Missing target_system")
        if mapping.source_system == mapping.target_system:
            issues.append("Source and target systems are the same")
        if mapping.confidence < 0.3:
            issues.append(f"Very low confidence: {mapping.confidence}")

        return {
            "xref_id": mapping.xref_id,
            "valid": len(issues) == 0,
            "issues": issues,
            "confidence": mapping.confidence,
            "auto_promote": mapping.confidence >= XREF_AUTO_PROMOTE_THRESHOLD,
        }

    def _deduplicate(
        self, mappings: List[CrossReferenceMapping]
    ) -> List[CrossReferenceMapping]:
        """Remove duplicate mappings, keeping the highest confidence."""
        seen: Dict[str, CrossReferenceMapping] = {}
        for m in mappings:
            key = f"{m.source_system}:{m.source_key}::{m.target_system}:{m.target_key}"
            rev_key = f"{m.target_system}:{m.target_key}::{m.source_system}:{m.source_key}"
            existing = seen.get(key) or seen.get(rev_key)
            if existing:
                if m.confidence > existing.confidence:
                    seen[key] = m
            else:
                seen[key] = m
        return list(seen.values())

    def _infer_domain(self, col_a: str, col_b: str) -> str:
        """Infer the business domain from column names."""
        combined = f"{col_a} {col_b}".lower()
        domain_hints = {
            "well": ["well"],
            "account": ["account", "acct"],
            "customer": ["customer", "cust"],
            "vendor": ["vendor", "vend"],
            "product": ["product", "prod"],
            "date": ["date", "month", "year", "period"],
            "location": ["location", "loc", "region", "state"],
            "owner": ["owner"],
        }
        for domain, hints in domain_hints.items():
            if any(h in combined for h in hints):
                return domain
        return "entity"
