from __future__ import annotations

import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional

from abstractruntime import Runtime, persist_workflow_snapshot


logger = logging.getLogger(__name__)


def _parse_visual_listener_id(workflow_id: str) -> Optional[tuple[str, str]]:
    """Return (flow_id, node_id) for a visual_event_listener_* workflow_id."""
    prefix = "visual_event_listener_"
    if not isinstance(workflow_id, str) or not workflow_id.startswith(prefix):
        return None
    rest = workflow_id[len(prefix) :]
    parts = rest.split("_", 1)
    if len(parts) != 2:
        return None
    flow_id, node_id = parts[0], parts[1]
    if not flow_id or not node_id:
        return None
    return (flow_id, node_id)


def _parse_visual_agent_id(workflow_id: str) -> Optional[tuple[str, str]]:
    """Return (flow_id, node_id) for a visual_react_agent_* workflow_id."""
    prefix = "visual_react_agent_"
    if not isinstance(workflow_id, str) or not workflow_id.startswith(prefix):
        return None
    rest = workflow_id[len(prefix) :]
    parts = rest.split("_", 1)
    if len(parts) != 2:
        return None
    flow_id, node_id = parts[0], parts[1]
    if not flow_id or not node_id:
        return None
    return (flow_id, node_id)


def _require_visualflow_deps() -> None:
    try:
        import abstractflow  # noqa: F401
    except Exception as e:  # pragma: no cover
        raise RuntimeError(
            "VisualFlow execution requires the AbstractFlow compiler library (not the web UI). "
            "Install/repair with: `pip install abstractgateway`"
        ) from e


@dataclass(frozen=True)
class VisualFlowRegistry:
    """Load VisualFlow JSON files from a directory."""

    flows_dir: Path

    def load(self) -> Dict[str, Any]:
        _require_visualflow_deps()
        from abstractflow.visual.models import VisualFlow

        base = Path(self.flows_dir).expanduser().resolve()
        if not base.exists():
            raise FileNotFoundError(f"flows_dir does not exist: {base}")

        flows: Dict[str, Any] = {}
        for p in sorted(base.glob("*.json")):
            try:
                raw = p.read_text(encoding="utf-8")
                data = json.loads(raw)
                flow = VisualFlow.model_validate(data)
                flow_id = str(getattr(flow, "id", "") or "").strip() or p.stem
                flows[flow_id] = flow
            except Exception as e:
                logger.warning("Failed to load flow file %s: %s", p, e)
        return flows


