"""BLCE Multi-System Model Merger — conformed dimension detection and merge.

Uses column-overlap heuristics to detect conformed dimensions across
source systems and merge them into a ``UnifiedModel``.
"""
from __future__ import annotations

import logging
from typing import Any, Dict, List, Set

from .contracts import AlteredModel, UnifiedModel

logger = logging.getLogger(__name__)

# Minimum column overlap ratio to consider tables as conformed
_OVERLAP_THRESHOLD = 0.60


class MultiSystemModelMerger:
    """Merge models from multiple source systems into a unified model.

    Parameters
    ----------
    overlap_threshold : float
        Minimum column overlap ratio (0-1) to treat two tables as the
        same conformed dimension.
    """

    def __init__(self, overlap_threshold: float = _OVERLAP_THRESHOLD):
        self.overlap_threshold = overlap_threshold
        self._source_models: Dict[str, AlteredModel] = {}

    # -- registration ------------------------------------------------------

    def add_source_system(self, system_name: str, model: AlteredModel) -> None:
        """Register a source system and its altered model."""
        self._source_models[system_name] = model

    # -- conformed dimension detection -------------------------------------

    def detect_conformed_dimensions(self) -> List[Dict[str, Any]]:
        """Detect conformed dimensions across source systems.

        Matching heuristics:
        1. Exact table name match (e.g. DIM_WELL in both systems).
        2. Prefix-stripped match (ENT_DIM_WELL <-> WP_DIM_WELL).
        3. Column overlap >threshold with different names.
        """
        systems = list(self._source_models.keys())
        if len(systems) < 2:
            return []

        conformed: List[Dict[str, Any]] = []
        seen_pairs: Set[str] = set()

        for i, sys_a in enumerate(systems):
            tables_a = self._source_models[sys_a].tables
            for j in range(i + 1, len(systems)):
                sys_b = systems[j]
                tables_b = self._source_models[sys_b].tables

                for ta in tables_a:
                    name_a = ta.get("table_name", ta.get("name", ""))
                    cols_a = self._get_column_names(ta)

                    for tb in tables_b:
                        name_b = tb.get("table_name", tb.get("name", ""))
                        cols_b = self._get_column_names(tb)

                        pair_key = f"{sys_a}:{name_a}|{sys_b}:{name_b}"
                        if pair_key in seen_pairs:
                            continue

                        match_type = self._match_tables(
                            name_a, cols_a, name_b, cols_b,
                        )
                        if match_type:
                            seen_pairs.add(pair_key)
                            overlap = self._column_overlap(cols_a, cols_b)
                            unified_cols = sorted(cols_a | cols_b)
                            conformed.append({
                                "dimension_name": self._canonical_name(name_a, name_b),
                                "systems": [
                                    {"system": sys_a, "table": name_a},
                                    {"system": sys_b, "table": name_b},
                                ],
                                "match_type": match_type,
                                "column_overlap": round(overlap, 3),
                                "unified_columns": unified_cols,
                            })

        return conformed

    # -- merge -------------------------------------------------------------

    def merge(self) -> UnifiedModel:
        """Merge all registered source systems into a ``UnifiedModel``.

        Conformed dimensions get unified column sets. Remaining tables
        are treated as facts. Conflicts are logged.
        """
        conformed = self.detect_conformed_dimensions()
        conformed_tables: Set[str] = set()
        for cd in conformed:
            for s in cd.get("systems", []):
                conformed_tables.add(f"{s['system']}:{s['table']}")

        # Collect facts (tables not identified as conformed dimensions)
        fact_tables: List[Dict[str, Any]] = []
        for sys_name, model in self._source_models.items():
            for t in model.tables:
                tbl_name = t.get("table_name", t.get("name", ""))
                key = f"{sys_name}:{tbl_name}"
                if key not in conformed_tables:
                    fact_tables.append({
                        "source_system": sys_name,
                        "table_name": tbl_name,
                        "columns": self._get_column_names_list(t),
                    })

        # Detect merge conflicts (same column, different types)
        conflicts: List[str] = []
        for cd in conformed:
            systems = cd.get("systems", [])
            if len(systems) >= 2:
                s0 = systems[0]
                s1 = systems[1]
                t0 = self._find_table(s0["system"], s0["table"])
                t1 = self._find_table(s1["system"], s1["table"])
                if t0 and t1:
                    type_map_0 = self._get_column_types(t0)
                    type_map_1 = self._get_column_types(t1)
                    shared = set(type_map_0.keys()) & set(type_map_1.keys())
                    for col in shared:
                        if type_map_0[col] and type_map_1[col] and type_map_0[col] != type_map_1[col]:
                            conflicts.append(
                                f"{cd['dimension_name']}.{col}: "
                                f"{s0['system']}={type_map_0[col]} vs "
                                f"{s1['system']}={type_map_1[col]}"
                            )

        return UnifiedModel(
            source_systems=list(self._source_models.keys()),
            conformed_dimensions=conformed,
            fact_tables=fact_tables,
            merge_conflicts=conflicts,
        )

    # -- reporting ---------------------------------------------------------

    def get_merge_report(self) -> Dict[str, Any]:
        """Return stats and conflict summary for the merge."""
        unified = self.merge()
        return {
            "source_systems": unified.source_systems,
            "conformed_dimensions": len(unified.conformed_dimensions),
            "fact_tables": len(unified.fact_tables),
            "merge_conflicts": len(unified.merge_conflicts),
            "conflict_details": unified.merge_conflicts,
            "dimension_names": [
                cd.get("dimension_name", "") for cd in unified.conformed_dimensions
            ],
        }

    # -- internal helpers --------------------------------------------------

    @staticmethod
    def _get_column_names(table: Dict[str, Any]) -> Set[str]:
        """Extract column names as a set (lowercase)."""
        cols = table.get("columns", [])
        result: Set[str] = set()
        for c in cols:
            if isinstance(c, str):
                result.add(c.lower())
            elif isinstance(c, dict):
                name = c.get("name", "")
                if name:
                    result.add(name.lower())
        return result

    @staticmethod
    def _get_column_names_list(table: Dict[str, Any]) -> List[str]:
        cols = table.get("columns", [])
        result: List[str] = []
        for c in cols:
            if isinstance(c, str):
                result.append(c)
            elif isinstance(c, dict):
                result.append(c.get("name", ""))
        return result

    @staticmethod
    def _get_column_types(table: Dict[str, Any]) -> Dict[str, str]:
        """Map column name (lower) -> type (lower)."""
        cols = table.get("columns", [])
        result: Dict[str, str] = {}
        for c in cols:
            if isinstance(c, dict):
                name = c.get("name", "").lower()
                ctype = c.get("type", "").lower()
                if name:
                    result[name] = ctype
        return result

    def _match_tables(
        self,
        name_a: str,
        cols_a: Set[str],
        name_b: str,
        cols_b: Set[str],
    ) -> str:
        """Determine match type between two tables. Returns match type or ''."""
        na = name_a.lower()
        nb = name_b.lower()

        # 1. Exact name match
        if na == nb:
            return "exact_name"

        # 2. Prefix-stripped match
        stripped_a = self._strip_prefix(na)
        stripped_b = self._strip_prefix(nb)
        if stripped_a and stripped_b and stripped_a == stripped_b:
            return "prefix_stripped"

        # 3. Column overlap
        if cols_a and cols_b:
            overlap = self._column_overlap(cols_a, cols_b)
            if overlap >= self.overlap_threshold:
                return "column_overlap"

        return ""

    @staticmethod
    def _strip_prefix(name: str) -> str:
        """Strip common ERP prefixes (e.g. ENT_, WP_, SAP_)."""
        parts = name.split("_", 1)
        if len(parts) == 2 and len(parts[0]) <= 4:
            return parts[1]
        return name

    @staticmethod
    def _column_overlap(cols_a: Set[str], cols_b: Set[str]) -> float:
        """Compute Jaccard-like overlap: |intersection| / min(|a|, |b|)."""
        if not cols_a or not cols_b:
            return 0.0
        intersection = cols_a & cols_b
        return len(intersection) / min(len(cols_a), len(cols_b))

    @staticmethod
    def _canonical_name(name_a: str, name_b: str) -> str:
        """Choose a canonical name for a conformed dimension."""
        # Prefer the shorter name or the one with DIM_ prefix
        na = name_a.upper()
        nb = name_b.upper()
        if na.startswith("DIM_"):
            return na
        if nb.startswith("DIM_"):
            return nb
        return na if len(na) <= len(nb) else nb

    def _find_table(self, system: str, table_name: str) -> Dict[str, Any]:
        """Find a table dict from a system model."""
        model = self._source_models.get(system)
        if model is None:
            return {}
        for t in model.tables:
            if t.get("table_name", t.get("name", "")) == table_name:
                return t
        return {}
