"""Skill Context Injector — assembles rich context from BLCE stores.

Given a natural-language question, this module searches skill manifests,
measures, xrefs, alterations, and governance decisions to build a
``ModelContext`` that an MCP client can use to formulate answers.

Also includes ``parse_change_description`` which converts free-text
model-change descriptions into structured ``ModelChangeProposal`` objects.
"""
from __future__ import annotations

import re
from datetime import datetime
from typing import Any, Dict, List, Optional

from ..contracts import ModelChangeProposal, ModelContext

# -----------------------------------------------------------------------
# Stop words (common English words to ignore during keyword extraction)
# -----------------------------------------------------------------------

_STOP_WORDS = frozenset({
    "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
    "have", "has", "had", "do", "does", "did", "will", "would", "shall",
    "should", "may", "might", "must", "can", "could", "of", "in", "to",
    "for", "with", "on", "at", "from", "by", "about", "as", "into",
    "through", "during", "before", "after", "above", "below", "between",
    "and", "but", "or", "nor", "not", "so", "yet", "both", "either",
    "neither", "each", "every", "all", "any", "few", "more", "most",
    "other", "some", "such", "no", "only", "own", "same", "than", "too",
    "very", "just", "because", "if", "when", "where", "how", "what",
    "which", "who", "whom", "this", "that", "these", "those", "i", "me",
    "my", "we", "our", "you", "your", "he", "him", "his", "she", "her",
    "it", "its", "they", "them", "their",
})


