"""Skill generator — auto-generate reusable skill prompts from CORE artifacts."""
from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from .constants import ToolClassification
from .contracts import (
    GovernanceDecision,
    LogicArtifact,
    MeetingNotesAnalysis,
    ProposedModel,
    ReportAnalysis,
    SkillManifest,
)


# ---------------------------------------------------------------------------
# Skill template
# ---------------------------------------------------------------------------

_SKILL_TEMPLATE = """\
You are a **DataBridge BLCE Business Logic Analyst** specializing in the {domain} domain.
This skill was auto-generated by the BLCE Governance Engine from CORE-classified business logic.

## Domain: {domain}

## Business Measures

{measures_section}

## Reporting Grain

{grain_section}

## Key Filters

{filters_section}

## Source Dependencies

{deps_section}

{extra_sections}## How to Use This Skill

When a user asks about {domain}-related metrics or reports:
1. Reference the measures above for correct calculation definitions
2. Ensure the grain columns are present in any query
3. Apply the standard filters where relevant
4. Use the source tables listed as dependencies

## DataBridge Tools Available

- `blce_parse_sql_logic` — Parse SQL to verify measure calculations
- `blce_normalize_measures` — Check for measure consistency
- `blce_discover_cross_references` — Map across systems
- `blce_collect_evidence_sample` — Pull sample data for validation
- `blce_classify_artifact` — Verify governance classification

---
*Generated: {generated_at} | Artifact: {artifact_id} | Classification: CORE*
"""


