"""
Issue #108: Grammar-aware validation for extracted node types.

Provides lightweight POS (Part-of-Speech) validation to check whether
extracted node labels match their expected grammatical form:
  - Entities (P, T, C) → should be noun phrases (NP)
  - Events (G, D, A) → should contain verbs (VP)
  - Propositions (F) → should be full clauses (S)

This is a *diagnostic* tool — it logs warnings for mismatches but does NOT
gate extraction.  Use it for debugging extraction quality.

Usage:
    from mcal.core.grammar_validator import GrammarValidator

    validator = GrammarValidator(enabled=True)
    warnings = validator.validate_graph(graph)
    for w in warnings:
        print(w)  # e.g., "GOAL node G1 'database performance' has no verb"
"""

from __future__ import annotations

import logging
import re
from typing import Optional

logger = logging.getLogger(__name__)

# Lightweight verb detection heuristics (no spaCy dependency).
# These patterns cover common English verb forms and infinitives.
_VERB_PATTERNS = re.compile(
    r"\b("
    r"(?:to\s+\w+)"             # infinitive: "to build", "to deploy"
    r"|(?:\w+ing)\b"            # gerund: "building", "deploying" (but not standalone "ing")
    r"|(?:use|build|deploy|test|run|fix|add|set|get|make|create|move|switch|choose|"
    r"buy|sell|save|plan|reduce|improve|migrate|implement|configure|establish|"
    r"send|update|delete|remove|start|stop|complete|finish|launch|release|"
    r"analyze|optimise|optimize|develop|design|refactor|scale|manage|track|monitor|"
    r"hire|fire|fund|spend|invest|earn|pay|negotiate|evaluate|decide|select|adopt|"
    r"focus|prioritize|prioritise|integrate|install|upgrade|downgrade|schedule|assign|"
    r"review|approve|reject|submit|push|merge|revert|rollback|archive|restore)\b"
    r")",
    re.IGNORECASE,
)

# Common noun‐phrase indicators (articles, possessives, adjectives before nouns)
_NP_INDICATORS = re.compile(
    r"\b(the|a|an|my|our|their|his|her|its|this|that|these|those|current|new|total|main|primary)\s",
    re.IGNORECASE,
)

# Clause indicators — subjects + verbs, "is/are/was/were/has/have" patterns
_CLAUSE_PATTERNS = re.compile(
    r"\b(is|are|was|were|has|have|had|does|did|will|shall|should|can|could|would|earns?|costs?|equals?|measures?|totals?)\b",
    re.IGNORECASE,
)


class GrammarMismatch:
    """A single grammar validation warning."""

    __slots__ = ("node_id", "node_type", "label", "expected", "detail")

    def __init__(
        self,
        node_id: str,
        node_type: str,
        label: str,
        expected: str,
        detail: str,
    ):
        self.node_id = node_id
        self.node_type = node_type
        self.label = label
        self.expected = expected
        self.detail = detail

    def __str__(self) -> str:
        return (
            f"[grammar] {self.node_type} node {self.node_id} "
            f"'{self.label}': expected {self.expected} — {self.detail}"
        )

    def __repr__(self) -> str:
        return f"GrammarMismatch({self.node_id!r}, {self.node_type!r}, {self.detail!r})"


# Maps from current (mistyped) type → suggested correction type
_HIGH_CONFIDENCE_CORRECTIONS: dict[str, dict[str, str]] = {
    # Entity with verb-starting label → likely a GOAL
    "P": {"verb_start": "G"},
    "T": {"verb_start": "G"},
    "C": {"verb_start": "G"},
    # FACT without clause/number → likely a CONCEPT
    "F": {"no_clause": "C"},
}


