"""CNL Parser for sensory-motor reflex arc specifications.

Parses Controlled Natural Language sentences into structured dictionaries.
Uses regex-based pattern matching only (no LLM).

Merged from v-copilot (strict patterns, named groups, verb/negated fields)
and v-jules (broader article and subject support).
"""

import re


class ParseError(Exception):
    """Raised when a sentence does not match the CNL grammar."""


# --- Pattern definitions for each concept ---

# Concept 1: Threshold Firing
# e.g. "The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0"
# e.g. "A motor neuron MUST NOT fire ONLY IF membrane potential is below threshold"
# e.g. "The motor neuron MUST emit a spike ONLY IF membrane potential exceeds 0.5"
_THRESHOLD_PATTERNS = [
    re.compile(
        r"^(?:The|A)\s+(?P<subject>(?:sensory|motor)\s+neuron)\s+"
        r"(?P<verb>MUST(?:\s+NOT)?)\s+"
        r"(?P<action>(?:fire|emit\s+a\s+spike))\s+"
        r"(?:ONLY\s+)?IF\s+membrane\s+potential\s+"
        r"(?P<condition>(?:exceeds|is\s+below)\s+(?:\d+(?:\.\d+)?|threshold))$",
        re.IGNORECASE,
    ),
]

# Concept 2: Refractory Period
# e.g. "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds"
# e.g. "A motor neuron MUST NOT respond to input DURING the refractory period"
# e.g. "The sensory neuron MUST remain inactive AFTER firing for 0.002 seconds"
# e.g. "A neuron MUST NOT accept input DURING the refractory period"
_REFRACTORY_PATTERNS = [
    re.compile(
        r"^(?:The|A)\s+(?P<subject>(?:sensory|motor)\s+neuron)\s+"
        r"(?P<verb>MUST(?:\s+NOT)?)\s+"
        r"(?P<action>(?:fire|respond\s+to\s+input|accept\s+input))\s+"
        r"DURING\s+the\s+refractory\s+period"
        r"(?:\s+of\s+(?P<duration>\d+(?:\.\d+)?)\s+seconds)?$",
        re.IGNORECASE,
    ),
    re.compile(
        r"^(?:The|A)\s+(?P<subject>(?:sensory|motor)\s+neuron)\s+"
        r"(?P<verb>MUST(?:\s+NOT)?)\s+"
        r"(?P<action>remain\s+inactive)\s+"
        r"AFTER\s+firing\s+for\s+"
        r"(?P<duration>\d+(?:\.\d+)?)\s+seconds$",
        re.IGNORECASE,
    ),
]

# Concept 3: Membrane Potential Decay
# e.g. "The sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds"
# e.g. "The motor neuron membrane potential MUST NOT increase WITHOUT input"
_DECAY_PATTERNS = [
    re.compile(
        r"^(?:The|A)\s+(?P<subject>(?:sensory|motor)\s+neuron\s+membrane\s+potential)\s+"
        r"(?P<verb>MUST(?:\s+NOT)?)\s+"
        r"(?P<action>decay(?:\s+toward\s+resting\s+potential)?)\s+"
        r"WITH\s+time\s+constant\s+(?:of\s+)?"
        r"(?P<tau>\d+(?:\.\d+)?)\s+seconds$",
        re.IGNORECASE,
    ),
    re.compile(
        r"^(?:The|A)\s+(?P<subject>(?:sensory|motor)\s+neuron\s+membrane\s+potential)\s+"
        r"(?P<verb>MUST\s+NOT)\s+"
        r"(?P<action>increase)\s+"
        r"WITHOUT\s+input$",
        re.IGNORECASE,
    ),
]

# Concept 4: Synaptic Weight
# e.g. "The connection from sensory neuron to motor neuron MUST have WITH synaptic weight of 1.0"
# e.g. "The connection from sensory neuron to motor neuron MUST transmit WITH synaptic weight of 0.5"
# e.g. "The connection from sensory neuron to motor neuron MUST NOT have WITH synaptic weight of 0.0"
_SYNAPTIC_PATTERNS = [
    re.compile(
        r"^The\s+connection\s+from\s+"
        r"(?P<subject>(?:sensory|motor)\s+neuron\s+to\s+(?:sensory|motor)\s+neuron)\s+"
        r"(?P<verb>MUST(?:\s+NOT)?)\s+"
        r"(?P<action>(?:have|transmit|scale\s+input))\s+"
        r"WITH\s+synaptic\s+weight\s+of\s+"
        r"(?P<weight>\d+(?:\.\d+)?)$",
        re.IGNORECASE,
    ),
]


def _try_match(patterns, concept_name, sentence):
    """Try to match a sentence against a list of patterns for a concept."""
    for pattern in patterns:
        m = pattern.match(sentence.strip())
        if m:
            groups = m.groupdict()
            verb_raw = groups.get("verb", "MUST")
            result = {
                "concept": concept_name,
                "subject": groups["subject"],
                "condition": None,
                "action": groups.get("action", ""),
                "verb": verb_raw.upper().strip(),
                "negated": "NOT" in verb_raw.upper(),
                "raw": sentence.strip(),
            }
            # Build condition from extra captured groups
            if concept_name == "threshold_firing":
                result["condition"] = groups.get("condition", "")
            elif concept_name == "refractory_period":
                duration = groups.get("duration")
                if duration:
                    result["condition"] = f"refractory period of {duration} seconds"
                else:
                    result["condition"] = "refractory period"
            elif concept_name == "membrane_potential_decay":
                tau = groups.get("tau")
                if tau:
                    result["condition"] = f"time constant of {tau} seconds"
                else:
                    result["condition"] = "no input"
            elif concept_name == "synaptic_weight":
                weight = groups.get("weight")
                if weight:
                    result["condition"] = f"synaptic weight of {weight}"
            return result
    return None


def parse(sentence: str) -> dict:
    """Parse a CNL sentence into a structured dictionary.

    Parameters
    ----------
    sentence : str
        A plain text CNL sentence.

    Returns
    -------
    dict
        Keys: concept, subject, condition, action, verb, negated, raw

    Raises
    ------
    ParseError
        If the sentence does not match any grammar pattern.
    """
    sentence = sentence.strip()
    if not sentence:
        raise ParseError("Empty sentence")

    concept_patterns = [
        (_THRESHOLD_PATTERNS, "threshold_firing"),
        (_REFRACTORY_PATTERNS, "refractory_period"),
        (_DECAY_PATTERNS, "membrane_potential_decay"),
        (_SYNAPTIC_PATTERNS, "synaptic_weight"),
    ]

    for patterns, concept_name in concept_patterns:
        result = _try_match(patterns, concept_name, sentence)
        if result:
            return result

    raise ParseError(
        f"Sentence does not match any CNL grammar pattern: '{sentence}'"
    )
