# /apps/aroflo_connector_app/agent/det_runner.py
from __future__ import annotations

import os
from dataclasses import asdict
from typing import Any, Dict, Optional, Tuple

from dotenv import load_dotenv

from ..client import AroFloClient
from ..config import AroFloConfigError
from ..zones.registry import build_registry

from .det_policy import DeterministicPolicy, apply_policy


class DeterministicError(RuntimeError):
    pass


def _make_client() -> AroFloClient:
    """
    Ensure .env is loaded when running as a module from flask_server root.
    """
    load_dotenv(os.path.join(os.getcwd(), ".env"))
    return AroFloClient()


def _find_operation(zone_obj: Any, op_code: str) -> Any:
    ops = getattr(zone_obj, "operations", []) or []
    for op in ops:
        if getattr(op, "code", None) == op_code:
            return op
    raise DeterministicError(f"Operation '{op_code}' not found in zone '{getattr(zone_obj, 'code', '?')}'.")


def _extract_request_if_rawwrap(result: Any) -> tuple[Any, dict | None]:
    """
    If the operation returns raw_wrap ({"data": ..., "meta": {"params":..., "var_string":...}}),
    normalize to:
      - data = result["data"]
      - request = result["meta"]
    Otherwise return (result, None).
    """
    if isinstance(result, dict) and "data" in result and "meta" in result:
        m = result.get("meta")
        if isinstance(m, dict) and "params" in m and "var_string" in m:
            return result["data"], m
    return result, None


def execute_deterministic(
    *,
    zone_code: str,
    op_code: str,
    params: Dict[str, Any],
    policy: Optional[DeterministicPolicy] = None,
    include_meta: bool = False,
) -> Dict[str, Any] | Any:
    """
    Deterministic runner:
    - loads registry
    - resolves zone + operation
    - applies policy (e.g., force dry_run on writes, pageSize bounds)
    - executes zone.execute(op_code, params=effective_params)
    - optionally wraps result with meta (and request info if raw_wrap)
    """
    policy = policy or DeterministicPolicy()

    try:
        client = _make_client()
    except AroFloConfigError as exc:
        raise DeterministicError(f"AroFlo configuration error: {exc}") from exc

    registry = build_registry(client)
    if zone_code not in registry:
        raise DeterministicError(f"Unknown zone '{zone_code}'. Available: {sorted(registry.keys())}")

    zone = registry[zone_code]
    op = _find_operation(zone, op_code)  # only for policy + meta

    # Apply deterministic policy (this is what must be executed)
    effective_params = apply_policy(op=op, params=params or {}, policy=policy)

    # IMPORTANT: your zones expect op_code (str), not ZoneOperation object
    result = zone.execute(op_code, params=effective_params)

    if not include_meta:
        return result

    # If zone returned raw_wrap, normalize output:
    request_meta = None
    result, request_meta = _extract_request_if_rawwrap(result)

    meta = {
        "zone": zone_code,
        "operation": op_code,
        "side_effect": getattr(op, "side_effect", None),
        "http_method": getattr(op, "http_method", None),
        "idempotent": getattr(op, "idempotent", None),
        "policy": asdict(policy),
        "effective_params": effective_params,
    }

    payload: Dict[str, Any] = {"data": result, "meta": meta}
    if request_meta:
        payload["request"] = request_meta

    return payload


def list_operations(zone_code: str) -> Tuple[str, list[dict]]:
    """
    Utility for CLI: list operation codes + labels for a zone.
    """
    try:
        client = _make_client()
    except AroFloConfigError as exc:
        raise DeterministicError(f"AroFlo configuration error: {exc}") from exc

    registry = build_registry(client)
    if zone_code not in registry:
        raise DeterministicError(f"Unknown zone '{zone_code}'. Available: {sorted(registry.keys())}")

    zone = registry[zone_code]
    ops = getattr(zone, "operations", []) or []
    out: list[dict] = []
    for op in ops:
        out.append(
            {
                "code": getattr(op, "code", None),
                "label": getattr(op, "label", None),
                "side_effect": getattr(op, "side_effect", None),
                "http_method": getattr(op, "http_method", None),
            }
        )
    return zone_code, out

