"""Task executor — async queue for dispatching tool jobs.

Manages a queue of ToolJob instances, runs them sequentially or with
configurable concurrency, and tracks results.
"""

from __future__ import annotations

import asyncio
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

import structlog

from proxym.dashboard.tickets import Ticket, TicketStatus
from proxym.tools import ToolDefinition, ToolRegistry
from proxym.observability import log_call
from proxym.tools.adapters import ToolResult, get_adapter
from proxym.tools.prompt_builder import build_prompt

logger = structlog.get_logger()


class JobStatus(str, Enum):
    QUEUED = "queued"
    RUNNING = "running"
    DONE = "done"
    FAILED = "failed"
    CANCELLED = "cancelled"


@dataclass
class ToolJob:
    """A single tool execution job."""
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    ticket_id: str = ""
    tool_name: str = ""
    mode: str = ""
    model: str = ""
    project_dir: str = ""
    prompt: str = ""
    status: JobStatus = JobStatus.QUEUED
    result: ToolResult | None = None
    created_at: float = field(default_factory=time.time)
    started_at: float = 0
    finished_at: float = 0
    error: str = ""

    def summary(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "ticket_id": self.ticket_id,
            "tool": self.tool_name,
            "mode": self.mode,
            "status": self.status.value,
            "created_at": self.created_at,
            "started_at": self.started_at,
            "finished_at": self.finished_at,
            "duration_s": round(self.finished_at - self.started_at, 2) if self.started_at else 0,
            "result": self.result.summary() if self.result else None,
            "error": self.error,
        }