class SkillGenerator:
    """Generate reusable skill prompts from CORE-classified artifacts.

    Only CORE artifacts produce skills. CUSTOM and CANDIDATE artifacts
    are skipped — they need more client data or governance review first.
    """

    def __init__(self, skills_dir: str = "skills"):
        self.skills_dir = skills_dir

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def generate_skill(
        self,
        artifact: LogicArtifact,
        decision: GovernanceDecision,
        *,
        client_id: str = "",
        domain: str = "",
        report_analyses: Optional[List[ReportAnalysis]] = None,
        notes_analyses: Optional[List[MeetingNotesAnalysis]] = None,
        proposed_model: Optional[ProposedModel] = None,
    ) -> Optional[SkillManifest]:
        """Generate a skill prompt from a CORE artifact.

        Returns None if the artifact is not CORE-classified.
        """
        if decision.classification != ToolClassification.CORE:
            return None

        # Infer domain from artifact
        if not domain:
            domain = self._infer_domain(artifact)

        # Build sections
        measures_section = self._build_measures_section(artifact)
        grain_section = self._build_grain_section(artifact)
        filters_section = self._build_filters_section(artifact)
        deps_section = self._build_deps_section(artifact)

        # Build extra sections from enriched context (P2.3)
        extra_parts: List[str] = []
        rules_sec = self._build_business_rules_section(notes_analyses)
        if rules_sec:
            extra_parts.append(rules_sec)
        kpi_sec = self._build_kpi_definitions_section(report_analyses, notes_analyses)
        if kpi_sec:
            extra_parts.append(kpi_sec)
        comp_sec = self._build_comparisons_section(report_analyses)
        if comp_sec:
            extra_parts.append(comp_sec)
        model_sec = self._build_model_context_section(proposed_model, domain)
        if model_sec:
            extra_parts.append(model_sec)
        extra_sections = "\n".join(extra_parts) + "\n" if extra_parts else ""

        # Render template
        content = _SKILL_TEMPLATE.format(
            domain=domain,
            measures_section=measures_section,
            grain_section=grain_section,
            filters_section=filters_section,
            deps_section=deps_section,
            extra_sections=extra_sections,
            generated_at=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
            artifact_id=artifact.artifact_id,
        )

        # Build skill name
        skill_name = self._make_skill_name(domain, artifact)
        skill_path = f"{self.skills_dir}/blce_{skill_name}.txt"

        # Collect covered items
        measures_covered = [
            m.alias or m.name for m in artifact.objects.measures if m.name or m.alias
        ]
        filters_covered = [
            f.column for f in artifact.objects.filters if f.column
        ]
        grain_cols = [g.column for g in artifact.objects.grain_columns if g.column]
        dep_names = [d.name for d in artifact.objects.dependencies if d.name]

        return SkillManifest(
            artifact_id=artifact.artifact_id,
            client_id=client_id or artifact.client_id,
            skill_name=skill_name,
            skill_path=skill_path,
            content=content,
            classification=ToolClassification.CORE,
            measures_covered=measures_covered,
            filters_covered=filters_covered,
            grain_columns=grain_cols,
            dependencies=dep_names,
        )

    def generate_batch(
        self,
        artifacts: List[LogicArtifact],
        decisions: List[GovernanceDecision],
        *,
        client_id: str = "",
        report_analyses: Optional[List[ReportAnalysis]] = None,
        notes_analyses: Optional[List[MeetingNotesAnalysis]] = None,
        proposed_model: Optional[ProposedModel] = None,
    ) -> List[SkillManifest]:
        """Generate skills for all CORE artifacts in a batch.

        Pairs artifacts with decisions by artifact_id.
        """
        # Build lookup: artifact_id → decision
        decision_map: Dict[str, GovernanceDecision] = {}
        for d in decisions:
            decision_map[d.artifact_id] = d

        skills: List[SkillManifest] = []
        for art in artifacts:
            decision = decision_map.get(art.artifact_id)
            if not decision:
                continue
            skill = self.generate_skill(
                art, decision,
                client_id=client_id,
                report_analyses=report_analyses,
                notes_analyses=notes_analyses,
                proposed_model=proposed_model,
            )
            if skill is not None:
                skills.append(skill)

        return skills

    def write_skill(self, manifest: SkillManifest) -> str:
        """Write a skill to disk and return the file path."""
        path = Path(manifest.skill_path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(manifest.content, encoding="utf-8")
        return str(path)

    def list_generated_skills(self) -> List[Dict[str, Any]]:
        """List all BLCE-generated skill files in the skills directory."""
        skills_path = Path(self.skills_dir)
        if not skills_path.exists():
            return []

        results = []
        for f in sorted(skills_path.glob("blce_*.txt")):
            content = f.read_text(encoding="utf-8")
            # Extract metadata from footer
            artifact_id = ""
            generated_at = ""
            for line in content.splitlines():
                if "Artifact:" in line:
                    match = re.search(r"Artifact:\s*(\S+)", line)
                    if match:
                        artifact_id = match.group(1)
                if "Generated:" in line:
                    match = re.search(r"Generated:\s*(.+?)\s*\|", line)
                    if match:
                        generated_at = match.group(1).strip()

            results.append({
                "file": str(f),
                "name": f.stem.replace("blce_", ""),
                "artifact_id": artifact_id,
                "generated_at": generated_at,
                "size_bytes": f.stat().st_size,
            })
        return results

    # ------------------------------------------------------------------
    # Extra section builders (P2.3)
    # ------------------------------------------------------------------

    @staticmethod
    def _build_business_rules_section(
        notes_analyses: Optional[List[MeetingNotesAnalysis]],
    ) -> str:
        """Build a Business Rules section from meeting notes."""
        if not notes_analyses:
            return ""
        rules: List[str] = []
        for na in notes_analyses:
            for rule in na.business_rules:
                if rule and rule not in rules:
                    rules.append(rule)
        if not rules:
            return ""
        lines = ["## Business Rules", ""]
        for r in rules:
            lines.append(f"- {r}")
        lines.append("")
        return "\n".join(lines)

    @staticmethod
    def _build_kpi_definitions_section(
        report_analyses: Optional[List[ReportAnalysis]],
        notes_analyses: Optional[List[MeetingNotesAnalysis]],
    ) -> str:
        """Build a KPI Definitions section from reports and meeting notes."""
        kpis: Dict[str, str] = {}  # name → formula/description
        for ra in (report_analyses or []):
            for kpi in ra.kpis:
                key = kpi.business_name or kpi.name
                if key and key not in kpis:
                    desc = kpi.formula or kpi.unit or kpi.name
                    kpis[key] = desc
        for na in (notes_analyses or []):
            for kpi in na.kpis:
                key = kpi.business_name or kpi.name
                if key and key not in kpis:
                    desc = kpi.formula or kpi.unit or kpi.name
                    kpis[key] = desc
        if not kpis:
            return ""
        lines = ["## KPI Definitions", ""]
        for name, formula in kpis.items():
            lines.append(f"- **{name}**: `{formula}`")
        lines.append("")
        return "\n".join(lines)

    @staticmethod
    def _build_comparisons_section(
        report_analyses: Optional[List[ReportAnalysis]],
    ) -> str:
        """Build a Comparisons section from report analyses."""
        if not report_analyses:
            return ""
        comparisons: List[str] = []
        for ra in report_analyses:
            for comp in ra.comparisons:
                if comp and comp not in comparisons:
                    comparisons.append(comp)
        if not comparisons:
            return ""
        lines = ["## Comparison Patterns", ""]
        for c in comparisons:
            lines.append(f"- {c}")
        lines.append("")
        return "\n".join(lines)

    @staticmethod
    def _build_model_context_section(
        proposed_model: Optional[ProposedModel],
        domain: str,
    ) -> str:
        """Build a Model Context section with related facts, conformed dims, hierarchies."""
        if not proposed_model:
            return ""
        domain_lower = domain.lower()

        # Find related facts (name contains domain keywords)
        related_facts = [
            f.name for f in proposed_model.facts
            if domain_lower in f.name.lower() or domain_lower in f.business_name.lower()
        ]

        # Conformed dimensions
        conformed = proposed_model.bus_matrix.get("conformed_dimensions", [])

        # Dimensions with hierarchies
        hierarchy_dims = [
            f"{d.name} ({' → '.join(d.hierarchy_levels)})"
            for d in proposed_model.dimensions
            if d.hierarchy_levels
        ]

        if not related_facts and not conformed and not hierarchy_dims:
            return ""

        lines = ["## Model Context", ""]
        if related_facts:
            lines.append(f"Related facts: {', '.join(related_facts)}")
        if conformed:
            lines.append(f"Conformed dimensions: {', '.join(conformed)}")
        if hierarchy_dims:
            lines.append(f"Hierarchies: {', '.join(hierarchy_dims)}")
        lines.append("")
        return "\n".join(lines)

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

    def _infer_domain(self, artifact: LogicArtifact) -> str:
        """Infer the business domain from artifact content."""
        # Try source_name first
        if artifact.source_name:
            return self._clean_domain_name(artifact.source_name)

        # Try measure names
        measure_names = [
            (m.alias or m.name or "").lower()
            for m in artifact.objects.measures
        ]
        if measure_names:
            # Use the first meaningful measure to infer domain
            name = measure_names[0]
            # Strip common prefixes
            for prefix in ("total_", "sum_", "count_", "avg_", "net_", "gross_"):
                if name.startswith(prefix):
                    name = name[len(prefix):]
                    break
            return self._clean_domain_name(name)

        # Try dependency names
        dep_names = [d.name for d in artifact.objects.dependencies if d.name]
        if dep_names:
            return self._clean_domain_name(dep_names[0])

        return "general"

    def _clean_domain_name(self, raw: str) -> str:
        """Clean a raw string into a domain name."""
        # Remove common suffixes
        name = raw.lower().strip()
        for suffix in ("_fact", "_dim", "_table", "_view", ".sql", ".py", ".xlsx"):
            if name.endswith(suffix):
                name = name[: -len(suffix)]
        # Replace separators
        name = re.sub(r"[^a-z0-9]+", "_", name).strip("_")
        return name or "general"

    def _make_skill_name(self, domain: str, artifact: LogicArtifact) -> str:
        """Create a filesystem-safe skill name."""
        base = re.sub(r"[^a-z0-9_]+", "_", domain.lower()).strip("_")
        return base or "logic"

    def _build_measures_section(self, artifact: LogicArtifact) -> str:
        """Build the measures section of the skill prompt."""
        if not artifact.objects.measures:
            return "No measures extracted."

        lines = []
        for m in artifact.objects.measures:
            name = m.alias or m.name or "unnamed"
            expr = m.expression or f"{m.aggregation.value}(...)"
            cols = ", ".join(m.source_columns) if m.source_columns else "n/a"
            lines.append(f"- **{name}**: `{expr}` (source: {cols})")
        return "\n".join(lines)

    def _build_grain_section(self, artifact: LogicArtifact) -> str:
        """Build the grain section of the skill prompt."""
        if not artifact.objects.grain_columns:
            return "No grain columns detected (flat aggregation)."

        cols = [g.column for g in artifact.objects.grain_columns]
        return f"Reporting grain: {' × '.join(cols)}"

    def _build_filters_section(self, artifact: LogicArtifact) -> str:
        """Build the filters section of the skill prompt."""
        if not artifact.objects.filters:
            return "No standard filters detected."

        lines = []
        for f in artifact.objects.filters:
            clause = f.source_clause or f"{f.column} {f.operator.value} {f.value}"
            lines.append(f"- `{clause}`")
        return "\n".join(lines)

    def _build_deps_section(self, artifact: LogicArtifact) -> str:
        """Build the dependencies section of the skill prompt."""
        if not artifact.objects.dependencies:
            return "No source tables detected."

        lines = []
        for d in artifact.objects.dependencies:
            parts = [p for p in [d.database, d.schema_name, d.name] if p]
            fqn = ".".join(parts) if len(parts) > 1 else d.name
            lines.append(f"- `{fqn}` ({d.dep_type})")
        return "\n".join(lines)

