# #region JobLifecycleModule [C:5] [TYPE Module] [SEMANTICS task,lifecycle,state,machine,execution]
# @BRIEF Task creation, execution, pause/resume, and completion transitions for plugin-backed
#        jobs — extracted from the monolithic TaskManager.
# @LAYER Core
# @RELATION DEPENDS_ON -> [TaskGraph]
# @RELATION DEPENDS_ON -> [EventBus]
# @RELATION DEPENDS_ON -> [TaskContext]
# @RELATION DEPENDS_ON -> [PluginLoader]
# @RELATION DEPENDS_ON -> [TaskPersistenceService]
# @RATIONALE Extracted from TaskManager to satisfy INV_7. JobLifecycle encodes the task state
#            machine with plugin execution, context injection, backward-compatible plugin
#            dispatch, and input/mapping pause/resume transitions.
# @REJECTED Keeping lifecycle/execution logic inside TaskManager was rejected — it mixes
#            async execution orchestration with passive registry and log buffering concerns,
#            violating single-responsibility and INV_7.
# @PRE Requested plugin ids resolve to executable plugins; task state transitions target known
#      TaskStatus values.
# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted
#       terminal or waiting state.
# @SIDE_EFFECT Schedules async execution, pauses on input/mapping requests, mutates persisted
#              task lifecycle markers.
# @INVARIANT A task cannot be resumed from a waiting state unless a matching future exists or
#            a new wait future is created.

import asyncio
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
import inspect
from typing import Any

from ..cot_logger import seed_trace_id
from ..logger import belief_scope, logger
from .context import TaskContext
from .graph import TaskGraph
from .models import Task, TaskStatus

def _task_to_dict(task: Task) -> dict:
    """Serialize a Task to a dict for WebSocket broadcast."""
    return {
        "id": task.id,
        "plugin_id": task.plugin_id,
        "status": task.status.value if isinstance(task.status, TaskStatus) else task.status,
        "started_at": task.started_at.isoformat() if task.started_at else None,
        "finished_at": task.finished_at.isoformat() if task.finished_at else None,
        "user_id": task.user_id,
        "params": task.params,
        "result": task.result,
        "input_required": task.input_required,
        "input_request": task.input_request,
        "error": str(task.result) if task.status == TaskStatus.FAILED and task.result else None,
    }


# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]
# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for
#        plugin-backed jobs.
# @RELATION DEPENDS_ON -> [TaskGraph]
# @RELATION DEPENDS_ON -> [TaskContext]
# @RELATION DEPENDS_ON -> [PluginLoader]
# @RELATION DEPENDS_ON -> [TaskPersistenceService]
# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.
# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted
#       terminal or waiting state.
# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution
#            orchestration (plugin dispatch, context injection, pause/resume futures) from
#            passive registry and log buffering concerns.
# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task
#            orchestration with synchronous registry operations and background threading,
#            violating single-responsibility and making the 708-line module unmaintainable.
class JobLifecycle:
    """Task state machine: creation, execution, pause/resume, completion."""

    # #region __init__ [TYPE Function]
    # @BRIEF Initialize with dependencies.
    def __init__(
        self,
        plugin_loader: Any,
        graph: TaskGraph,
        event_bus: Any,
        persistence_service: Any,
        executor: ThreadPoolExecutor,
        loop: asyncio.AbstractEventLoop,
    ):
        self.plugin_loader = plugin_loader
        self.graph = graph
        self.event_bus = event_bus
        self.persistence_service = persistence_service
        self.executor = executor
        self.loop = loop
        self._dataset_subscribers: dict[str, list[asyncio.Queue]] = {}
    # #endregion __init__

    # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]
    # @BRIEF Creates and queues a new task for execution.
    # @PRE Plugin with plugin_id exists. Params are valid dict.
    # @POST Task is created, added to registry, and scheduled for execution.
    # @RAISES ValueError if plugin not found or params invalid.
    async def create_task(
        self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,
        add_log_callback=None,
    ) -> Task:
        with belief_scope("JobLifecycle.create_task", f"plugin_id={plugin_id}"):
            if not self.plugin_loader.has_plugin(plugin_id):
                logger.error(f"Plugin with ID '{plugin_id}' not found.")
                raise ValueError(f"Plugin with ID '{plugin_id}' not found.")
            self.plugin_loader.get_plugin(plugin_id)
            if not isinstance(params, dict):
                logger.error("Task parameters must be a dictionary.")
                raise ValueError("Task parameters must be a dictionary.")
            logger.reason("Creating task node and scheduling execution")
            task = Task(plugin_id=plugin_id, params=params, user_id=user_id)
            self.graph.add_task(task)
            self.persistence_service.persist_task(task)
            logger.info(f"Task {task.id} created and scheduled for execution")
            self.loop.create_task(
                self._run_task(task.id, add_log_callback=add_log_callback)
            )
            logger.reflect(
                "Task creation persisted and execution scheduled",
                extra={"task_id": task.id, "plugin_id": plugin_id},
            )
            return task
    # #endregion create_task

    # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]
    # @BRIEF Internal method to execute a task with TaskContext support.
    # @PRE Task exists in registry.
    # @POST Task is executed, status updated to SUCCESS or FAILED.
    # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.
    async def _run_task(self, task_id: str, add_log_callback=None):
        seed_trace_id()
        with belief_scope("JobLifecycle._run_task", f"task_id={task_id}"):
            task = self.graph.get_task(task_id)
            if task is None:
                logger.error(f"Task {task_id} not found in registry")
                return
            plugin = self.plugin_loader.get_plugin(task.plugin_id)
            logger.reason(
                "Transitioning task to running state",
                extra={"task_id": task_id, "plugin_id": task.plugin_id},
            )
            logger.info(
                f"Starting execution of task {task_id} for plugin '{plugin.name}'"
            )
            task.status = TaskStatus.RUNNING
            task.started_at = datetime.now(UTC)
            self.persistence_service.persist_task(task)
            self._broadcast_task_status(task)
            if add_log_callback:
                add_log_callback(
                    task_id, "INFO",
                    f"Task started for plugin '{plugin.name}'",
                    source="system",
                )
            try:
                params = {**task.params, "_task_id": task_id}
                sig = inspect.signature(plugin.execute)
                accepts_context = "context" in sig.parameters
                if accepts_context:
                    # Create TaskContext for new-style plugins
                    context = TaskContext(
                        task_id=task_id,
                        add_log_fn=add_log_callback,
                        params=params,
                        default_source="plugin",
                        background_tasks=None,
                    )
                    if asyncio.iscoroutinefunction(plugin.execute):
                        task.result = await plugin.execute(params, context=context)
                    else:
                        task.result = await self.loop.run_in_executor(
                            self.executor,
                            lambda: plugin.execute(params, context=context),
                        )
                else:
                    if asyncio.iscoroutinefunction(plugin.execute):
                        task.result = await plugin.execute(params)
                    else:
                        task.result = await self.loop.run_in_executor(
                            self.executor, plugin.execute, params
                        )
                logger.info(f"Task {task_id} completed successfully")
                task.status = TaskStatus.SUCCESS
                self._broadcast_task_status(task)
                if add_log_callback:
                    add_log_callback(
                        task_id, "INFO",
                        f"Task completed successfully for plugin '{plugin.name}'",
                        source="system",
                    )
            except Exception as e:
                logger.error(f"Task {task_id} failed: {e}")
                task.status = TaskStatus.FAILED
                self._broadcast_task_status(task)
                if add_log_callback:
                    add_log_callback(
                        task_id, "ERROR",
                        f"Task failed: {e}",
                        source="system",
                        metadata={"error_type": type(e).__name__},
                    )
            finally:
                task.finished_at = datetime.now(UTC)
                self.event_bus.flush_task_logs(task_id)
                self.persistence_service.persist_task(task)
                logger.info(
                    f"Task {task_id} execution finished with status: {task.status}"
                )
                logger.reflect(
                    "Task lifecycle reached persisted terminal state",
                    extra={"task_id": task_id, "status": str(task.status)},
                )
                # Broadcast dataset.updated for mapping and documentation tasks (FR-024, R4)
                if task.plugin_id in ("dataset-mapper", "llm_documentation"):
                    dataset_ids = task.params.get("dataset_ids") or [task.params.get("dataset_id")]
                    env_id = task.params.get("env") or task.params.get("environment_id", "")
                    if dataset_ids and env_id:
                        self._broadcast_dataset_updated(env_id, dataset_ids)
    # #endregion _run_task

    # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]
    # @BRIEF Resumes a task that is awaiting mapping.
    # @PRE Task exists and is in AWAITING_MAPPING state.
    # @POST Task status updated to RUNNING, params updated, execution resumed.
    # @RAISES ValueError if task not found or not awaiting mapping.
    async def resolve_task(
        self, task_id: str, resolution_params: dict[str, Any],
    ) -> None:
        with belief_scope("JobLifecycle.resolve_task", f"task_id={task_id}"):
            task = self.graph.get_task(task_id)
            if not task or task.status != TaskStatus.AWAITING_MAPPING:
                raise ValueError("Task is not awaiting mapping.")
            task.params.update(resolution_params)
            task.status = TaskStatus.RUNNING
            self.persistence_service.persist_task(task)
            self._broadcast_task_status(task)
            self.graph.resolve_future(task_id, True)
    # #endregion resolve_task

    # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]
    # @BRIEF Pauses execution and waits for a resolution signal.
    # @PRE Task exists.
    # @POST Execution pauses until future is set.
    async def wait_for_resolution(self, task_id: str) -> None:
        with belief_scope("JobLifecycle.wait_for_resolution", f"task_id={task_id}"):
            task = self.graph.get_task(task_id)
            if not task:
                return
            task.status = TaskStatus.AWAITING_MAPPING
            self.persistence_service.persist_task(task)
            self._broadcast_task_status(task)
            future = self.loop.create_future()
            self.graph.create_future(task_id, future)
            try:
                await future
            finally:
                self.graph.remove_future(task_id)
    # #endregion wait_for_resolution

    # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]
    # @BRIEF Pauses execution and waits for user input.
    # @PRE Task exists.
    # @POST Execution pauses until future is set via resume_task_with_password.
    async def wait_for_input(self, task_id: str) -> None:
        with belief_scope("JobLifecycle.wait_for_input", f"task_id={task_id}"):
            task = self.graph.get_task(task_id)
            if not task:
                return
            future = self.loop.create_future()
            self.graph.create_future(task_id, future)
            try:
                await future
            finally:
                self.graph.remove_future(task_id)
    # #endregion wait_for_input

    # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]
    # @BRIEF Transition a task to AWAITING_INPUT state with input request.
    # @PRE Task exists and is in RUNNING state.
    # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.
    # @RAISES ValueError if task not found or not RUNNING.
    def await_input(
        self, task_id: str, input_request: dict[str, Any],
        add_log_callback=None,
    ) -> None:
        with belief_scope("JobLifecycle.await_input", f"task_id={task_id}"):
            task = self.graph.get_task(task_id)
            if not task:
                raise ValueError(f"Task {task_id} not found")
            if task.status != TaskStatus.RUNNING:
                raise ValueError(
                    f"Task {task_id} is not RUNNING (current: {task.status})"
                )
            task.status = TaskStatus.AWAITING_INPUT
            task.input_required = True
            task.input_request = input_request
            self.persistence_service.persist_task(task)
            self._broadcast_task_status(task)
            if add_log_callback:
                add_log_callback(
                    task_id, "INFO",
                    "Task paused for user input",
                    metadata={"input_request": input_request},
                )
    # #endregion await_input

    # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]
    # @BRIEF Resume a task that is awaiting input with provided passwords.
    # @PRE Task exists and is in AWAITING_INPUT state.
    # @POST Task status changed to RUNNING, passwords injected, task resumed.
    # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.
    def resume_task_with_password(
        self, task_id: str, passwords: dict[str, str],
        add_log_callback=None,
    ) -> None:
        with belief_scope(
            "JobLifecycle.resume_task_with_password", f"task_id={task_id}"
        ):
            task = self.graph.get_task(task_id)
            if not task:
                raise ValueError(f"Task {task_id} not found")
            if task.status != TaskStatus.AWAITING_INPUT:
                raise ValueError(
                    f"Task {task_id} is not AWAITING_INPUT (current: {task.status})"
                )
            if not isinstance(passwords, dict) or not passwords:
                raise ValueError("Passwords must be a non-empty dictionary")
            task.params["passwords"] = passwords
            task.input_required = False
            task.input_request = None
            task.status = TaskStatus.RUNNING
            self.persistence_service.persist_task(task)
            self._broadcast_task_status(task)
            if add_log_callback:
                add_log_callback(
                    task_id, "INFO",
                    "Task resumed with passwords",
                    metadata={"databases": list(passwords.keys())},
                )
            self.graph.resolve_future(task_id, True)
    # #endregion resume_task_with_password

    # #region subscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,subscribe,events,websocket]
    # @BRIEF Subscribe to dataset.updated events for a specific environment.
    async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:
        queue = asyncio.Queue()
        if env_id not in self._dataset_subscribers:
            self._dataset_subscribers[env_id] = []
        self._dataset_subscribers[env_id].append(queue)
        return queue
    # #endregion subscribe_dataset_events

    # #region unsubscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,unsubscribe,events]
    # @BRIEF Unsubscribe from dataset.updated events for a specific environment.
    def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):
        if env_id in self._dataset_subscribers:
            if queue in self._dataset_subscribers[env_id]:
                self._dataset_subscribers[env_id].remove(queue)
            if not self._dataset_subscribers[env_id]:
                del self._dataset_subscribers[env_id]
    # #endregion unsubscribe_dataset_events

    # #region _broadcast_task_status [C:2] [TYPE Function] [SEMANTICS broadcast,status,websocket]
    # @BRIEF Broadcast task status update to all subscribers via EventBus.
    # @POST EventBus.broadcast_status called with serialized task dict.
    def _broadcast_task_status(self, task: Task):
        self.event_bus.broadcast_status(task.id, _task_to_dict(task))
    # #endregion _broadcast_task_status

    # #region _broadcast_dataset_updated [C:3] [TYPE Function] [SEMANTICS dataset,broadcast,event,websocket]
    # @BRIEF Broadcast dataset.updated event to all subscribers for a given environment.
    def _broadcast_dataset_updated(self, env_id: str, dataset_ids: list[int]):
        event = {"type": "dataset.updated", "payload": {"env_id": env_id, "dataset_ids": dataset_ids}}
        if env_id in self._dataset_subscribers:
            for queue in self._dataset_subscribers[env_id]:
                try:
                    self.loop.call_soon_threadsafe(queue.put_nowait, event)
                except Exception:
                    pass
    # #endregion _broadcast_dataset_updated
# #endregion JobLifecycle
# #endregion JobLifecycleModule
