"""Grain analysis for cross-system fact table comparison."""
from __future__ import annotations

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

from .contracts import (
    AlignmentStrategy,
    GrainComparison,
    GrainLevel,
    GrainRelationship,
    GrainSpec,
)

logger = logging.getLogger(__name__)

# Grain hierarchy — finer to coarser
_GRAIN_ORDER: Dict[GrainLevel, int] = {
    GrainLevel.TRANSACTIONAL: 0,
    GrainLevel.DAILY: 1,
    GrainLevel.MONTHLY: 2,
    GrainLevel.QUARTERLY: 3,
    GrainLevel.YEARLY: 4,
}

# Temporal column name patterns → grain classification
_TEMPORAL_PATTERNS: List[tuple] = [
    (re.compile(r"(?:^|_)(date|dt|day)(?:_|$)", re.I), GrainLevel.DAILY),
    (re.compile(r"(?:^|_)(month|mth|mon|yyyymm)(?:_|$)", re.I), GrainLevel.MONTHLY),
    (re.compile(r"(?:^|_)(quarter|qtr|yyyyqq)(?:_|$)", re.I), GrainLevel.QUARTERLY),
    (re.compile(r"(?:^|_)(year|yr|yyyy)(?:_|$)", re.I), GrainLevel.YEARLY),
    (re.compile(r"(?:^|_)(timestamp|ts|created_at|updated_at)(?:_|$)", re.I), GrainLevel.TRANSACTIONAL),
]


class GrainAnalyzer:
    """Detect and compare grain across system fact tables."""

    def __init__(self):
        self._specs: Dict[str, List[GrainSpec]] = {}

    def register_system(
        self,
        system_name: str,
        tables: List[Dict[str, Any]],
    ) -> List[GrainSpec]:
        """Register fact tables for a system and detect their grain.

        Each table dict should have:
          - table_name: str
          - columns: List[str]
          - row_count: int (optional)
        """
        specs = []
        for tbl in tables:
            spec = self._detect_grain(system_name, tbl)
            specs.append(spec)
        self._specs[system_name] = specs
        logger.info("Registered %d fact tables for system '%s'", len(specs), system_name)
        return specs

    def _detect_grain(self, system: str, table: Dict[str, Any]) -> GrainSpec:
        """Detect grain for a single fact table."""
        name = table.get("table_name", "")
        columns = table.get("columns", [])
        row_count = table.get("row_count", 0)

        temporal_col = ""
        grain_level = GrainLevel.TRANSACTIONAL
        grain_cols: List[str] = []

        for col in columns:
            col_upper = col.upper()
            # Detect temporal column
            for pattern, level in _TEMPORAL_PATTERNS:
                if pattern.search(col):
                    temporal_col = col
                    grain_level = level
                    grain_cols.append(col)
                    break

            # Detect dimension key columns (common FK patterns)
            if col_upper.endswith("_ID") or col_upper.endswith("_KEY"):
                if not col_upper.startswith("SURROGATE") and col_upper not in ("ROW_ID",):
                    grain_cols.append(col)

        return GrainSpec(
            table_name=name,
            source_system=system,
            grain_columns=grain_cols,
            grain_level=grain_level,
            row_count_estimate=row_count,
            temporal_column=temporal_col,
        )

    @staticmethod
    def classify_grain_from_column(col_name: str) -> GrainLevel:
        """Classify temporal grain from a column name."""
        for pattern, level in _TEMPORAL_PATTERNS:
            if pattern.search(col_name):
                return level
        return GrainLevel.TRANSACTIONAL

    def compare_grains(
        self,
        system_a: str,
        table_a: str,
        system_b: str,
        table_b: str,
    ) -> GrainComparison:
        """Compare grain between two specific fact tables."""
        spec_a = self._find_spec(system_a, table_a)
        spec_b = self._find_spec(system_b, table_b)
        if not spec_a or not spec_b:
            missing = table_a if not spec_a else table_b
            raise ValueError(f"Table '{missing}' not found in registered specs")
        return self._compare(spec_a, spec_b)

    def compare_all(self) -> List[GrainComparison]:
        """Compare grain across all registered system table pairs."""
        systems = list(self._specs.keys())
        results: List[GrainComparison] = []
        for i, sys_a in enumerate(systems):
            for j in range(i + 1, len(systems)):
                sys_b = systems[j]
                for spec_a in self._specs[sys_a]:
                    for spec_b in self._specs[sys_b]:
                        comp = self._compare(spec_a, spec_b)
                        results.append(comp)
        return results

    def _compare(self, a: GrainSpec, b: GrainSpec) -> GrainComparison:
        """Compare two grain specs."""
        # Find shared dimension columns (by stripped name)
        a_dims = self._strip_prefixes(a.grain_columns)
        b_dims = self._strip_prefixes(b.grain_columns)
        shared = sorted(a_dims & b_dims)

        # Determine grain relationship
        order_a = _GRAIN_ORDER.get(a.grain_level, 0)
        order_b = _GRAIN_ORDER.get(b.grain_level, 0)

        if not shared:
            relationship = GrainRelationship.INCOMPATIBLE
            strategy = AlignmentStrategy.BRIDGE_TABLE
        elif order_a == order_b:
            relationship = GrainRelationship.SAME
            strategy = AlignmentStrategy.DIRECT_UNION
        elif order_a < order_b:
            relationship = GrainRelationship.A_FINER
            strategy = AlignmentStrategy.AGGREGATE_TO_COARSER
        else:
            relationship = GrainRelationship.B_FINER
            strategy = AlignmentStrategy.AGGREGATE_TO_COARSER

        return GrainComparison(
            system_a=a.source_system,
            table_a=a.table_name,
            grain_a=a,
            system_b=b.source_system,
            table_b=b.table_name,
            grain_b=b,
            relationship=relationship,
            shared_dimensions=shared,
            alignment_strategy=strategy,
        )

    def _find_spec(self, system: str, table: str) -> Optional[GrainSpec]:
        specs = self._specs.get(system, [])
        for s in specs:
            if s.table_name == table:
                return s
        return None

    @staticmethod
    def _strip_prefixes(cols: List[str]) -> Set[str]:
        """Strip common ERP prefixes to normalize column names for comparison."""
        stripped = set()
        for c in cols:
            norm = re.sub(r"^(DIM_|FACT_|ENT_|STG_|SRC_)", "", c.upper())
            stripped.add(norm)
        return stripped

    @staticmethod
    def is_finer(a: GrainLevel, b: GrainLevel) -> bool:
        """Return True if grain a is finer than grain b."""
        return _GRAIN_ORDER.get(a, 0) < _GRAIN_ORDER.get(b, 0)

    @staticmethod
    def coarser_grain(a: GrainLevel, b: GrainLevel) -> GrainLevel:
        """Return the coarser of two grain levels."""
        if _GRAIN_ORDER.get(a, 0) >= _GRAIN_ORDER.get(b, 0):
            return a
        return b