class GrammarValidator:
    """Lightweight grammar-aware validator for extracted graph nodes.

    Args:
        enabled: When ``False`` (default), :meth:`validate_graph` is a no-op.
            Toggle at runtime via ``validator.enabled = True``.
    """

    def __init__(self, enabled: bool = False):
        self.enabled = enabled
        # Issue #120: Track correction statistics
        self.corrections_applied: int = 0
        self.warnings_logged: int = 0

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

    def validate_and_correct(self, graph) -> list[GrammarMismatch]:
        """Validate all nodes and auto-correct high-confidence mismatches.

        High-confidence corrections (Issue #120):
        - Entity (P/T/C) with verb-starting label → GOAL (G)
        - FACT (F) without clause or number → CONCEPT (C)

        Low-confidence mismatches (e.g., GOAL without verb) are logged
        as warnings but not corrected.

        Returns a list of :class:`GrammarMismatch` for all detected issues
        (both corrected and warnings-only).
        """
        if not self.enabled:
            return []

        from .unified_extractor import NodeType  # deferred to avoid circular import

        mismatches: list[GrammarMismatch] = []
        corrected = 0
        warned = 0

        for node_id, node in list(graph.nodes.items()):
            w = self._validate_node(node_id, node)
            if not w:
                continue

            mismatches.append(w)
            ntype = node.type.value
            suggestion = self.suggest_correction(ntype, w.detail)

            if suggestion is not None:
                old_type = node.type
                node.type = NodeType(suggestion)
                corrected += 1
                logger.info(
                    "Grammar auto-correct %s: %s → %s (label=%r)",
                    node_id, old_type.value, suggestion, node.label,
                )
            else:
                warned += 1
                logger.debug(str(w))

        self.corrections_applied += corrected
        self.warnings_logged += warned

        if mismatches:
            logger.info(
                "Grammar validation: %d issues (%d corrected, %d warnings) in %d nodes",
                len(mismatches), corrected, warned, len(graph.nodes),
            )
            # Store stats in graph metadata
            graph.metadata["grammar_corrections"] = corrected
            graph.metadata["grammar_warnings"] = warned

        return mismatches

    def suggest_correction(
        self, current_type: str, detail: str
    ) -> Optional[str]:
        """Suggest a corrected node type for a high-confidence mismatch.

        Args:
            current_type: Single-char type value (P/T/C/G/D/A/F).
            detail: The mismatch detail string from :class:`GrammarMismatch`.

        Returns:
            A single-char corrected type, or ``None`` if no correction is warranted.
        """
        corrections = _HIGH_CONFIDENCE_CORRECTIONS.get(current_type, {})
        if not corrections:
            return None

        # Entity with verb-starting label → GOAL
        if current_type in ("P", "T", "C") and "starts with a verb" in detail:
            return corrections.get("verb_start")

        # FACT without clause/number → CONCEPT
        if current_type == "F" and "no verb/copula and no number" in detail:
            return corrections.get("no_clause")

        return None

    def reset_stats(self) -> None:
        """Reset correction/warning counters."""
        self.corrections_applied = 0
        self.warnings_logged = 0

    def validate_graph(self, graph) -> list[GrammarMismatch]:
        """Validate all nodes in *graph* against grammar expectations.

        Returns a list of :class:`GrammarMismatch` warnings.  Does **not**
        modify the graph.
        """
        if not self.enabled:
            return []

        warnings: list[GrammarMismatch] = []
        for node_id, node in graph.nodes.items():
            w = self._validate_node(node_id, node)
            if w:
                warnings.append(w)
                logger.debug(str(w))

        if warnings:
            logger.info(
                "Grammar validation: %d/%d nodes have mismatches",
                len(warnings),
                len(graph.nodes),
            )
        return warnings

    def validate_label(self, label: str, node_type_value: str) -> Optional[GrammarMismatch]:
        """Validate a single label against expected grammar for its type.

        Args:
            label: The node label text.
            node_type_value: Single-char type value (P/T/C/G/D/A/F).

        Returns:
            A :class:`GrammarMismatch` or ``None`` if valid.
        """
        if not self.enabled:
            return None
        return self._check(label, node_type_value)

    # --------------------------------------------------------------------- #
    # Internal
    # --------------------------------------------------------------------- #

    def _validate_node(self, node_id: str, node) -> Optional[GrammarMismatch]:
        """Validate one graph node."""
        label = node.label
        ntype = node.type.value  # e.g. "P", "G", "F"
        mismatch = self._check(label, ntype, node_id=node_id)
        return mismatch

    def _check(
        self,
        label: str,
        ntype: str,
        node_id: str = "?",
    ) -> Optional[GrammarMismatch]:
        """Core heuristic check."""
        label_lower = label.lower().strip()
        if not label_lower:
            return None

        # ── Entity types (P, T, C): expect noun phrase — warn if has verb ──
        if ntype in ("P", "T", "C"):
            # Short labels (1 word) that are just names are fine
            if len(label_lower.split()) <= 1:
                return None
            # If the label starts with a verb / infinitive, might be a mistyped goal
            starts_with_verb = bool(re.match(
                r"^(to\s+\w+|use\s|build\s|deploy\s|choose\s|switch\s|set\s"
                r"|create\s|move\s|migrate\s|implement\s|configure\s|reduce\s"
                r"|improve\s|optimize\s|optimise\s|install\s|test\s|run\s|fix\s)",
                label_lower,
            ))
            if starts_with_verb:
                type_name = {"P": "PERSON", "T": "THING", "C": "CONCEPT"}[ntype]
                return GrammarMismatch(
                    node_id=node_id,
                    node_type=type_name,
                    label=label,
                    expected="noun phrase (NP)",
                    detail="label starts with a verb — may be a mistyped GOAL/DECISION/ACTION",
                )

        # ── Event types (G, D, A): expect verb phrase — warn if no verb ──
        elif ntype in ("G", "D", "A"):
            has_verb = bool(_VERB_PATTERNS.search(label_lower))
            if not has_verb:
                type_name = {"G": "GOAL", "D": "DECISION", "A": "ACTION"}[ntype]
                return GrammarMismatch(
                    node_id=node_id,
                    node_type=type_name,
                    label=label,
                    expected="verb phrase (VP)",
                    detail="label has no verb — may be a mistyped entity/concept",
                )

        # ── Fact type (F): expect clause with value/assertion ──
        elif ntype == "F":
            has_clause = bool(_CLAUSE_PATTERNS.search(label_lower))
            has_number = bool(re.search(r"\d", label_lower))
            # A FACT should ideally be a full clause or contain a number
            if not has_clause and not has_number:
                return GrammarMismatch(
                    node_id=node_id,
                    node_type="FACT",
                    label=label,
                    expected="declarative clause with value",
                    detail="label has no verb/copula and no number — may be a mistyped concept",
                )

        return None
