"""
依存関係解決エンジン（リアクティブパイプライン用）。

ゴールから逆算して必要なタスクだけを実行する。
将来的に別パッケージとして切り出す可能性あり。
"""
import inspect
from logging import getLogger
import time

# @reactive でデコレートされた関数を (モジュール名, 関数) で保持。
# 利用側が _register_tasks で get_reactive_tasks(__name__) により自モジュール分だけ登録する。
_REACTIVE_REGISTRY = []


def reactive(func):
    """DependencyEngine タスクであることを示すマーカー。

    デコレートした関数の名前がそのまま reactive プロパティ名になる
    (例: genice.cages)。名前は名詞にすること。

    副作用: この関数を _REACTIVE_REGISTRY に登録する。
    get_reactive_tasks(module_name) で同一モジュールのタスク一覧を取得できる。
    """
    _REACTIVE_REGISTRY.append((func.__module__, func))
    return func


def get_reactive_tasks(module_name: str):
    """指定モジュールで @reactive を付けた関数のリストを返す。"""
    return [f for mod, f in _REACTIVE_REGISTRY if mod == module_name]


class DependencyEngine:
    """ゴール(target)から依存を逆算して必要なタスクのみ実行するエンジン。"""

    logger = getLogger("dependency_engine")

    def __init__(self):
        self.registry = {}  # { 'output_name': function }
        self.cache = {}

    def task(self, func):
        """デコレータ: 関数名 = 生成される変数名 として登録"""
        self.registry[func.__name__] = func
        return func

    def resolve(self, target: str, inputs: dict):
        """ゴール(target)から逆算して計算する"""

        # 1. 既に計算済み or 入力として与えられているならそれを返す
        if target in inputs:
            return inputs[target]
        if target in self.cache:
            return self.cache[target]

        # 2. 生成ルール（関数）を探す
        if target not in self.registry:
            raise ValueError(f"Don't know how to make '{target}'")

        func = self.registry[target]

        # 3. その関数の引数（依存先）を調べて、再帰的に解決する
        sig = inspect.signature(func)
        dependencies = {}
        for param_name in sig.parameters:
            dependencies[param_name] = self.resolve(param_name, inputs)

        # 4. 実行して結果を保存
        self.logger.info(f"Executing: {target}")
        now = time.time()
        result = func(**dependencies)
        delta = time.time() - now
        self.logger.info(f"{delta:.4f} sec for {target}")
        self.cache[target] = result
        return result


def _demo():
    """パッケージ単体テスト用デモ（python -m dependency_engine で実行可能）"""
    engine = DependencyEngine()

    @engine.task
    def reaction_A(raw_material: int):
        return raw_material * 2

    @engine.task
    def reaction_B(reaction_A: int, catalyst: int):
        return reaction_A + catalyst

    result = engine.resolve(
        target="reaction_B", inputs={"raw_material": 10, "catalyst": 5}
    )
    print(result)


if __name__ == "__main__":
    _demo()
