# agi_lab/agents/neuromorphic.py

from __future__ import annotations


if __package__ in {None, ""}:  # pragma: no cover - permite ejecución como script
    import sys
    from pathlib import Path

    _current_file = Path(__file__).resolve()
    for _parent in (_current_file.parent, *_current_file.parents):
        if (_parent / "pyproject.toml").exists() or (_parent / ".git").exists():
            _project_root = _parent
            break
    else:
        _project_root = _current_file.parents[2]
    _src_root = _project_root / "src"
    for _path in (_project_root, _src_root):
        _path_str = str(_path)
        if _path_str not in sys.path:
            sys.path.insert(0, _path_str)
    from agix._script_utils import bootstrap_script_environment

    bootstrap_script_environment("__main__", str(_current_file))

from typing import Optional

import numpy as np

from agix.agents.base import AGIAgent
from agix.qualia.spirit import QualiaSpirit


class PlasticSynapse:
    """
    Modelo simple de sinapsis plástica usando una regla hebbiana modificada.
    """
    def __init__(self, input_size, output_size, learning_rate=0.01, decay=0.01):
        self.weights = np.random.uniform(-0.5, 0.5, (output_size, input_size))
        self.base_learning_rate = learning_rate
        self.learning_rate = learning_rate
        self.decay = decay

    def forward(self, x):
        return np.dot(self.weights, x)

    def hebbian_update(self, x, y):
        """
        Regla hebbiana: Δw_ij = η * y_i * x_j
        """
        x_norm = np.linalg.norm(x) + 1e-9
        y_norm = np.linalg.norm(y) + 1e-9
        normalized_x = x / x_norm
        normalized_y = y / y_norm

        lr = self.learning_rate
        delta_w = lr * np.outer(normalized_y, normalized_x)
        decay_term = self.decay * self.weights
        self.weights += delta_w - decay_term
        self.weights = np.clip(self.weights, -1.0, 1.0)

        # Reducir gradualmente la tasa de aprendizaje para favorecer la convergencia
        min_lr = self.base_learning_rate * 0.1
        self.learning_rate = max(min_lr, self.learning_rate * (1.0 - self.decay * 0.5))

        return delta_w - decay_term


class NeuromorphicAgent(AGIAgent):
    """
    Agente con arquitectura inspirada en redes neuronales plásticas.
    """
    def __init__(
        self,
        input_size,
        output_size,
        learning_rate=0.01,
        decay: float = 0.01,
        qualia: Optional[QualiaSpirit] = None,
    ):
        super().__init__(name="NeuromorphicAgent", qualia=qualia)
        self.input_size = input_size
        self.output_size = output_size
        self.synapse = PlasticSynapse(
            input_size, output_size, learning_rate, decay=decay
        )
        self.last_input = None
        self.last_output = None
        self.decay = decay
        self.learning_rate = learning_rate

    def perceive(self, observation):
        x = np.asarray(observation).flatten()
        x = x[:self.input_size]  # Truncar si necesario
        self.last_input = x
        self.last_output = self.synapse.forward(x)
        super().perceive(observation)

    def decide(self, pad: 'PADState | None' = None):
        pad = super().decide(pad)
        if self.last_output is None:
            return np.random.randint(self.output_size)
        action = int(np.argmax(self.last_output))
        return action

    def learn(self, reward, done=False):
        if self.last_input is None or self.last_output is None:
            return
        y = self.last_output * reward  # Modulación por refuerzo
        self.synapse.hebbian_update(self.last_input, y)

    def reset(self):
        super().reset()
        self.synapse = PlasticSynapse(
            self.input_size,
            self.output_size,
            self.learning_rate,
            decay=self.decay,
        )
