"""DM Spec Auto-Discovery — generate TableSpec from relationships + classification.

Produces draft DM specifications by examining:
- Classification (dimension/fact/bridge) from DimensionDetector
- Relationship metadata (FK columns, overlap scores)
- Column metadata (data types, naming patterns)
- ERP type for SCD2 boilerplate and naming conventions

The generated specs are meant to be reviewed and refined before loading.
"""

from __future__ import annotations

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

from .dm_loader import ColumnSpec, TableSpec

logger = logging.getLogger(__name__)

# Standard SCD2 boilerplate columns appended to every table
_SCD2_COLUMNS = [
    ColumnSpec(alias="SOURCE_SYSTEM", kind="const", const_value="'{erp}'::STRING"),
    ColumnSpec(alias="LOAD_RUN_ID", kind="const", const_value="'{run_id}'::STRING"),
    ColumnSpec(alias="EFFECTIVE_FROM_TS", kind="ts", source_names=["LASTUPDATED", "_FIVETRAN_SYNCED", "MODIFIED_DATE"]),
    ColumnSpec(alias="EFFECTIVE_TO_TS", kind="null_ts"),
    ColumnSpec(alias="IS_CURRENT", kind="const", const_value="TRUE::BOOLEAN"),
]

# Patterns for identifying PK columns
_PK_SUFFIXES = ("_id", "_hid", "_tid", "_key", "_code", "_num", "id", "hid", "tid")
_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)


