"""Run launching: glue between the API and the agent runner.

POST /agents/{id}/run schedules execution here. In production the run executes in
the background so the request returns immediately; tests flip ``agent_run_inline``
so the run completes deterministically before the response. The LLM client is
resolved through a swappable factory so tests can inject a scripted fake.
"""

from __future__ import annotations

import asyncio
import uuid
from collections.abc import Callable

from sqlalchemy.orm import Session

from legate.agent.enums import RunStatus
from legate.agent.runner import AgentRunner
from legate.config import get_settings
from legate.db.models.agent import Agent
from legate.db.models.approval import Approval
from legate.db.models.run import Run
from legate.llm.base import LLMClient
from legate.llm.factory import build_llm_client
from legate.observability.logging import get_logger

logger = get_logger(__name__)

# (provider, model) -> client. Overridable in tests via set_llm_factory.
LLMFactory = Callable[[str, str], LLMClient]
_llm_factory: LLMFactory | None = None


def set_llm_factory(factory: LLMFactory | None) -> None:
    global _llm_factory
    _llm_factory = factory


def _resolve_llm(provider: str, model: str) -> LLMClient:
    if _llm_factory is not None:
        return _llm_factory(provider, model)
    return build_llm_client(provider)


def _build_embedder():
    """Build the embedder, or None if it cannot be configured (kb.search degrades)."""
    try:
        from legate.rag.embeddings.factory import build_embedder

        return build_embedder()
    except Exception:
        logger.warning("embedder unavailable; kb.search will return no results", exc_info=True)
        return None


def _build_vector_store():
    try:
        from legate.rag.vectorstore.factory import build_vector_store

        return build_vector_store()
    except Exception:
        logger.warning("vector store unavailable; kb.search will return no results", exc_info=True)
        return None


def _build_runner(db: Session, run: Run, agent: Agent) -> AgentRunner:
    from legate.connectors.resolution import build_workspace_tool_registry

    return AgentRunner(
        db=db,
        llm=_resolve_llm(agent.provider, agent.model),
        registry=build_workspace_tool_registry(db, run.workspace_id),
        embedder=_build_embedder(),
        vector_store=_build_vector_store(),
    )


async def execute_run_by_id(run_id: uuid.UUID, session_factory: Callable[[], Session]) -> None:
    """Load a run + its agent in a fresh session and drive it to completion."""
    db = session_factory()
    try:
        run = db.get(Run, run_id)
        if run is None:
            logger.warning("run %s not found", run_id)
            return
        agent = db.get(Agent, run.agent_id) if run.agent_id else None
        if agent is None:
            run.status = RunStatus.FAILED
            run.error = "agent not found"
            db.add(run)
            db.commit()
            return
        await _build_runner(db, run, agent).execute(run, agent)
    except Exception:  # pragma: no cover - defensive; runner handles its own errors
        logger.exception("run %s crashed", run_id)
    finally:
        db.close()


async def resume_run_by_id(
    run_id: uuid.UUID,
    approval_id: uuid.UUID,
    approved: bool,
    session_factory: Callable[[], Session],
) -> None:
    """Continue a suspended agent or workflow run after an approval decision."""
    db = session_factory()
    try:
        run = db.get(Run, run_id)
        approval = db.get(Approval, approval_id)
        if run is None or approval is None:
            logger.warning("cannot resume run %s (missing run/approval)", run_id)
            return
        if run.workflow_id is not None:
            from legate.db.models.workflow import Workflow
            from legate.workflow.executor import WorkflowExecutor

            workflow = db.get(Workflow, run.workflow_id)
            if workflow is None:
                logger.warning("cannot resume workflow run %s (missing workflow)", run_id)
                return
            await WorkflowExecutor(db=db).resume(run, workflow, approval, approved=approved)
            return
        agent = db.get(Agent, run.agent_id) if run.agent_id else None
        if agent is None:
            logger.warning("cannot resume run %s (missing agent)", run_id)
            return
        await _build_runner(db, run, agent).resume(run, agent, approval, approved=approved)
    except Exception:  # pragma: no cover - defensive; runner handles its own errors
        logger.exception("resume of run %s crashed", run_id)
    finally:
        db.close()


async def launch_run(run_id: uuid.UUID, session_factory: Callable[[], Session]) -> None:
    """Run inline (tests) or schedule as a background task (production)."""
    if get_settings().agent_run_inline:
        await execute_run_by_id(run_id, session_factory)
    else:
        asyncio.create_task(execute_run_by_id(run_id, session_factory))


async def launch_resume(
    run_id: uuid.UUID,
    approval_id: uuid.UUID,
    approved: bool,
    session_factory: Callable[[], Session],
) -> None:
    if get_settings().agent_run_inline:
        await resume_run_by_id(run_id, approval_id, approved, session_factory)
    else:
        asyncio.create_task(resume_run_by_id(run_id, approval_id, approved, session_factory))
