"""Table Analyzer — heuristic classification of tables without FK relationships.

Provides ERP-aware domain inference, natural key detection, role classification
(transaction/reference/bridge/config), and plain-English purpose summaries.

Used as a fallback when DimensionDetector cannot run (no FK relationships found).
Output shape matches DimensionDetector.classify_tables() so downstream consumers
(bus_matrix, model_proposer, intake_generator) work unchanged.
"""
from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)

# ── Column-name patterns (shared with model_proposer) ──────────────────────
_PK_SUFFIXES = re.compile(r"(_ID|_HID|_TID|_KEY|_CODE|_NUM|_SK)$", re.IGNORECASE)
_DATE_PATTERNS = re.compile(r"(DATE|_DT$|_DATE$|_TS$|TIMESTAMP)", re.IGNORECASE)
_MEASURE_PATTERNS = re.compile(
    r"(AMOUNT|AMT|QTY|QUANTITY|TOTAL|BALANCE|RATE|PRICE|COST|COUNT|SUM|WEIGHT|VOLUME)",
    re.IGNORECASE,
)

# ── Domain keyword → domain label ──────────────────────────────────────────
_DOMAIN_KEYWORDS: Dict[str, str] = {
    "REVENUE": "Revenue",
    "REV": "Revenue",
    "PRODUCTION": "Production",
    "PROD": "Production",
    "VOLUME": "Production",
    "WELL": "Operations",
    "COMPLETION": "Operations",
    "DRILL": "Operations",
    "METER": "Operations",
    "PROPERTY": "Operations",
    "ACCOUNT": "Finance",
    "GL": "General Ledger",
    "GENERAL_LEDGER": "General Ledger",
    "JOURNAL": "General Ledger",
    "LEDGER": "General Ledger",
    "BUDGET": "Finance",
    "FINANCIAL": "Finance",
    "INVOICE": "Finance",
    "PAYMENT": "Finance",
    "BILLING": "Finance",
    "JIB": "Joint Interest Billing",
    "AFE": "Capital / AFE",
    "CAPITAL": "Capital / AFE",
    "CAPEX": "Capital / AFE",
    "LAND": "Land / Contracts",
    "CONTRACT": "Land / Contracts",
    "LEASE": "Land / Contracts",
    "TRACT": "Land / Contracts",
    "INTEREST": "Land / Contracts",
    "OWNER": "Land / Contracts",
    "TAX": "Tax",
    "PAYROLL": "Payroll",
    "VENDOR": "Procurement",
    "PURCHASER": "Procurement",
    "PURCHASE": "Procurement",
    "CUSTOMER": "Customers",
    "ENTITY": "Organization",
    "CORP": "Organization",
    "DEPARTMENT": "Organization",
    "COST_CENTER": "Organization",
    "USER": "Admin",
    "PREFERENCE": "Admin",
    "CONFIG": "Admin",
    "AGENT": "Admin",
    "DATE": "Calendar",
    "CALENDAR": "Calendar",
    "REPORT": "Reporting",
    "ANALYTICS": "Reporting",
    "XREF": "Cross-Reference",
    "FACTLESS": "Cross-Reference",
    "MAPPING": "Cross-Reference",
    "HIERARCHY": "Hierarchy",
    "GROUP": "Hierarchy",
    "DECK": "Forecasting",
    "FORECAST": "Forecasting",
    "ESTIMATE": "Forecasting",
}

# ── Table name prefix → role hint ──────────────────────────────────────────
_ROLE_PREFIX_MAP = {
    "DIM_": "reference",
    "FACT_": "transaction",
    "FCT_": "transaction",
    "XREF_": "bridge",
    "FACTLESS_XREF_": "bridge",
    "BRIDGE_": "bridge",
    "LKP_": "reference",
    "REF_": "reference",
    "STG_": "config",
    "CFG_": "config",
    "VW_": "reference",
    "REPORT_": "transaction",
    "DT_": "transaction",
}