class SkillContextInjector:
    """Assembles a ``ModelContext`` by searching across BLCE stores."""

    def __init__(
        self,
        skill_generator=None,
        alteration_engine=None,
        merger=None,
    ):
        self._skill_generator = skill_generator
        self._alteration_engine = alteration_engine
        self._merger = merger

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

    def assemble_context(
        self,
        question: str,
        client_id: str = "",
        *,
        skill_store: Optional[Dict[str, Dict[str, Any]]] = None,
        artifact_cache: Optional[Dict[str, Dict[str, Any]]] = None,
        governance_store: Optional[Dict[str, Dict[str, Any]]] = None,
    ) -> ModelContext:
        """Build a ``ModelContext`` for a natural-language *question*.

        Parameters
        ----------
        question:
            The user's question about the data model.
        client_id:
            Client identifier.
        skill_store:
            Dict of skill_id → skill manifest dicts.
        artifact_cache:
            Dict of artifact_id → artifact dicts.
        governance_store:
            Dict of decision_id → governance decision dicts.
        """
        skill_store = skill_store or {}
        artifact_cache = artifact_cache or {}
        governance_store = governance_store or {}

        keywords = self._extract_keywords(question)

        relevant_skills = self._match_skills(keywords, skill_store)
        relevant_measures = self._match_measures(keywords, artifact_cache)
        bus_matrix_excerpt = self._build_bus_matrix_excerpt(keywords, artifact_cache)

        # Dimensions from bus matrix excerpt
        relevant_dimensions: List[str] = []
        for dims in bus_matrix_excerpt.values():
            if isinstance(dims, list):
                for d in dims:
                    if isinstance(d, str) and d not in relevant_dimensions:
                        relevant_dimensions.append(d)

        # Cross-references from merger
        relevant_xrefs: List[Dict[str, Any]] = []
        if self._merger is not None:
            try:
                xrefs = self._merger.get_cross_references()
                for xr in xrefs:
                    xd = xr if isinstance(xr, dict) else (xr.model_dump() if hasattr(xr, "model_dump") else {})
                    xr_text = " ".join(str(v).lower() for v in xd.values())
                    if any(kw in xr_text for kw in keywords):
                        relevant_xrefs.append(xd)
            except (AttributeError, TypeError):
                pass

        # Alteration history
        alteration_history: List[Dict[str, Any]] = []
        if self._alteration_engine is not None:
            try:
                alts = self._alteration_engine.get_alterations()
                for alt in alts:
                    ad = alt if isinstance(alt, dict) else (alt.model_dump() if hasattr(alt, "model_dump") else {})
                    alteration_history.append(ad)
            except (AttributeError, TypeError):
                pass

        # Governance summary
        governance_summary: Dict[str, Any] = {}
        if governance_store:
            counts: Dict[str, int] = {}
            for gd in governance_store.values():
                cls = gd.get("classification", "unknown")
                if isinstance(cls, str):
                    counts[cls] = counts.get(cls, 0) + 1
                else:
                    val = getattr(cls, "value", str(cls))
                    counts[val] = counts.get(val, 0) + 1
            governance_summary = {"classification_counts": counts, "total_decisions": len(governance_store)}

        return ModelContext(
            client_id=client_id,
            question=question,
            relevant_skills=relevant_skills,
            relevant_measures=relevant_measures,
            relevant_dimensions=relevant_dimensions,
            relevant_xrefs=relevant_xrefs,
            bus_matrix_excerpt=bus_matrix_excerpt,
            alteration_history=alteration_history,
            governance_summary=governance_summary,
        )

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

    @staticmethod
    def _extract_keywords(text: str) -> List[str]:
        """Split *text* into lowercase tokens, removing stop words."""
        tokens = re.split(r"\W+", text.lower())
        return [t for t in tokens if t and t not in _STOP_WORDS]

    @staticmethod
    def _match_skills(
        keywords: List[str],
        skill_store: Dict[str, Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Return skills whose name or measures overlap with *keywords*."""
        matches: List[Dict[str, Any]] = []
        for _sid, sd in skill_store.items():
            name = (sd.get("skill_name") or "").lower()
            measures = [m.lower() for m in sd.get("measures_covered", [])]
            combined = name + " " + " ".join(measures)
            if any(kw in combined for kw in keywords):
                matches.append(sd)
        return matches

    @staticmethod
    def _match_measures(
        keywords: List[str],
        artifact_cache: Dict[str, Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Return measures whose name/alias overlaps with *keywords*."""
        matches: List[Dict[str, Any]] = []
        seen: set = set()
        for _aid, ad in artifact_cache.items():
            for m in ad.get("objects", {}).get("measures", []):
                mname = (m.get("name") or m.get("alias") or "").lower()
                if mname and mname not in seen:
                    if any(kw in mname for kw in keywords):
                        matches.append(m)
                        seen.add(mname)
        return matches

    @staticmethod
    def _build_bus_matrix_excerpt(
        keywords: List[str],
        artifact_cache: Dict[str, Dict[str, Any]],
    ) -> Dict[str, Any]:
        """Build a bus-matrix-style excerpt from matching artifacts.

        Returns a dict mapping fact/source names → list of grain columns
        that appear across matched artifacts.
        """
        excerpt: Dict[str, List[str]] = {}
        for _aid, ad in artifact_cache.items():
            source_name = ad.get("source_name") or ad.get("artifact_id", "")
            grains = ad.get("objects", {}).get("grain_columns", [])
            measures = ad.get("objects", {}).get("measures", [])
            combined_text = source_name.lower()
            for m in measures:
                combined_text += " " + (m.get("name") or m.get("alias") or "").lower()
            if any(kw in combined_text for kw in keywords):
                grain_names = []
                for g in grains:
                    gname = g.get("column") or g.get("alias") or ""
                    if gname:
                        grain_names.append(gname)
                if grain_names:
                    excerpt[source_name] = grain_names
        return excerpt


# -----------------------------------------------------------------------
# NL Change-Description Parser
# -----------------------------------------------------------------------

# Regex patterns for recognised change actions
_CHANGE_PATTERNS = [
    (
        r"rename\s+table\s+(\S+)\s+to\s+(\S+)",
        lambda m: {
            "action": "alter_table_name",
            "target": m.group(1),
            "old_value": m.group(1),
            "new_value": m.group(2),
        },
    ),
    (
        r"rename\s+column\s+(\S+)\.(\S+)\s+to\s+(.+?)(?:\.|;|$)",
        lambda m: {
            "action": "alter_column_business_name",
            "target": f"{m.group(1)}.{m.group(2)}",
            "old_value": m.group(2),
            "new_value": m.group(3).strip(),
        },
    ),
    (
        r"reclassify\s+(\S+)\.(\S+)\s+as\s+(\S+)",
        lambda m: {
            "action": "alter_classification",
            "target": f"{m.group(1)}.{m.group(2)}",
            "old_value": "",
            "new_value": m.group(3),
        },
    ),
    (
        r"add\s+relationship\s+(\S+)\.(\S+)\s*(?:->|to)\s*(\S+)\.(\S+)",
        lambda m: {
            "action": "add_relationship",
            "target": f"{m.group(1)}.{m.group(2)}",
            "old_value": "",
            "new_value": f"{m.group(3)}.{m.group(4)}",
        },
    ),
    (
        r"add\s+measure\s+(\S+)\s+on\s+(\S+)\s+as\s+(.+?)(?:\.|;|$)",
        lambda m: {
            "action": "add_business_measure",
            "target": m.group(2),
            "old_value": m.group(1),
            "new_value": m.group(3).strip(),
        },
    ),
]


def parse_change_description(description: str) -> ModelChangeProposal:
    """Parse a free-text model change description into a structured proposal.

    Supports:
    - ``rename table X to Y``
    - ``rename column T.C to NewName``
    - ``reclassify T.C as classification``
    - ``add relationship T.C -> T2.C2``
    - ``add measure name on table as expression``

    Risk levels:
    - *low*: 0-1 non-rename actions
    - *medium*: 2+ actions
    - *high*: any rename or reclassify action
    """
    desc_lower = description.lower()
    parsed_actions: List[Dict[str, Any]] = []

    for pattern, builder in _CHANGE_PATTERNS:
        for match in re.finditer(pattern, desc_lower, re.IGNORECASE):
            parsed_actions.append(builder(match))

    # Determine risk
    has_risky = any(
        a["action"] in ("alter_table_name", "alter_classification")
        for a in parsed_actions
    )

    if has_risky:
        risk_level = "high"
    elif len(parsed_actions) >= 2:
        risk_level = "medium"
    else:
        risk_level = "low"

    impact_parts: List[str] = []
    for a in parsed_actions:
        impact_parts.append(f"{a['action']} on {a['target']}")

    return ModelChangeProposal(
        description=description,
        parsed_actions=parsed_actions,
        impact_summary="; ".join(impact_parts) if impact_parts else "No recognised actions",
        risk_level=risk_level,
        requires_review=len(parsed_actions) > 0,
    )