class VisualFlowGatewayHost:
    """Gateway host that starts/ticks runs from VisualFlow JSON.

    This host is optional. For dependency-light deployments prefer bundle mode (`.flow` bundles),
    which compiles VisualFlow via `abstractruntime.visualflow_compiler` without importing `abstractflow`.
    """

    def __init__(
        self,
        *,
        flows_dir: Path,
        flows: Dict[str, Any],
        run_store: Any,
        ledger_store: Any,
        artifact_store: Any,
    ) -> None:
        _require_visualflow_deps()
        self._flows_dir = Path(flows_dir).expanduser().resolve()
        self._flows = dict(flows)
        self._run_store = run_store
        self._ledger_store = ledger_store
        self._artifact_store = artifact_store

    @property
    def flows_dir(self) -> Path:
        return self._flows_dir

    @property
    def flows(self) -> Dict[str, Any]:
        return dict(self._flows)

    @property
    def run_store(self) -> Any:
        return self._run_store

    @property
    def ledger_store(self) -> Any:
        return self._ledger_store

    @property
    def artifact_store(self) -> Any:
        return self._artifact_store

    def start_run(
        self,
        *,
        flow_id: str,
        input_data: Dict[str, Any],
        actor_id: str = "gateway",
        bundle_id: Optional[str] = None,
        session_id: Optional[str] = None,
    ) -> str:
        # bundle_id is ignored for VisualFlow sources (kept for API compatibility with bundle mode).
        del bundle_id
        _require_visualflow_deps()
        from abstractflow.visual.executor import create_visual_runner
        from abstractflow.visual.workspace_scoped_tools import WorkspaceScope, build_scoped_tool_executor

        fid = str(flow_id or "").strip()
        if not fid:
            raise ValueError("flow_id is required")
        visual_flow = self._flows.get(fid)
        if visual_flow is None:
            raise KeyError(f"Flow '{fid}' not found in {self._flows_dir}")

        data = dict(input_data or {})
        scope = WorkspaceScope.from_input_data(data)

        # Tool execution policy: mirror bundle mode semantics.
        #
        # - approval (default): execute safe tools locally; require explicit approval for dangerous/unknown tools.
        # - passthrough: require explicit approval for *all* tools (then execute in-process on resume).
        # - delegated: do not execute tools; TOOL_CALLS yields a durable JOB wait for external executors.
        # - local/local_all: execute all tools locally (dev-only; unsafe).
        tool_mode = str(os.getenv("ABSTRACTGATEWAY_TOOL_MODE") or "approval").strip().lower()
        local_executor: Any = None
        if scope is not None:
            local_executor = build_scoped_tool_executor(scope=scope)
        else:
            try:
                from abstractruntime.integrations.abstractcore.default_tools import build_default_tool_map
                from abstractruntime.integrations.abstractcore.tool_executor import MappingToolExecutor
            except Exception:
                local_executor = None
            else:
                local_executor = MappingToolExecutor(build_default_tool_map())
        if tool_mode in {"local", "local_all"}:
            tool_executor = local_executor
        elif tool_mode in {"approval", "local_approval", "local-approval"}:
            try:
                from abstractruntime.integrations.abstractcore.tool_executor import ApprovalToolExecutor, ToolApprovalPolicy
            except Exception:
                tool_executor = local_executor
            else:
                tool_executor = ApprovalToolExecutor(delegate=local_executor, policy=ToolApprovalPolicy()) if local_executor is not None else None
        elif tool_mode in {"delegated", "delegate", "job"}:
            try:
                from abstractruntime.integrations.abstractcore.tool_executor import PassthroughToolExecutor
            except Exception:
                tool_executor = None
            else:
                tool_executor = PassthroughToolExecutor(mode="delegated")
        else:
            try:
                from abstractruntime.integrations.abstractcore.tool_executor import ApprovalToolExecutor, ToolApprovalPolicy
            except Exception:
                tool_executor = None
            else:
                # Back-compat: "passthrough" means "approval required for all tools".
                tool_executor = ApprovalToolExecutor(delegate=local_executor, policy=ToolApprovalPolicy(auto_approve_tools=set())) if local_executor is not None else None

        vis_runner = create_visual_runner(
            visual_flow,
            flows=self._flows,
            run_store=self._run_store,
            ledger_store=self._ledger_store,
            artifact_store=self._artifact_store,
            tool_executor=tool_executor,
            input_data=data,
        )
        sid = str(session_id).strip() if isinstance(session_id, str) and session_id.strip() else None
        run_id = str(vis_runner.start(data, actor_id=actor_id, session_id=sid))

        # Persist a workflow snapshot for reproducible replay (best-effort).
        try:
            flow_dict = None
            try:
                to_dump = getattr(visual_flow, "model_dump", None)
                if callable(to_dump):
                    flow_dict = to_dump()
                else:
                    flow_dict = dict(visual_flow) if isinstance(visual_flow, dict) else None
            except Exception:
                flow_dict = None
            if isinstance(flow_dict, dict):
                snapshot = {"kind": "visualflow_json", "flow_id": fid, "visualflow": flow_dict}
                persist_workflow_snapshot(
                    run_store=self._run_store,
                    artifact_store=self._artifact_store,
                    run_id=str(run_id),
                    workflow_id=str(fid),
                    snapshot=snapshot,
                    format="visualflow_json",
                )
        except Exception:
            pass

        return run_id

    def runtime_and_workflow_for_run(self, run_id: str) -> tuple[Runtime, Any]:
        """Return (runtime, workflow_spec) for the given run, ensuring derived VisualFlow workflows are registered."""

        run = self._run_store.load(run_id)
        if run is None:
            raise KeyError(f"Run '{run_id}' not found")
        workflow_id = getattr(run, "workflow_id", None)
        if not isinstance(workflow_id, str) or not workflow_id:
            raise ValueError(f"Run '{run_id}' missing workflow_id")

        # Determine which VisualFlow root we need to compile to ensure this workflow_id is registered.
        root_flow_id = workflow_id
        parsed = _parse_visual_listener_id(workflow_id)
        if parsed is not None:
            root_flow_id = parsed[0]
        parsed2 = _parse_visual_agent_id(workflow_id)
        if parsed2 is not None:
            root_flow_id = parsed2[0]

        visual_flow = self._flows.get(root_flow_id)
        if visual_flow is None:
            raise KeyError(f"Flow '{root_flow_id}' not found (needed for workflow '{workflow_id}')")

        # Rebuild tool scope from persisted vars (best-effort).
        _require_visualflow_deps()
        from abstractflow.visual.executor import create_visual_runner
        from abstractflow.visual.workspace_scoped_tools import WorkspaceScope, build_scoped_tool_executor

        vars0 = getattr(run, "vars", None)
        scope = WorkspaceScope.from_input_data(vars0) if isinstance(vars0, dict) else None
        tool_mode = str(os.getenv("ABSTRACTGATEWAY_TOOL_MODE") or "approval").strip().lower()
        local_executor: Any = None
        if scope is not None:
            local_executor = build_scoped_tool_executor(scope=scope)
        else:
            try:
                from abstractruntime.integrations.abstractcore.default_tools import build_default_tool_map
                from abstractruntime.integrations.abstractcore.tool_executor import MappingToolExecutor
            except Exception:
                local_executor = None
            else:
                local_executor = MappingToolExecutor(build_default_tool_map())
        if tool_mode in {"local", "local_all"}:
            tool_executor = local_executor
        elif tool_mode in {"approval", "local_approval", "local-approval"}:
            try:
                from abstractruntime.integrations.abstractcore.tool_executor import ApprovalToolExecutor, ToolApprovalPolicy
            except Exception:
                tool_executor = local_executor
            else:
                tool_executor = ApprovalToolExecutor(delegate=local_executor, policy=ToolApprovalPolicy()) if local_executor is not None else None
        else:
            try:
                from abstractruntime.integrations.abstractcore.tool_executor import PassthroughToolExecutor
            except Exception:
                tool_executor = None
            else:
                tool_executor = PassthroughToolExecutor(mode="approval_required")

        input_defaults: Dict[str, Any] = {}
        if isinstance(vars0, dict):
            prov = vars0.get("provider")
            if isinstance(prov, str) and prov.strip():
                input_defaults["provider"] = prov.strip()
            mod = vars0.get("model")
            if isinstance(mod, str) and mod.strip():
                input_defaults["model"] = mod.strip()

        runner = create_visual_runner(
            visual_flow,
            flows=self._flows,
            run_store=self._run_store,
            ledger_store=self._ledger_store,
            artifact_store=self._artifact_store,
            tool_executor=tool_executor,
            input_data=input_defaults or None,
        )
        runtime = runner.runtime

        # WorkflowSpec lookup: root uses runner.workflow; derived workflows use runtime.workflow_registry.
        if getattr(runner.workflow, "workflow_id", None) == workflow_id:
            return (runtime, runner.workflow)

        reg = getattr(runtime, "workflow_registry", None)
        if reg is None or not hasattr(reg, "get"):
            raise KeyError(f"Runtime has no workflow_registry for workflow '{workflow_id}'")
        wf = reg.get(workflow_id)
        if wf is None:
            raise KeyError(f"Workflow '{workflow_id}' not registered")
        return (runtime, wf)