# ── Role keyword signals ───────────────────────────────────────────────────
_ROLE_KEYWORDS_TXN = {"TRANSACTION", "FACT", "FINANCIAL", "ACTUALS", "DETAIL", "DETAILS", "LEDGER", "JOURNAL"}
_ROLE_KEYWORDS_REF = {"MASTER", "LOOKUP", "CODE", "TYPE", "STATUS", "CATEGORY", "DICTIONARY"}
_ROLE_KEYWORDS_BRIDGE = {"XREF", "CROSS_REF", "MAPPING", "LINK", "ASSOC", "FACTLESS"}


# ── Dataclasses ────────────────────────────────────────────────────────────

@dataclass
class TableAnalysis:
    """Per-table analysis result."""
    table_name: str
    domain: str = "Unknown"
    role: str = "unknown"          # transaction | reference | bridge | config | unknown
    natural_keys: List[str] = field(default_factory=list)
    purpose_summary: str = ""
    confidence: float = 0.0
    erp_prefix: str = ""
    erp_domain: str = ""
    column_count: int = 0
    row_count: int = 0
    has_date_columns: bool = False
    has_measure_columns: bool = False
    fk_like_column_count: int = 0

    def to_dict(self) -> Dict[str, Any]:
        return {
            "table_name": self.table_name,
            "domain": self.domain,
            "role": self.role,
            "natural_keys": self.natural_keys,
            "purpose_summary": self.purpose_summary,
            "confidence": self.confidence,
            "erp_prefix": self.erp_prefix,
            "erp_domain": self.erp_domain,
        }


@dataclass
class EnhancedClassification:
    """Aggregated result — matches DimensionDetector.classify_tables() shape."""
    classification: Dict[str, str] = field(default_factory=dict)
    table_analyses: Dict[str, Dict[str, Any]] = field(default_factory=dict)
    domain_groups: Dict[str, List[str]] = field(default_factory=dict)
    natural_keys: Dict[str, List[str]] = field(default_factory=dict)
    purpose_summaries: Dict[str, str] = field(default_factory=dict)
    erp_type: str = ""
    method: str = "heuristic_table_analyzer"

    def to_dict(self) -> Dict[str, Any]:
        return {
            "classification": self.classification,
            "table_analyses": self.table_analyses,
            "domain_groups": self.domain_groups,
            "natural_keys": self.natural_keys,
            "purpose_summaries": self.purpose_summaries,
            "erp_type": self.erp_type,
            "method": self.method,
        }


# ── Main analyzer class ───────────────────────────────────────────────────

