"""BLCE Semantic Layer Builder — generate semantic layer definitions.

Produces Cortex Analyst-compatible semantic layer entries for
all measures and dimensions in a ProposedModel.  Includes exporters
for Cortex Analyst YAML, Tableau TDS, and Looker LookML.
"""
from __future__ import annotations

import logging
import xml.etree.ElementTree as ET
from typing import Any, Dict, List

from .contracts import (
    ProposedDimension,
    ProposedFact,
    ProposedModel,
    SemanticLayerDefinition,
)

logger = logging.getLogger(__name__)


class SemanticLayerBuilder:
    """Build semantic layer definitions from a ProposedModel."""

    def build_semantic_layer(
        self, model: ProposedModel
    ) -> List[SemanticLayerDefinition]:
        """Generate semantic layer entries for all model objects.

        Args:
            model: ProposedModel with dimensions, facts, and measures.

        Returns:
            List of SemanticLayerDefinition entries.
        """
        definitions: List[SemanticLayerDefinition] = []

        # Dimensions
        for dim in model.dimensions:
            definitions.extend(self._dim_entries(dim))

        # Facts — measures
        for fact in model.facts:
            definitions.extend(self._fact_entries(fact))

        # Measure dictionary entries
        for name, formula in model.measure_dictionary.items():
            # Only add if not already covered by fact measures
            if not any(d.name == name for d in definitions):
                definitions.append(
                    SemanticLayerDefinition(
                        entry_type="measure",
                        name=name,
                        business_name=name.replace("_", " ").title(),
                        definition=formula,
                        aggregation="custom",
                    )
                )

        return definitions

    def to_cortex_config(
        self, definitions: List[SemanticLayerDefinition]
    ) -> Dict[str, Any]:
        """Convert definitions to Cortex Analyst compatible format.

        Returns:
            Dict with tables, dimensions, measures, time_dimensions.
        """
        tables: List[Dict[str, str]] = []
        dims: List[Dict[str, Any]] = []
        measures: List[Dict[str, Any]] = []
        time_dims: List[Dict[str, Any]] = []

        seen_tables: set = set()

        for d in definitions:
            # Track tables
            if d.table and d.table not in seen_tables:
                seen_tables.add(d.table)
                tables.append({"name": d.table, "description": ""})

            if d.entry_type == "dimension":
                dims.append({
                    "name": d.name,
                    "synonyms": [d.business_name] if d.business_name else [],
                    "description": d.definition or d.business_name,
                    "expr": d.column or d.name,
                    "data_type": d.data_type or "TEXT",
                })
            elif d.entry_type == "time_dimension":
                time_dims.append({
                    "name": d.name,
                    "synonyms": [d.business_name] if d.business_name else [],
                    "description": d.definition or d.business_name,
                    "expr": d.column or d.name,
                    "data_type": d.data_type or "DATE",
                })
            elif d.entry_type == "measure":
                measures.append({
                    "name": d.name,
                    "synonyms": [d.business_name] if d.business_name else [],
                    "description": d.definition or d.business_name,
                    "expr": d.column or d.name,
                    "data_type": d.data_type or "NUMBER",
                    "default_aggregation": d.aggregation or "sum",
                })

        return {
            "name": "auto_generated_semantic_model",
            "tables": tables,
            "dimensions": dims,
            "time_dimensions": time_dims,
            "measures": measures,
        }

    # ------------------------------------------------------------------
    # BI Tool Exporters (P3.2)
    # ------------------------------------------------------------------

    def export_cortex_yaml(
        self,
        model: ProposedModel,
        model_name: str = "auto_semantic_model",
        database: str = "",
        schema: str = "",
    ) -> str:
        """Export model as Cortex Analyst YAML.

        Args:
            model: ProposedModel to export.
            model_name: Name of the semantic model.
            database: Snowflake database for base_table references.
            schema: Snowflake schema for base_table references.

        Returns:
            YAML string matching Cortex Analyst specification.
        """
        import yaml

        entries = self.build_semantic_layer(model)
        cortex = self.to_cortex_config(entries)

        # Build per-table structure matching Cortex Analyst spec
        table_defs = []
        table_names = {t["name"] for t in cortex.get("tables", [])}

        for tname in sorted(table_names):
            base = f"{database}.{schema}.{tname}" if database and schema else tname
            t_dims = [
                d for d in cortex.get("dimensions", [])
                if any(e.table == tname and e.name == d["name"] for e in entries)
            ]
            t_time = [
                d for d in cortex.get("time_dimensions", [])
                if any(e.table == tname and e.name == d["name"] for e in entries)
            ]
            t_meas = [
                m for m in cortex.get("measures", [])
                if any(e.table == tname and e.name == m["name"] for e in entries)
            ]

            tdef: Dict[str, Any] = {"name": tname, "base_table": base}
            if t_dims:
                tdef["dimensions"] = t_dims
            if t_time:
                tdef["time_dimensions"] = t_time
            if t_meas:
                tdef["measures"] = t_meas
            table_defs.append(tdef)

        doc = {"name": model_name, "tables": table_defs}
        return yaml.dump(doc, default_flow_style=False, sort_keys=False)

    def export_tableau_tds(
        self,
        model: ProposedModel,
        connection_name: str = "snowflake",
        schema: str = "",
    ) -> str:
        """Export model as Tableau TDS XML.

        Args:
            model: ProposedModel to export.
            connection_name: Tableau connection name.
            schema: Schema for table references.

        Returns:
            TDS XML string.
        """
        entries = self.build_semantic_layer(model)

        root = ET.Element("datasource", attrib={
            "formatted-name": "databridge_semantic_model",
            "inline": "true",
        })
        conn = ET.SubElement(root, "connection", attrib={
            "class": "snowflake",
            "schema": schema,
            "server": connection_name,
        })

        # Group by table
        tables: Dict[str, List] = {}
        for e in entries:
            tname = e.table or "unknown"
            tables.setdefault(tname, []).append(e)

        for tname, cols in sorted(tables.items()):
            relation = ET.SubElement(conn, "relation", attrib={
                "name": tname, "table": f"[{schema}].[{tname}]" if schema else f"[{tname}]",
                "type": "table",
            })

            for col in cols:
                attribs: Dict[str, str] = {"name": f"[{col.column or col.name}]"}

                if col.entry_type == "measure":
                    attribs["datatype"] = "real"
                    attribs["role"] = "measure"
                    attribs["default-aggregation"] = col.aggregation or "sum"
                elif col.entry_type == "time_dimension":
                    attribs["datatype"] = "date"
                    attribs["role"] = "dimension"
                else:
                    attribs["datatype"] = "string"
                    attribs["role"] = "dimension"

                ET.SubElement(root, "column", attrib=attribs)

        ET.indent(root, space="  ")
        return ET.tostring(root, encoding="unicode", xml_declaration=True)

    def export_looker_lookml(
        self,
        model: ProposedModel,
        connection_name: str = "snowflake",
    ) -> str:
        """Export model as Looker LookML.

        Args:
            model: ProposedModel to export.
            connection_name: Looker connection name.

        Returns:
            LookML string with view and explore blocks.
        """
        entries = self.build_semantic_layer(model)
        lines: List[str] = []
        lines.append(f"connection: \"{connection_name}\"")
        lines.append("")

        # Group entries by table for view blocks
        tables: Dict[str, List] = {}
        for e in entries:
            tname = e.table or "unknown"
            tables.setdefault(tname, []).append(e)

        for tname, cols in sorted(tables.items()):
            lines.append(f"view: {tname.lower()} {{")
            lines.append(f"  sql_table_name: {tname} ;;")
            lines.append("")

            for col in cols:
                col_name = (col.column or col.name).lower()
                biz = col.business_name or col_name.replace("_", " ").title()

                if col.entry_type == "measure":
                    agg = col.aggregation or "sum"
                    lines.append(f"  measure: {col_name} {{")
                    lines.append(f"    type: {agg}")
                    lines.append(f"    sql: ${{TABLE}}.{col.column or col.name} ;;")
                    lines.append(f"    label: \"{biz}\"")
                    lines.append("  }")
                    lines.append("")
                else:
                    dtype = "date" if col.entry_type == "time_dimension" else "string"
                    lines.append(f"  dimension: {col_name} {{")
                    lines.append(f"    type: {dtype}")
                    lines.append(f"    sql: ${{TABLE}}.{col.column or col.name} ;;")
                    lines.append(f"    label: \"{biz}\"")
                    lines.append("  }")
                    lines.append("")

            lines.append("}")
            lines.append("")

        # Explore blocks — one per fact with joins to dimensions
        for fact in model.facts:
            fname = fact.name.lower()
            lines.append(f"explore: {fname} {{")
            for fk in fact.dimension_fks:
                dim_name = fk.lower()
                # Strip _SK/_FK suffix for join name
                join_name = dim_name.replace("_sk", "").replace("_fk", "")
                lines.append(f"  join: {join_name} {{")
                lines.append(f"    relationship: many_to_one")
                lines.append(f"    sql_on: ${{{fname}}}.{fk} = ${{{join_name}}}.{fk} ;;")
                lines.append("  }")
            lines.append("}")
            lines.append("")

        return "\n".join(lines)

    def export_all(
        self,
        model: ProposedModel,
        format: str = "cortex",
        **kwargs: Any,
    ) -> str:
        """Dispatch to the appropriate exporter.

        Args:
            model: ProposedModel to export.
            format: One of "cortex", "tableau", "looker".
            **kwargs: Passed to the specific exporter.

        Returns:
            Formatted string (YAML, XML, or LookML).
        """
        exporters = {
            "cortex": self.export_cortex_yaml,
            "tableau": self.export_tableau_tds,
            "looker": self.export_looker_lookml,
        }
        exporter = exporters.get(format)
        if not exporter:
            raise ValueError(f"Unknown format '{format}'. Choose: {', '.join(exporters)}")
        return exporter(model, **kwargs)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _dim_entries(
        self, dim: ProposedDimension
    ) -> List[SemanticLayerDefinition]:
        """Generate semantic entries for a dimension's attributes."""
        entries: List[SemanticLayerDefinition] = []

        # Natural key
        nk = dim.natural_key or f"{dim.name.replace('DIM_', '')}_ID"
        entries.append(
            SemanticLayerDefinition(
                entry_type="dimension",
                name=nk,
                business_name=dim.business_name,
                definition=f"Natural key of {dim.business_name}",
                table=dim.name,
                column=nk,
                data_type="TEXT",
            )
        )

        # Attributes
        for attr in dim.attributes:
            attr_upper = attr.upper()
            is_date = any(p in attr_upper for p in ("DATE", "_DT", "_TS"))
            entry_type = "time_dimension" if is_date else "dimension"
            data_type = "DATE" if is_date else "TEXT"

            entries.append(
                SemanticLayerDefinition(
                    entry_type=entry_type,
                    name=attr_upper,
                    business_name=attr.replace("_", " ").title(),
                    definition=f"{dim.business_name} attribute",
                    table=dim.name,
                    column=attr_upper,
                    data_type=data_type,
                )
            )

        return entries

    def _fact_entries(
        self, fact: ProposedFact
    ) -> List[SemanticLayerDefinition]:
        """Generate semantic entries for a fact's measures."""
        entries: List[SemanticLayerDefinition] = []

        for m in fact.measures:
            entries.append(
                SemanticLayerDefinition(
                    entry_type="measure",
                    name=m.upper(),
                    business_name=m.replace("_", " ").title(),
                    definition=f"Measure from {fact.business_name}",
                    table=fact.name,
                    column=m.upper(),
                    data_type="NUMBER",
                    aggregation="sum",
                )
            )

        return entries