class DMSpecGenerator:
    """Generate draft DM specifications from discovery metadata."""

    def __init__(self):
        self.last_ai_enrichment: Optional[Dict[str, Any]] = None

    def generate(
        self,
        relationships: List[Dict[str, Any]],
        classification: Dict[str, str],
        column_metadata: Optional[Dict[str, List[Dict[str, Any]]]] = None,
        erp_type: str = "UNKNOWN",
        run_id: str = "GENERATED",
        ai_advisor: Optional[Any] = None,
    ) -> List[TableSpec]:
        """Generate TableSpec objects from discovery results.

        Args:
            relationships: Relationship dicts with source_table, target_table, source_column, target_column.
            classification: Table → classification mapping from DimensionDetector.
            column_metadata: Optional dict of table_name → list of column dicts
                            (each with 'name', 'data_type', 'ordinal_position').
            erp_type: ERP system name for SOURCE_SYSTEM constant.
            run_id: Run ID for LOAD_RUN_ID constant.

        Returns:
            List of TableSpec objects (dimensions first, then facts).
        """
        dims = sorted(t for t, c in classification.items() if c == "dimension")
        facts = sorted(t for t, c in classification.items() if c == "fact")

        # Build FK lookup: source_table → [(source_col, target_table, target_col)]
        fk_map: Dict[str, List[Dict[str, str]]] = {}
        for rel in relationships:
            src_t = rel.get("source_table", "")
            if src_t:
                fk_map.setdefault(src_t, []).append({
                    "source_column": rel.get("source_column", ""),
                    "target_table": rel.get("target_table", ""),
                    "target_column": rel.get("target_column", ""),
                })

        # Build dim PK lookup: dim_table → primary key column
        dim_pks: Dict[str, str] = {}
        for dim in dims:
            cols = column_metadata.get(dim, []) if column_metadata else []
            pk = self._guess_pk(dim, cols)
            if pk:
                dim_pks[dim] = pk

        specs: List[TableSpec] = []

        # Generate dimension specs
        for dim in dims:
            spec = self._gen_dim_spec(dim, column_metadata, erp_type, run_id)
            if spec:
                specs.append(spec)

        # Generate fact specs
        for fact in facts:
            spec = self._gen_fact_spec(fact, column_metadata, fk_map, dims, dim_pks, erp_type, run_id)
            if spec:
                specs.append(spec)

        # AI enrichment (stored on instance, not in return value)
        self.last_ai_enrichment = None
        if ai_advisor and ai_advisor.available:
            try:
                enrichment = ai_advisor.enhance_spec_generation(
                    specs=[s.to_dict() for s in specs],
                    classification=classification,
                    column_metadata=column_metadata,
                )
                if enrichment:
                    self.last_ai_enrichment = enrichment
            except Exception:
                logger.debug("AI advisor enrichment failed for generate", exc_info=True)

        return specs

    def _gen_dim_spec(
        self,
        table: str,
        column_metadata: Optional[Dict[str, List[Dict[str, Any]]]],
        erp_type: str,
        run_id: str,
    ) -> Optional[TableSpec]:
        """Generate a dimension table spec."""
        cols = column_metadata.get(table, []) if column_metadata else []
        target = f"DIM_{self._clean_name(table)}"

        columns: List[ColumnSpec] = []
        for col in cols:
            name = col.get("name", col.get("COLUMN_NAME", ""))
            dtype = col.get("data_type", col.get("DATA_TYPE", "")).upper()
            kind = self._infer_kind(name, dtype)
            columns.append(ColumnSpec(alias=name, kind=kind, source_names=[name]))

        # Add SCD2 boilerplate
        columns.extend(self._scd2_columns(erp_type, run_id))

        return TableSpec(
            target_table=target,
            source_tables=[table],
            is_transaction=False,
            columns=columns,
        )

    def _gen_fact_spec(
        self,
        table: str,
        column_metadata: Optional[Dict[str, List[Dict[str, Any]]]],
        fk_map: Dict[str, List[Dict[str, str]]],
        dims: List[str],
        dim_pks: Dict[str, str],
        erp_type: str,
        run_id: str,
    ) -> Optional[TableSpec]:
        """Generate a fact table spec."""
        cols = column_metadata.get(table, []) if column_metadata else []
        target = f"FCT_{self._clean_name(table)}"

        columns: List[ColumnSpec] = []
        fk_cols: Set[str] = set()

        # Add FK columns referencing dimensions as date_key or num
        for fk in fk_map.get(table, []):
            src_col = fk["source_column"]
            tgt_table = fk["target_table"]
            if tgt_table in dims:
                fk_cols.add(src_col)
                if _DATE_PATTERNS.search(src_col):
                    columns.append(ColumnSpec(alias=f"{src_col}_KEY", kind="date_key", source_names=[src_col]))
                else:
                    columns.append(ColumnSpec(alias=src_col, kind="num", source_names=[src_col]))

        # Add remaining columns
        for col in cols:
            name = col.get("name", col.get("COLUMN_NAME", ""))
            if name in fk_cols:
                continue  # Already handled as FK
            dtype = col.get("data_type", col.get("DATA_TYPE", "")).upper()
            kind = self._infer_kind(name, dtype)
            columns.append(ColumnSpec(alias=name, kind=kind, source_names=[name]))

        # Add SCD2 boilerplate
        columns.extend(self._scd2_columns(erp_type, run_id))

        return TableSpec(
            target_table=target,
            source_tables=[table],
            is_transaction=True,
            columns=columns,
        )

    def _guess_pk(self, table: str, cols: List[Dict[str, Any]]) -> Optional[str]:
        """Guess the primary key column for a table."""
        for col in cols:
            name = col.get("name", col.get("COLUMN_NAME", "")).lower()
            if name.endswith(("_id", "_hid", "_tid")):
                return col.get("name", col.get("COLUMN_NAME", ""))
        # Fallback: first column with 'id' suffix
        for col in cols:
            name = col.get("name", col.get("COLUMN_NAME", "")).lower()
            if "id" in name:
                return col.get("name", col.get("COLUMN_NAME", ""))
        return None

    @staticmethod
    def _infer_kind(col_name: str, data_type: str) -> str:
        """Infer column kind from name and data type."""
        name_lower = col_name.lower()

        if _DATE_PATTERNS.search(name_lower):
            return "ts"
        if _MEASURE_PATTERNS.search(name_lower):
            return "num"
        if name_lower.endswith(_PK_SUFFIXES):
            return "num"

        if any(t in data_type for t in ("NUMBER", "INT", "FLOAT", "DECIMAL", "NUMERIC")):
            return "num"
        if any(t in data_type for t in ("DATE", "TIME", "TIMESTAMP")):
            return "ts"
        if "BOOL" in data_type:
            return "bool"

        return "str"

    @staticmethod
    def _clean_name(table: str) -> str:
        """Strip common ERP prefixes to create clean target name."""
        upper = table.upper()
        # Strip 2-letter ERP prefixes like AA, GL, RV followed by MAS/TXN
        for pattern in [r"^[A-Z]{2}(MAS|TXN|FCT|DIM)", r"^[A-Z]{2}"]:
            m = re.match(pattern, upper)
            if m:
                stripped = upper[m.end():]
                if stripped:
                    return stripped
        return upper

    @staticmethod
    def _scd2_columns(erp_type: str, run_id: str) -> List[ColumnSpec]:
        """Return SCD2 boilerplate columns with substituted values."""
        return [
            ColumnSpec(alias="SOURCE_SYSTEM", kind="const", const_value=f"'{erp_type}'::STRING"),
            ColumnSpec(alias="LOAD_RUN_ID", kind="const", const_value=f"'{run_id}'::STRING"),
            ColumnSpec(alias="EFFECTIVE_FROM_TS", kind="ts", source_names=["LASTUPDATED", "_FIVETRAN_SYNCED", "MODIFIED_DATE"]),
            ColumnSpec(alias="EFFECTIVE_TO_TS", kind="null_ts"),
            ColumnSpec(alias="IS_CURRENT", kind="const", const_value="TRUE::BOOLEAN"),
        ]

    def to_json(self, specs: List[TableSpec], path: Optional[str | Path] = None) -> str:
        """Serialize specs to JSON. Optionally write to file."""
        from .dm_loader import DMSpecLoader
        return DMSpecLoader.to_json(specs, path)
