# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2025-2026 Dmitrii Gagarin aka madgagarin


from logging import getLogger
from typing import Any

logger = getLogger(__name__)


class ActionFactory:
    """A factory that provides handlers with methods for process control."""

    def __init__(
        self,
        job_id: str,
        default_dispatch_timeout: int | None = None,
        default_result_timeout: int | None = None,
    ):
        self._job_id = job_id
        self._default_dispatch_timeout = default_dispatch_timeout
        self._default_result_timeout = default_result_timeout
        self._next_state_val: str | None = None
        self._task_to_dispatch_val: dict[str, Any] | None = None
        self._sub_blueprint_to_run_val: dict[str, Any] | None = None
        self._parallel_tasks_to_dispatch_val: dict[str, Any] | None = None
        self._pending_events: list[tuple[str, dict[str, Any]]] = []

    def _check_for_existing_action(self) -> None:
        """
        Helper to ensure only one action is set.
        Raises RuntimeError if any action value is already set.
        """
        if any(
            [
                self._next_state_val,
                self._task_to_dispatch_val,
                self._sub_blueprint_to_run_val,
                self._parallel_tasks_to_dispatch_val,
            ]
        ):
            raise RuntimeError(
                "Cannot set multiple actions in the same step. "
                "An action (transition, task, blueprint, or parallel) has already been defined."
            )

    @property
    def next_state(self) -> str | None:
        return self._next_state_val

    @property
    def task_to_dispatch(self) -> dict[str, Any] | None:
        return self._task_to_dispatch_val

    @property
    def sub_blueprint_to_run(self) -> dict[str, Any] | None:
        return self._sub_blueprint_to_run_val

    @property
    def parallel_tasks_to_dispatch(self) -> dict[str, Any] | None:
        return self._parallel_tasks_to_dispatch_val

    @property
    def pending_events(self) -> list[tuple[str, dict[str, Any]]]:
        return self._pending_events

    def send_event(self, event_type: str, payload: dict[str, Any]) -> None:
        """Emits a generic event from the blueprint logic."""
        logger.debug(f"Job {self._job_id}: Ghost emitting event '{event_type}'")
        self._pending_events.append((event_type, payload))

    def dispatch_parallel(self, tasks: list[dict[str, Any]], aggregate_into: str) -> None:
        """
        Dispatches multiple tasks for parallel execution.
        """
        self._check_for_existing_action()
        logger.debug(
            f"Job {self._job_id}: Dispatching {len(tasks)} tasks in parallel, aggregating into '{aggregate_into}'"
        )
        self._parallel_tasks_to_dispatch_val = {
            "tasks": tasks,
            "aggregate_into": aggregate_into,
        }

    def transition_to(self, state: str) -> None:
        """Schedules a transition to a new state."""
        self._check_for_existing_action()
        logger.debug(f"Job {self._job_id}: Transitioning to '{state}'")
        self._next_state_val = state

    def dispatch_task(
        self,
        task_type: str,
        params: dict[str, Any],
        transitions: dict[str, str],
        event_transitions: dict[str, str] | None = None,
        dispatch_strategy: str = "default",
        resource_requirements: dict[str, Any] | None = None,
        timeout_seconds: int | None = None,
        dispatch_timeout_seconds: int | None = None,
        result_timeout_seconds: int | None = None,
        max_cost: float | None = None,
        priority: float = 0.0,
        resource_hint: str | None = None,
    ) -> None:
        """Dispatches a task to a worker for execution."""
        self._check_for_existing_action()
        logger.debug(f"Job {self._job_id}: Dispatching task '{task_type}'")

        # HLN: If resource_hint is provided, inject it into params for the Dispatcher
        if resource_hint:
            params["resource_hint"] = resource_hint

        # Priority: explicit param > job default
        final_dispatch_timeout = (
            dispatch_timeout_seconds if dispatch_timeout_seconds is not None else self._default_dispatch_timeout
        )
        final_result_timeout = (
            result_timeout_seconds if result_timeout_seconds is not None else self._default_result_timeout
        )

        self._task_to_dispatch_val = {
            "type": task_type,
            "params": params,
            "transitions": transitions,
            "event_transitions": event_transitions or {},
            "dispatch_strategy": dispatch_strategy,
            "resource_requirements": resource_requirements,
            "timeout_seconds": timeout_seconds,
            "dispatch_timeout_seconds": final_dispatch_timeout,
            "result_timeout_seconds": final_result_timeout,
            "max_cost": max_cost,
            "priority": priority,
        }

    def await_human_approval(
        self,
        integration: str,
        message: str,
        transitions: dict[str, str],
    ) -> None:
        """Pauses the pipeline until an external signal (human approval) is received."""
        self._check_for_existing_action()
        logger.debug(f"Job {self._job_id}: Awaiting human approval via {integration}")
        self._task_to_dispatch_val = {
            "type": "human_approval",
            "integration": integration,
            "message": message,
            "transitions": transitions,
        }

    def run_blueprint(
        self,
        blueprint_name: str,
        initial_data: dict[str, Any],
        transitions: dict[str, str],
        dispatch_timeout: int | None = None,
        result_timeout: int | None = None,
    ) -> None:
        """Runs a child blueprint and waits for its result."""
        self._check_for_existing_action()
        logger.debug(f"Job {self._job_id}: Running sub-blueprint '{blueprint_name}'")
        self._sub_blueprint_to_run_val = {
            "blueprint_name": blueprint_name,
            "initial_data": initial_data,
            "transitions": transitions,
            "dispatch_timeout": dispatch_timeout,
            "result_timeout": result_timeout,
        }