class TableAnalyzer:
    """Heuristic table analyzer — works without FK relationships.

    Uses naming patterns, column analysis, row counts, and ERP prefix
    matching to classify tables into Kimball roles.
    """

    def __init__(
        self,
        erp_type: Optional[str] = None,
        table_profiles: Optional[List[Dict[str, Any]]] = None,
        column_metadata: Optional[List[Dict[str, Any]]] = None,
    ):
        """
        Args:
            erp_type: Override ERP type. Auto-detected if None.
            table_profiles: Per-table profiles from load_metadata
                (keys: table_name, row_count, columns).
            column_metadata: INFORMATION_SCHEMA column metadata
                (keys: TABLE_NAME, COLUMN_NAME, DATA_TYPE).
        """
        self.erp_type = erp_type
        self.table_profiles = {
            p.get("table_name", ""): p for p in (table_profiles or [])
        }
        # Build column lookup: table → [col_name, ...]
        self._columns_by_table: Dict[str, List[str]] = {}
        for cm in (column_metadata or []):
            tbl = cm.get("TABLE_NAME", "")
            col = cm.get("COLUMN_NAME", "")
            if tbl and col:
                self._columns_by_table.setdefault(tbl, []).append(col)
        # Also populate from table_profiles if column_metadata is sparse
        for tbl, profile in self.table_profiles.items():
            if tbl not in self._columns_by_table:
                cols = profile.get("columns", [])
                self._columns_by_table[tbl] = [
                    c.get("column_name", c.get("name", "")) if isinstance(c, dict) else str(c)
                    for c in cols
                ]

    # ── 1. Enhanced ERP detection ──────────────────────────────────────

    def detect_erp_enhanced(self, table_names: List[str]) -> Dict[str, Any]:
        """Detect ERP type with per-table prefix→domain mapping.

        Wraps detect_erp_type_detailed() from the registry.
        """
        try:
            from src.data_modeling.erp_configs.registry import detect_erp_type_detailed
            result = detect_erp_type_detailed(table_names)
            if result.get("erp_type"):
                self.erp_type = result["erp_type"]
            return result
        except Exception as exc:
            logger.debug("detect_erp_type_detailed failed: %s", exc)
            return {"erp_type": self.erp_type or "", "confidence": 0, "per_table_matches": {}}

    # ── 2. Domain inference ────────────────────────────────────────────

    def infer_domain(self, table_name: str, erp_match: Optional[Dict[str, str]] = None) -> str:
        """Infer business domain for a table.

        3-tier strategy:
          (a) ERP prefix match from BUILTIN_MAPS
          (b) Table name keyword matching
          (c) Column name pattern matching
        """
        upper = table_name.upper()

        # (a) ERP prefix match
        if erp_match and erp_match.get("domain"):
            return erp_match["domain"]

        # (b) Table name keywords — check longest keywords first
        for keyword, domain in sorted(_DOMAIN_KEYWORDS.items(), key=lambda x: len(x[0]), reverse=True):
            if keyword in upper:
                return domain

        # (c) Column name patterns
        columns = self._columns_by_table.get(table_name, [])
        col_domains: Dict[str, int] = {}
        for col in columns:
            col_upper = col.upper()
            for keyword, domain in _DOMAIN_KEYWORDS.items():
                if keyword in col_upper:
                    col_domains[domain] = col_domains.get(domain, 0) + 1
        if col_domains:
            return max(col_domains, key=col_domains.get)  # type: ignore[arg-type]

        return "Unknown"

    # ── 3. Natural key detection ───────────────────────────────────────

    def detect_natural_keys(self, table_name: str) -> List[str]:
        """Detect likely natural key columns.

        Scoring:
          +3 for _ID/_KEY/_CODE suffix
          +2 for non-null (if metadata available)
          +2 for first column position
          -3 for measure patterns (AMOUNT, QTY, etc.)
          Threshold: score >= 5
        """
        columns = self._columns_by_table.get(table_name, [])
        if not columns:
            return []

        scored: List[Tuple[str, int]] = []
        for idx, col in enumerate(columns):
            score = 0
            upper = col.upper()

            # +3 for PK suffix
            if _PK_SUFFIXES.search(upper):
                score += 3

            # +2 for first column
            if idx == 0:
                score += 2

            # -3 for measure pattern
            if _MEASURE_PATTERNS.search(upper):
                score -= 3

            # -2 for date pattern (dates are rarely keys)
            if _DATE_PATTERNS.search(upper):
                score -= 2

            # +2 for "natural key" naming (NAME, PROPNUM, etc.)
            if upper.endswith("_NAME") or upper.endswith("NUM") or upper == "NAME":
                score += 2

            if score >= 3:
                scored.append((col, score))

        scored.sort(key=lambda x: x[1], reverse=True)
        return [col for col, _ in scored[:5]]

    # ── 4. Role classification ─────────────────────────────────────────

    def classify_role(self, table_name: str, domain: Optional[str] = None) -> Tuple[str, float]:
        """Classify table role using multi-signal scoring.

        Args:
            table_name: Table name to classify.
            domain: Pre-computed domain (avoids re-inferring without ERP match).

        Returns (role, confidence) where role is one of:
        transaction, reference, bridge, config, unknown.
        """
        upper = table_name.upper()
        columns = self._columns_by_table.get(table_name, [])
        profile = self.table_profiles.get(table_name, {})
        row_count = profile.get("row_count", 0) or 0
        col_count = len(columns)

        # Score accumulators
        scores: Dict[str, float] = {
            "transaction": 0,
            "reference": 0,
            "bridge": 0,
            "config": 0,
        }

        # ── Signal 1: Name prefix (strong signal) ──
        for prefix, role in sorted(_ROLE_PREFIX_MAP.items(), key=lambda x: len(x[0]), reverse=True):
            if upper.startswith(prefix):
                scores[role] += 4.0
                break

        # ── Signal 2: Name keywords ──
        for kw in _ROLE_KEYWORDS_TXN:
            if kw in upper:
                scores["transaction"] += 2.0
                break
        for kw in _ROLE_KEYWORDS_REF:
            if kw in upper:
                scores["reference"] += 2.0
                break
        for kw in _ROLE_KEYWORDS_BRIDGE:
            if kw in upper:
                scores["bridge"] += 2.5
                break

        # ── Signal 2b: ERP domain-based inference ──
        domain = domain or self.infer_domain(table_name)
        _TRANSACTION_DOMAINS = {
            "General Ledger", "Revenue", "Joint Interest Billing", "Tax",
            "Payroll", "Reporting", "Forecasting",
        }
        _MOSTLY_TRANSACTION_DOMAINS = {
            "Finance", "Production", "Capital / AFE", "Accounts / AP",
            "Fixed Assets / Budget", "Land / Contracts",
            "Production / Decline", "AFE / Capital", "Operations",
        }
        _REFERENCE_DOMAINS = {
            "Calendar", "Organization", "Admin", "Hierarchy", "Customers",
            "Procurement",
        }
        if domain in _TRANSACTION_DOMAINS:
            scores["transaction"] += 2.0
        elif domain in _MOSTLY_TRANSACTION_DOMAINS:
            scores["transaction"] += 2.0
        elif domain in _REFERENCE_DOMAINS:
            scores["reference"] += 2.0

        # ── Signal 3: Row count ──
        if row_count > 100_000:
            scores["transaction"] += 2.0
        elif row_count > 10_000:
            scores["transaction"] += 1.0
        elif 0 < row_count <= 1_000:
            scores["reference"] += 2.0
        elif 0 < row_count <= 10_000:
            scores["reference"] += 1.0

        # ── Signal 4: Column analysis ──
        date_cols = sum(1 for c in columns if _DATE_PATTERNS.search(c))
        measure_cols = sum(1 for c in columns if _MEASURE_PATTERNS.search(c))
        fk_cols = sum(1 for c in columns if _PK_SUFFIXES.search(c))

        if date_cols >= 1 and measure_cols >= 1:
            scores["transaction"] += 2.0
        if measure_cols == 0 and fk_cols <= 2 and col_count > 0:
            scores["reference"] += 1.5
        if fk_cols >= 2 and col_count <= fk_cols + 3 and measure_cols == 0:
            scores["bridge"] += 2.0
        if col_count <= 5 and row_count > 0 and row_count <= 100:
            scores["config"] += 2.0

        # Pick the best role (tie-break: prefer reference > transaction > bridge > config)
        _TIE_BREAK = {"reference": 0.001, "bridge": 0.0005}
        adjusted = {r: s + _TIE_BREAK.get(r, 0) for r, s in scores.items()}
        best_role = max(adjusted, key=adjusted.get)  # type: ignore[arg-type]
        best_score = scores[best_role]

        if best_score < 2.0:
            return "unknown", 0.3

        # Confidence: normalize score to 0.4-0.95 range
        confidence = min(0.95, 0.4 + best_score * 0.07)
        return best_role, round(confidence, 2)

    # ── 5. Purpose summary ─────────────────────────────────────────────

    def generate_summary(self, analysis: TableAnalysis) -> str:
        """Generate plain-English purpose summary."""
        parts = []

        # Role description
        role_desc = {
            "transaction": "Transaction/fact",
            "reference": "Reference/dimension",
            "bridge": "Bridge/cross-reference",
            "config": "Configuration",
            "unknown": "Unclassified",
        }
        parts.append(f"{role_desc.get(analysis.role, 'Unclassified')} table")

        # Domain
        if analysis.domain and analysis.domain != "Unknown":
            parts.append(f"in the {analysis.domain} domain")

        # Column info
        if analysis.column_count > 0:
            parts.append(f"with {analysis.column_count} columns")

        # Key info
        if analysis.natural_keys:
            key_str = ", ".join(analysis.natural_keys[:3])
            parts.append(f"(keys: {key_str})")

        # Row count
        if analysis.row_count > 0:
            if analysis.row_count >= 1_000_000:
                parts.append(f"containing {analysis.row_count:,.0f} rows")
            elif analysis.row_count >= 1_000:
                parts.append(f"containing {analysis.row_count:,} rows")
            else:
                parts.append(f"containing {analysis.row_count} rows")

        return ". ".join([" ".join(parts)]) + "."

    # ── 6. Analyze all — orchestrator ──────────────────────────────────

    def analyze_all(self, table_names: List[str], ai_advisor: Optional[Any] = None) -> EnhancedClassification:
        """Run full analysis on all tables.

        Returns EnhancedClassification with classification dict matching
        DimensionDetector.classify_tables() output shape.
        """
        # Step 1: ERP detection with per-table matches
        erp_result = self.detect_erp_enhanced(table_names)
        per_table_matches = erp_result.get("per_table_matches", {})
        erp_type = erp_result.get("erp_type", self.erp_type or "")

        # Step 2: Analyze each table
        analyses: Dict[str, TableAnalysis] = {}
        for table in table_names:
            erp_match = per_table_matches.get(table)
            columns = self._columns_by_table.get(table, [])
            profile = self.table_profiles.get(table, {})

            analysis = TableAnalysis(table_name=table)
            analysis.erp_prefix = (erp_match or {}).get("prefix", "")
            analysis.erp_domain = (erp_match or {}).get("domain", "")
            analysis.column_count = len(columns)
            analysis.row_count = profile.get("row_count", 0) or 0

            # Column flags
            analysis.has_date_columns = any(_DATE_PATTERNS.search(c) for c in columns)
            analysis.has_measure_columns = any(_MEASURE_PATTERNS.search(c) for c in columns)
            analysis.fk_like_column_count = sum(1 for c in columns if _PK_SUFFIXES.search(c))

            # Domain
            analysis.domain = self.infer_domain(table, erp_match)

            # Natural keys
            analysis.natural_keys = self.detect_natural_keys(table)

            # Role (pass pre-computed domain to avoid re-inferring without ERP match)
            analysis.role, analysis.confidence = self.classify_role(table, domain=analysis.domain)

            # Purpose summary
            analysis.purpose_summary = self.generate_summary(analysis)

            analyses[table] = analysis

        # Step 3: Build output
        result = EnhancedClassification(erp_type=erp_type)

        # Role → classification mapping
        _role_to_classification = {
            "transaction": "fact",
            "reference": "dimension",
            "bridge": "bridge",
            "config": "unknown",
            "unknown": "unknown",
        }

        for table, analysis in analyses.items():
            result.classification[table] = _role_to_classification.get(analysis.role, "unknown")
            result.table_analyses[table] = analysis.to_dict()
            result.natural_keys[table] = analysis.natural_keys
            result.purpose_summaries[table] = analysis.purpose_summary

            # Domain groups
            domain = analysis.domain
            result.domain_groups.setdefault(domain, []).append(table)

        logger.info(
            "TableAnalyzer classified %d tables: %d fact, %d dimension, %d bridge, %d unknown",
            len(analyses),
            sum(1 for v in result.classification.values() if v == "fact"),
            sum(1 for v in result.classification.values() if v == "dimension"),
            sum(1 for v in result.classification.values() if v == "bridge"),
            sum(1 for v in result.classification.values() if v == "unknown"),
        )

        # AI enrichment (additive only)
        if ai_advisor and ai_advisor.available:
            try:
                enrichment = ai_advisor.enhance_table_analysis(
                    tables=result.to_dict(),
                    column_metadata=self._columns_by_table,
                )
                if enrichment:
                    result.table_analyses["_ai_advisor"] = enrichment
            except Exception:
                logger.debug("AI advisor enrichment failed for analyze_all", exc_info=True)

        return result
