"""Background task observer: fire-and-forget CLI execution with notification."""

from __future__ import annotations

import asyncio
import contextlib
import logging
import secrets
import time
from collections.abc import Awaitable, Callable
from datetime import datetime, timezone
from typing import TYPE_CHECKING

from sygen_bot.background.models import BackgroundResult, BackgroundSubmit, BackgroundTask
from sygen_bot.i18n import t
from sygen_bot.infra.task_runner import run_oneshot_task

if TYPE_CHECKING:
    from sygen_bot.cli.param_resolver import TaskExecutionConfig
    from sygen_bot.cli.service import CLIService
    from sygen_bot.workspace.paths import SygenPaths

logger = logging.getLogger(__name__)

BgResultCallback = Callable[[BackgroundResult], Awaitable[None]]

MAX_TASKS_PER_CHAT = 5


class BackgroundObserver:
    """Manages fire-and-forget background CLI tasks."""

    def __init__(
        self,
        paths: SygenPaths,
        *,
        timeout_seconds: float,
        cli_service: CLIService | None = None,
        trace_retention_days: int = 30,
        trace_max_files: int = 1000,
        agent_name: str = "",
    ) -> None:
        self._paths = paths
        self._timeout_seconds = timeout_seconds
        self._cli_service = cli_service
        self._trace_retention_days = trace_retention_days
        self._trace_max_files = trace_max_files
        self._agent_name = agent_name
        self._on_result: BgResultCallback | None = None
        self._tasks: dict[str, BackgroundTask] = {}

    def set_result_handler(self, handler: BgResultCallback) -> None:
        self._on_result = handler

    def submit(
        self,
        sub: BackgroundSubmit,
        exec_config: TaskExecutionConfig,
    ) -> str:
        """Submit a background task. Returns task_id."""
        active = sum(
            1
            for t in self._tasks.values()
            if t.chat_id == sub.chat_id and t.asyncio_task and not t.asyncio_task.done()
        )
        if active >= MAX_TASKS_PER_CHAT:
            msg = t("tasks.too_many", max=MAX_TASKS_PER_CHAT)
            raise ValueError(msg)

        task_id = secrets.token_hex(4)
        has_session_override = bool(sub.provider_override)
        bg_task = BackgroundTask(
            task_id=task_id,
            chat_id=sub.chat_id,
            prompt=sub.prompt,
            message_id=sub.message_id,
            thread_id=sub.thread_id,
            provider=sub.provider_override if has_session_override else exec_config.provider,
            model=sub.model_override if has_session_override else exec_config.model,
            submitted_at=time.monotonic(),
            session_name=sub.session_name,
            resume_session_id=sub.resume_session_id,
        )
        atask = asyncio.create_task(self._run(bg_task, exec_config))
        bg_task.asyncio_task = atask
        atask.add_done_callback(lambda _t: self._tasks.pop(task_id, None))
        self._tasks[task_id] = bg_task
        logger.info(
            "Background task submitted id=%s chat=%d provider=%s session=%s",
            task_id,
            sub.chat_id,
            bg_task.provider,
            sub.session_name or "<stateless>",
        )
        return task_id

    def active_tasks(self, chat_id: int | None = None) -> list[BackgroundTask]:
        tasks = [t for t in self._tasks.values() if t.asyncio_task and not t.asyncio_task.done()]
        if chat_id is not None:
            tasks = [t for t in tasks if t.chat_id == chat_id]
        return tasks

    async def cancel_all(self, chat_id: int) -> int:
        count = 0
        cancelled: list[asyncio.Task[None]] = []
        for task in list(self._tasks.values()):
            if task.chat_id == chat_id and task.asyncio_task and not task.asyncio_task.done():
                task.asyncio_task.cancel()
                cancelled.append(task.asyncio_task)
                count += 1
        if cancelled:
            await asyncio.gather(*cancelled, return_exceptions=True)
        return count

    async def shutdown(self) -> None:
        cancelled: list[asyncio.Task[None]] = []
        for task in list(self._tasks.values()):
            if task.asyncio_task and not task.asyncio_task.done():
                task.asyncio_task.cancel()
                cancelled.append(task.asyncio_task)
        if cancelled:
            await asyncio.gather(*cancelled, return_exceptions=True)
        self._tasks.clear()

    async def _run(self, bg_task: BackgroundTask, exec_config: TaskExecutionConfig) -> None:
        if bg_task.session_name and self._cli_service:
            await self._run_with_session(bg_task)
        else:
            await self._run_oneshot(bg_task, exec_config)

    async def _run_oneshot(self, bg_task: BackgroundTask, exec_config: TaskExecutionConfig) -> None:
        """Legacy stateless execution via run_oneshot_task."""
        t0 = time.monotonic()
        try:
            result = await run_oneshot_task(
                exec_config,
                bg_task.prompt,
                cwd=self._paths.workspace,
                timeout_seconds=self._timeout_seconds,
                timeout_label="Background task",
            )

            elapsed = time.monotonic() - t0
            bg_result = BackgroundResult(
                task_id=bg_task.task_id,
                chat_id=bg_task.chat_id,
                message_id=bg_task.message_id,
                thread_id=bg_task.thread_id,
                prompt_preview=bg_task.prompt[:60],
                result_text=result.result_text,
                status="error:cli_not_found" if result.execution is None else result.status,
                elapsed_seconds=elapsed,
                provider=bg_task.provider,
                model=bg_task.model,
            )
            await self._deliver(bg_result)
            self._record_trace(bg_task, bg_result)
        except asyncio.CancelledError:
            elapsed = time.monotonic() - t0
            with contextlib.suppress(Exception):
                await self._deliver(
                    BackgroundResult(
                        task_id=bg_task.task_id,
                        chat_id=bg_task.chat_id,
                        message_id=bg_task.message_id,
                        thread_id=bg_task.thread_id,
                        prompt_preview=bg_task.prompt[:60],
                        result_text="",
                        status="aborted",
                        elapsed_seconds=elapsed,
                        provider=bg_task.provider,
                        model=bg_task.model,
                    )
                )
            raise
        except Exception:
            logger.exception("Background task failed id=%s", bg_task.task_id)
            elapsed = time.monotonic() - t0
            with contextlib.suppress(Exception):
                await self._deliver(
                    BackgroundResult(
                        task_id=bg_task.task_id,
                        chat_id=bg_task.chat_id,
                        message_id=bg_task.message_id,
                        thread_id=bg_task.thread_id,
                        prompt_preview=bg_task.prompt[:60],
                        result_text=t("tasks.internal_error"),
                        status="error:internal",
                        elapsed_seconds=elapsed,
                        provider=bg_task.provider,
                        model=bg_task.model,
                    )
                )

    async def _run_with_session(self, bg_task: BackgroundTask) -> None:
        """Named session execution via CLIService with resume support."""
        from sygen_bot.cli.types import AgentRequest

        assert self._cli_service is not None

        t0 = time.monotonic()
        process_label = f"ns:{bg_task.session_name}"
        try:
            request = AgentRequest(
                prompt=bg_task.prompt,
                model_override=bg_task.model or None,
                provider_override=bg_task.provider or None,
                chat_id=bg_task.chat_id,
                process_label=process_label,
                resume_session=bg_task.resume_session_id or None,
                timeout_seconds=self._timeout_seconds,
            )
            response = await self._cli_service.execute(request)

            elapsed = time.monotonic() - t0
            status = "ok"
            if response.is_error:
                status = "error:cli"
                if response.timed_out:
                    status = "error:timeout"

            bg_result = BackgroundResult(
                task_id=bg_task.task_id,
                chat_id=bg_task.chat_id,
                message_id=bg_task.message_id,
                thread_id=bg_task.thread_id,
                prompt_preview=bg_task.prompt[:60],
                result_text=response.result or "",
                status=status,
                elapsed_seconds=elapsed,
                provider=bg_task.provider,
                model=bg_task.model,
                session_name=bg_task.session_name,
                session_id=response.session_id or "",
            )
            await self._deliver(bg_result)
            self._record_trace(bg_task, bg_result)
        except asyncio.CancelledError:
            elapsed = time.monotonic() - t0
            with contextlib.suppress(Exception):
                await self._deliver(
                    BackgroundResult(
                        task_id=bg_task.task_id,
                        chat_id=bg_task.chat_id,
                        message_id=bg_task.message_id,
                        thread_id=bg_task.thread_id,
                        prompt_preview=bg_task.prompt[:60],
                        result_text="",
                        status="aborted",
                        elapsed_seconds=elapsed,
                        provider=bg_task.provider,
                        model=bg_task.model,
                        session_name=bg_task.session_name,
                    )
                )
            raise
        except Exception:
            logger.exception(
                "Named session task failed id=%s name=%s", bg_task.task_id, bg_task.session_name
            )
            elapsed = time.monotonic() - t0
            with contextlib.suppress(Exception):
                await self._deliver(
                    BackgroundResult(
                        task_id=bg_task.task_id,
                        chat_id=bg_task.chat_id,
                        message_id=bg_task.message_id,
                        thread_id=bg_task.thread_id,
                        prompt_preview=bg_task.prompt[:60],
                        result_text=t("tasks.internal_error"),
                        status="error:internal",
                        elapsed_seconds=elapsed,
                        provider=bg_task.provider,
                        model=bg_task.model,
                        session_name=bg_task.session_name,
                    )
                )

    def _record_trace(self, bg_task: BackgroundTask, result: BackgroundResult) -> None:
        from sygen_bot.observability.recorder import record_trace

        now = datetime.now(timezone.utc)
        started = datetime.fromtimestamp(
            time.time() - result.elapsed_seconds, tz=timezone.utc
        )
        record_trace(
            self._paths.logs_dir,
            trace_type="task",
            name=bg_task.session_name or bg_task.task_id,
            started=started,
            finished=now,
            duration_sec=result.elapsed_seconds,
            status=result.status,
            provider=result.provider,
            model=result.model,
            error=result.status if result.status != "ok" and not result.status.startswith("ok") else None,
            summary=result.result_text[:200] if result.result_text else None,
            retention_days=self._trace_retention_days,
            max_files=self._trace_max_files,
            agent_name=self._agent_name,
        )

    async def _deliver(self, result: BackgroundResult) -> None:
        if self._on_result is None:
            logger.warning("No result handler set for background task %s", result.task_id)
            return
        try:
            await self._on_result(result)
        except Exception:
            logger.exception("Error delivering background result id=%s", result.task_id)
