"""Nengo Code Generator.

Translates validated CNL specifications into working Nengo networks
for a sensory-motor reflex arc.
"""

import nengo

from utils import extract_numeric


class GeneratorError(Exception):
    """Raised when a required concept is missing from the parsed spec."""


def generate(parsed_specs: list[dict], neuron_params: dict) -> nengo.Network:
    """Generate a Nengo network from validated CNL specifications.

    Parameters
    ----------
    parsed_specs : list[dict]
        Output of cnl_parser.parse() — list of parsed sentence dictionaries.
    neuron_params : dict
        Neuron parameters: tau, threshold, reset_potential, refractory_period,
        synaptic_weight.

    Returns
    -------
    nengo.Network
        A Nengo network with sensory input ensemble, motor output ensemble,
        and a weighted connection between them.

    Raises
    ------
    GeneratorError
        If any required concept is missing from the parsed spec.
    """
    # Verify all 4 concepts are present
    concepts_found = {spec["concept"] for spec in parsed_specs}
    required = {
        "threshold_firing",
        "refractory_period",
        "membrane_potential_decay",
        "synaptic_weight",
    }
    missing = required - concepts_found
    if missing:
        raise GeneratorError(
            f"Missing required concepts in spec: {', '.join(sorted(missing))}"
        )

    # Extract parameters from specs (use spec values if available, fall back to params)
    spec_params = {
        "tau_rc": neuron_params.get("tau", 0.02),
        "tau_ref": neuron_params.get("refractory_period", 0.002),
        "synaptic_weight": neuron_params.get("synaptic_weight", 1.0),
        # threshold controls the effective intercepts: higher threshold → narrower range
        "threshold": neuron_params.get("threshold", 1.0),
    }

    # Map concept names to the parameter key they override
    _CONCEPT_TO_PARAM = {
        "membrane_potential_decay": "tau_rc",
        "refractory_period": "tau_ref",
        "synaptic_weight": "synaptic_weight",
        "threshold_firing": "threshold",
    }

    # Override from parsed specs where numeric values are available
    for spec in parsed_specs:
        param_key = _CONCEPT_TO_PARAM.get(spec["concept"])
        if param_key is not None:
            val = extract_numeric(spec["condition"])
            if val is not None:
                spec_params[param_key] = val

    tau_rc = spec_params["tau_rc"]
    tau_ref = spec_params["tau_ref"]
    synaptic_weight = spec_params["synaptic_weight"]
    threshold = spec_params["threshold"]

    # Map threshold to intercepts: intercepts in (-1, threshold) so neurons fire
    # only when their represented input exceeds the threshold level.
    intercepts = nengo.dists.Uniform(-1.0, min(threshold - 0.05, 0.9))

    # Build the Nengo network
    net = nengo.Network(label="reflex_arc")
    with net:
        # LIF neuron type with parameters from the spec
        neuron_type = nengo.LIF(tau_rc=tau_rc, tau_ref=tau_ref)

        # Sensory input ensemble (1D) — intercepts derived from threshold concept
        net.sensory = nengo.Ensemble(
            n_neurons=50,
            dimensions=1,
            neuron_type=neuron_type,
            intercepts=intercepts,
            label="sensory",
        )

        # Motor output ensemble (1D)
        net.motor = nengo.Ensemble(
            n_neurons=50,
            dimensions=1,
            neuron_type=neuron_type,
            intercepts=intercepts,
            label="motor",
        )

        # Connection with synaptic weight
        net.connection = nengo.Connection(
            net.sensory,
            net.motor,
            transform=synaptic_weight,
        )

        # Input node for stimulation
        net.input_node = nengo.Node(output=0.5, label="stimulus")
        nengo.Connection(net.input_node, net.sensory)

        # Output probe
        net.motor_probe = nengo.Probe(net.motor, synapse=0.01)

    return net
