"""Cortex Discovery Agent.

AI-powered hierarchy discovery using heuristic analysis.
Works without Snowflake Cortex by default; Cortex COMPLETE()
is an optional enhancement when available.
"""

from __future__ import annotations

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

from .types import (
    DiscoveryResult,
    DynamicColumnMapping,
    JoinPattern,
    MartConfig,
    QualityIssue,
    QualityIssueLevel,
    ReportType,
)

logger = logging.getLogger(__name__)

# Known patterns for report type detection
_PL_KEYWORDS = {"revenue", "cost", "expense", "profit", "loss", "income", "ebitda", "margin"}
_BS_KEYWORDS = {"asset", "liability", "equity", "balance", "receivable", "payable", "debt"}
_LOS_KEYWORDS = {"service", "product", "segment", "division", "business_unit", "department"}

# Known ID_SOURCE patterns
_KNOWN_ID_SOURCES = {
    "REVENUE", "COST", "HEADCOUNT", "FTE", "BUDGET", "ACTUAL",
    "FORECAST", "VARIANCE", "GROSS", "NET", "OPEX", "CAPEX",
}


class CortexDiscoveryAgent:
    """Heuristic-based hierarchy and mart config discovery."""

    def __init__(self) -> None:
        self._results: Dict[str, DiscoveryResult] = {}

    # ------------------------------------------------------------------
    # Discovery
    # ------------------------------------------------------------------

    def discover_hierarchy(
        self,
        table_data: Dict[str, Any],
        table_name: str = "unknown",
    ) -> DiscoveryResult:
        """Scan table metadata/data to infer hierarchy structure.

        Detects:
        - Report type (P&L, Balance Sheet, LOS) from column names/values
        - Hierarchy levels from LEVEL_N pattern columns
        - ID_SOURCE distribution
        - Data quality issues

        Args:
            table_data: Dict with keys:
                - columns: List[str] — column names
                - sample_values: Dict[str, List] — column name → sample values
                - row_count: int (optional)
            table_name: Name of the source table.

        Returns:
            DiscoveryResult with findings.
        """
        columns = [c.upper() for c in table_data.get("columns", [])]
        sample_values = table_data.get("sample_values", {})
        quality_issues: List[QualityIssue] = []
        suggestions: List[str] = []

        # Detect hierarchy levels
        level_cols = self._detect_level_columns(columns)

        # Detect report type from column names and values
        report_type, type_confidence = self._detect_report_type(columns, sample_values)

        # Find ID_SOURCE values
        id_source_values = self._extract_id_sources(columns, sample_values)

        # Detect join patterns
        join_patterns = self.detect_join_patterns(id_source_values, sample_values)

        # Quality checks
        quality_issues.extend(self._check_quality(columns, sample_values, level_cols))

        # Suggestions
        if not level_cols:
            suggestions.append("No LEVEL_N columns found — consider manual hierarchy definition")
        if len(id_source_values) == 0:
            suggestions.append("No ID_SOURCE column found — add column mappings manually")
        if len(join_patterns) == 0:
            suggestions.append("No join patterns detected — add at least one UNION ALL branch")
        if report_type == ReportType.CUSTOM:
            suggestions.append(f"Could not determine report type from columns; defaulting to Custom")

        confidence = type_confidence * (0.5 + 0.5 * min(len(level_cols) / 5, 1.0))

        result = DiscoveryResult(
            hierarchy_type=report_type,
            level_count=len(level_cols),
            detected_levels=level_cols,
            suggestions=suggestions,
            quality_issues=quality_issues,
            confidence=round(confidence, 2),
            id_source_values=id_source_values,
            recommended_join_patterns=join_patterns,
        )
        self._results[table_name] = result
        logger.info(
            "Discovery for %s: type=%s, levels=%d, confidence=%.2f",
            table_name, report_type.value, len(level_cols), confidence,
        )
        return result

    # ------------------------------------------------------------------
    # ID_SOURCE Analysis
    # ------------------------------------------------------------------

    def analyze_id_source_distribution(
        self,
        sample_values: Dict[str, List[Any]],
    ) -> Dict[str, Any]:
        """Analyze the value distribution of ID_SOURCE column.

        Args:
            sample_values: Column name → list of sample values.

        Returns:
            Dict with distinct_count, top_values, coverage.
        """
        id_col = None
        for col in sample_values:
            if col.upper() == "ID_SOURCE":
                id_col = col
                break

        if id_col is None:
            return {"distinct_count": 0, "top_values": [], "coverage": 0.0}

        values = [str(v).upper() for v in sample_values[id_col] if v is not None]
        counter = Counter(values)
        total = len(values)
        distinct = len(counter)

        top = counter.most_common(10)
        known_count = sum(c for v, c in top if v in _KNOWN_ID_SOURCES)

        return {
            "distinct_count": distinct,
            "top_values": [{"value": v, "count": c} for v, c in top],
            "coverage": round(known_count / total, 2) if total else 0.0,
            "total_rows": total,
        }

    # ------------------------------------------------------------------
    # Join Pattern Detection
    # ------------------------------------------------------------------

    def detect_join_patterns(
        self,
        id_source_values: List[str],
        sample_values: Dict[str, List[Any]],
    ) -> List[JoinPattern]:
        """Infer UNION ALL branches from ID_SOURCE groupings.

        Groups ID_SOURCE values by prefix or semantic category
        to suggest join pattern branches.

        Args:
            id_source_values: Distinct ID_SOURCE values.
            sample_values: Full sample data for context.

        Returns:
            List of inferred JoinPattern objects.
        """
        if not id_source_values:
            return []

        # Group by prefix (first word before underscore)
        groups: Dict[str, List[str]] = {}
        for val in id_source_values:
            prefix = val.split("_")[0] if "_" in val else val
            groups.setdefault(prefix, []).append(val)

        patterns: List[JoinPattern] = []
        for prefix, members in groups.items():
            pattern = JoinPattern(
                name=prefix.lower(),
                join_keys=["HIERARCHY_KEY"],
                fact_keys=[f"AMT_{m}" for m in members[:3]],  # Limit fact keys
            )
            patterns.append(pattern)

        return patterns

    # ------------------------------------------------------------------
    # Typo Detection
    # ------------------------------------------------------------------

    def detect_typos(
        self,
        values: List[str],
        known_values: Optional[Set[str]] = None,
    ) -> List[Dict[str, Any]]:
        """Fuzzy match values against known set to find typos.

        Uses simple edit-distance heuristic. For production use,
        integrates with ``databridge.reconciler.find_similar``.

        Args:
            values: Values to check.
            known_values: Reference set (defaults to _KNOWN_ID_SOURCES).

        Returns:
            List of potential typo dicts with value, suggestion, distance.
        """
        reference = known_values or _KNOWN_ID_SOURCES
        typos: List[Dict[str, Any]] = []

        for val in values:
            val_upper = val.upper()
            if val_upper in reference:
                continue
            # Simple prefix match as heuristic
            for known in reference:
                if _edit_distance(val_upper, known) <= 2 and val_upper != known:
                    typos.append({
                        "value": val,
                        "suggestion": known,
                        "distance": _edit_distance(val_upper, known),
                    })
                    break

        return typos

    # ------------------------------------------------------------------
    # Config Recommendation
    # ------------------------------------------------------------------

    def generate_config_recommendation(
        self,
        table_name: str,
        table_data: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Build a MartConfig recommendation from discovery results.

        Args:
            table_name: Source table identifier.
            table_data: Table metadata (columns, sample_values).

        Returns:
            Dict representation of a recommended MartConfig.
        """
        result = self._results.get(table_name)
        if result is None:
            result = self.discover_hierarchy(table_data, table_name)

        config_dict = {
            "project_name": _suggest_project_name(table_name),
            "report_type": result.hierarchy_type.value,
            "hierarchy_table": f"{table_name}_HIERARCHY",
            "mapping_table": table_name,
            "account_segment": "GROSS",
            "measure_prefix": "AMT_",
            "has_sign_change": result.hierarchy_type == ReportType.PROFIT_AND_LOSS,
            "has_exclusions": False,
            "has_group_filter_precedence": False,
            "column_mappings": [
                {"id_source": v, "physical_column": f"AMT_{v}"}
                for v in result.id_source_values[:20]
            ],
            "join_patterns": [
                jp.model_dump() for jp in result.recommended_join_patterns
            ],
            "confidence": result.confidence,
            "quality_issues": [qi.model_dump() for qi in result.quality_issues],
            "suggestions": result.suggestions,
        }
        return config_dict

    # ------------------------------------------------------------------
    # Explain
    # ------------------------------------------------------------------

    def explain_discovery(self, table_name: str) -> str:
        """Human-readable summary of discovery results.

        Args:
            table_name: Table to explain.

        Returns:
            Formatted summary string.
        """
        result = self._results.get(table_name)
        if result is None:
            return f"No discovery results for '{table_name}'. Run discover_hierarchy first."

        lines = [
            f"Discovery Summary: {table_name}",
            f"{'=' * 40}",
            f"Report Type:    {result.hierarchy_type.value} (confidence: {result.confidence})",
            f"Levels Found:   {result.level_count} — {', '.join(result.detected_levels) or 'none'}",
            f"ID_SOURCE vals: {len(result.id_source_values)}",
            f"Join Patterns:  {len(result.recommended_join_patterns)}",
        ]

        if result.quality_issues:
            lines.append(f"\nQuality Issues ({len(result.quality_issues)}):")
            for qi in result.quality_issues:
                lines.append(f"  [{qi.level.value.upper()}] {qi.message}")
                if qi.suggestion:
                    lines.append(f"    → {qi.suggestion}")

        if result.suggestions:
            lines.append(f"\nSuggestions ({len(result.suggestions)}):")
            for s in result.suggestions:
                lines.append(f"  • {s}")

        return "\n".join(lines)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _detect_level_columns(columns: List[str]) -> List[str]:
        """Find LEVEL_N pattern columns."""
        levels = []
        for col in columns:
            if re.match(r"^LEVEL_?\d+$", col, re.IGNORECASE):
                levels.append(col)
        levels.sort(key=lambda c: int(re.search(r"\d+", c).group()))  # type: ignore[union-attr]
        return levels

    @staticmethod
    def _detect_report_type(
        columns: List[str],
        sample_values: Dict[str, List[Any]],
    ) -> tuple[ReportType, float]:
        """Detect report type from column names and values."""
        col_text = " ".join(columns).lower()
        # Also include sample values for richer signal
        val_text = ""
        for vals in sample_values.values():
            val_text += " ".join(str(v).lower() for v in vals[:20] if v)

        combined = col_text + " " + val_text

        pl_score = sum(1 for kw in _PL_KEYWORDS if kw in combined)
        bs_score = sum(1 for kw in _BS_KEYWORDS if kw in combined)
        los_score = sum(1 for kw in _LOS_KEYWORDS if kw in combined)

        max_score = max(pl_score, bs_score, los_score)
        if max_score == 0:
            return ReportType.CUSTOM, 0.3

        total = pl_score + bs_score + los_score
        confidence = round(max_score / total, 2) if total else 0.3

        if pl_score == max_score:
            return ReportType.PROFIT_AND_LOSS, confidence
        if bs_score == max_score:
            return ReportType.BALANCE_SHEET, confidence
        return ReportType.LINE_OF_SERVICE, confidence

    @staticmethod
    def _extract_id_sources(
        columns: List[str],
        sample_values: Dict[str, List[Any]],
    ) -> List[str]:
        """Extract distinct ID_SOURCE values."""
        for col_key, vals in sample_values.items():
            if col_key.upper() == "ID_SOURCE":
                return sorted(set(str(v).upper() for v in vals if v))
        return []

    @staticmethod
    def _check_quality(
        columns: List[str],
        sample_values: Dict[str, List[Any]],
        level_cols: List[str],
    ) -> List[QualityIssue]:
        """Run basic quality checks."""
        issues: List[QualityIssue] = []

        # Check for NULL-heavy columns
        for col, vals in sample_values.items():
            if not vals:
                continue
            null_ratio = sum(1 for v in vals if v is None or str(v).strip() == "") / len(vals)
            if null_ratio > 0.5:
                issues.append(QualityIssue(
                    level=QualityIssueLevel.WARNING,
                    message=f"Column '{col}' has {null_ratio:.0%} NULL/empty values",
                    column=col,
                    suggestion=f"Review data completeness for {col}",
                ))

        # Check for hierarchy gaps
        if len(level_cols) >= 2:
            # Verify sequential numbering
            level_nums = []
            for lc in level_cols:
                m = re.search(r"\d+", lc)
                if m:
                    level_nums.append(int(m.group()))
            expected = list(range(min(level_nums), max(level_nums) + 1))
            if level_nums != expected:
                issues.append(QualityIssue(
                    level=QualityIssueLevel.ERROR,
                    message=f"Hierarchy level gap: found {level_nums}, expected {expected}",
                    suggestion="Ensure hierarchy levels are sequential",
                ))

        return issues


# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

def _edit_distance(a: str, b: str) -> int:
    """Simple Levenshtein distance (for short strings)."""
    if len(a) > len(b):
        a, b = b, a
    distances = list(range(len(a) + 1))
    for i2, c2 in enumerate(b):
        new_distances = [i2 + 1]
        for i1, c1 in enumerate(a):
            if c1 == c2:
                new_distances.append(distances[i1])
            else:
                new_distances.append(1 + min(distances[i1], distances[i1 + 1], new_distances[-1]))
        distances = new_distances
    return distances[-1]


def _suggest_project_name(table_name: str) -> str:
    """Generate a project name from a table name."""
    name = table_name.upper()
    for prefix in ("STG_", "RAW_", "SRC_", "DIM_", "FACT_"):
        if name.startswith(prefix):
            name = name[len(prefix):]
    return f"MART_{name}"