class ToolExecutor:
    """Async executor for tool jobs with a bounded queue."""

    _instance: ToolExecutor | None = None

    def __init__(self, max_concurrent: int = 1) -> None:
        self._queue: asyncio.Queue[ToolJob] = asyncio.Queue(maxsize=100)
        self._jobs: dict[str, ToolJob] = {}
        self._max_concurrent = max_concurrent
        self._running = False
        self._worker_task: asyncio.Task | None = None
        logger.info("tool_executor_init", max_concurrent=max_concurrent)

    @classmethod
    def get(cls) -> ToolExecutor:
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    async def start(self) -> None:
        """Start the background worker loop."""
        if self._running:
            return
        self._running = True
        self._worker_task = asyncio.create_task(self._worker_loop())
        logger.info("tool_executor_started")

    async def stop(self) -> None:
        """Stop the worker loop gracefully."""
        self._running = False
        if self._worker_task:
            self._worker_task.cancel()
            try:
                await self._worker_task
            except asyncio.CancelledError:
                pass
            except RuntimeError:
                # Task is from a different event loop (test cleanup)
                pass
        logger.info("tool_executor_stopped")

    def enqueue(
        self,
        ticket: Ticket,
        tool: ToolDefinition,
        project_dir: str,
        mode: str = "",
        model: str = "",
    ) -> ToolJob:
        """Create and enqueue a new job. Returns the job (status=queued)."""
        prompt = build_prompt(ticket, tool, mode)
        job = ToolJob(
            ticket_id=ticket.id,
            tool_name=tool.name,
            mode=mode or (tool.modes[0] if tool.modes else "code"),
            model=model or tool.default_model,
            project_dir=project_dir,
            prompt=prompt,
        )
        self._jobs[job.id] = job
        try:
            self._queue.put_nowait(job)
        except asyncio.QueueFull:
            job.status = JobStatus.FAILED
            job.error = "Queue full"
        logger.info("tool_job_enqueued", job_id=job.id, tool=tool.name, ticket=ticket.id)
        return job

    def get_job(self, job_id: str) -> ToolJob | None:
        return self._jobs.get(job_id)

    def get_jobs_for_ticket(self, ticket_id: str) -> list[ToolJob]:
        """Return all jobs for a ticket, most recent first."""
        jobs = [j for j in self._jobs.values() if j.ticket_id == ticket_id]
        jobs.sort(key=lambda j: j.created_at, reverse=True)
        return jobs

    def list_jobs(
        self, status: JobStatus | None = None, limit: int = 50,
    ) -> list[ToolJob]:
        jobs = list(self._jobs.values())
        if status:
            jobs = [j for j in jobs if j.status == status]
        jobs.sort(key=lambda j: j.created_at, reverse=True)
        return jobs[:limit]

    def cancel_job(self, job_id: str) -> bool:
        """Cancel a queued job. Returns False if not found or already running."""
        job = self._jobs.get(job_id)
        if not job or job.status != JobStatus.QUEUED:
            return False
        job.status = JobStatus.CANCELLED
        job.finished_at = time.time()
        return True

    def queue_summary(self) -> dict[str, Any]:
        """Summary stats for the queue."""
        jobs = list(self._jobs.values())
        by_status = {}
        for j in jobs:
            by_status[j.status.value] = by_status.get(j.status.value, 0) + 1
        return {
            "total_jobs": len(jobs),
            "by_status": by_status,
            "queue_size": self._queue.qsize(),
            "running": self._running,
            "max_concurrent": self._max_concurrent,
        }

    async def _worker_loop(self) -> None:
        """Main worker loop — pulls jobs from queue and executes them."""
        sem = asyncio.Semaphore(self._max_concurrent)
        while self._running:
            try:
                job = await asyncio.wait_for(self._queue.get(), timeout=1.0)
            except (asyncio.TimeoutError, asyncio.CancelledError):
                continue

            if job.status == JobStatus.CANCELLED:
                continue

            async with sem:
                await self._execute_job(job)

    @log_call(level="info", slow_threshold_ms=60000)
    async def _execute_job(self, job: ToolJob) -> None:
        """Execute a single job."""
        job.status = JobStatus.RUNNING
        job.started_at = time.time()
        logger.info("tool_job_started", job_id=job.id, tool=job.tool_name)
        self._sync_ticket_execution(job)

        adapter = get_adapter(job.tool_name)
        if not adapter:
            job.status = JobStatus.FAILED
            job.error = f"No adapter for tool '{job.tool_name}'"
            job.finished_at = time.time()
            logger.warning("tool_job_no_adapter", job_id=job.id, tool=job.tool_name)
            self._sync_ticket_execution(job)
            return

        try:
            result = await adapter.run(
                prompt=job.prompt,
                project_dir=job.project_dir,
                ticket_id=job.ticket_id,
                mode=job.mode,
                model=job.model,
            )
            job.result = result
            job.status = JobStatus.DONE if result.success else JobStatus.FAILED
            if not result.success:
                job.error = result.stderr[:500]
        except Exception as exc:
            job.status = JobStatus.FAILED
            job.error = str(exc)
        finally:
            job.finished_at = time.time()
            logger.info("tool_job_finished", job_id=job.id, status=job.status.value,
                       duration=f"{job.finished_at - job.started_at:.1f}s")
            self._sync_ticket_execution(job)

    def _sync_ticket_execution(self, job: ToolJob) -> None:
        """Write job status back to the ticket's last_execution field."""
        if not job.ticket_id:
            return
        try:
            from proxym.dashboard.tickets import JobExecution, TicketManager
            from proxym.dashboard._tickets_api import _tickets
            mgr = _tickets()
            files = []
            stdout_tail = ""
            if job.result:
                files = getattr(job.result, "files_changed", []) or []
                stdout_tail = (getattr(job.result, "stdout", "") or "")[-500:]
            execution = JobExecution(
                job_id=job.id,
                tool=job.tool_name,
                status=job.status.value,
                started_at=job.started_at,
                finished_at=job.finished_at,
                duration_s=round(job.finished_at - job.started_at, 2) if job.started_at else 0,
                error=job.error,
                files_changed=files,
                stdout_tail=stdout_tail,
            )
            updates: dict[str, Any] = {"last_execution": execution}
            ticket = mgr.get_ticket(job.ticket_id)
            if (
                job.status == JobStatus.DONE
                and ticket is not None
                and ticket.status in {
                    TicketStatus.BACKLOG,
                    TicketStatus.TODO,
                    TicketStatus.IN_PROGRESS,
                }
            ):
                updates["status"] = TicketStatus.IN_REVIEW.value
            mgr.update_ticket(job.ticket_id, updates)
        except Exception as exc:
            logger.debug("sync_ticket_exec_failed", job_id=job.id, error=str(exc))
