"""Orchestration domain view models plus SQLModel → view adapters.

View models are plain pydantic — no SQLModel imports. The ``to_view`` /
``to_detail`` adapters pull SQLModel in and are the only path that knows about
the DB schema, keeping imports one-directional (SQLModel → pydantic).
"""

from __future__ import annotations

from collections import Counter
from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field

from ..view_models import LogLine, SnowflakeId
from .models import (
    TASK_COMPLETED,
    ExecutionWorker,
    ExecutionWorkerStatus,
    Job,
    JobStatus,
    PreservationMode,
    RegisteredJob,
    RunType,
    Task,
    TaskStatus,
)


class JobView(BaseModel):
    """Compact job representation used by list endpoints."""

    id: SnowflakeId
    name: str
    status: JobStatus
    run_type: RunType
    preservation_mode: PreservationMode
    registered_job_id: SnowflakeId | None = None
    created_at: datetime
    started_at: datetime | None = None
    completed_at: datetime | None = None
    error: str | None = None
    total_tasks: int = 0
    completed_tasks: int = 0


class TaskView(BaseModel):
    """Compact task representation used by list endpoints and ``JobDetail``."""

    id: SnowflakeId
    job_id: SnowflakeId
    entrypoint: str
    name: str
    status: TaskStatus
    attempt: int
    created_at: datetime
    started_at: datetime | None = None
    completed_at: datetime | None = None
    # Surfaced in list views so a terminal status carries its reason at a
    # glance — e.g. a fail-fast group-sibling abort (status ``CANCELLED`` with
    # a sibling-aborted error) reads differently from an operator cancellation
    # (``CANCELLED`` with no error).
    error: str | None = None


class TaskDetail(TaskView):
    """Full task representation used by ``GET /tasks/{id}``."""

    kwargs: dict[str, Any] = Field(default_factory=dict)
    result: dict[str, Any] | None = None
    log_path: str | None = None
    execution_worker_id: SnowflakeId | None = None
    max_retries: int = 0


class TaskLogsView(BaseModel):
    """Captured log lines for a task, served by ``GET /tasks/{id}/logs``."""

    available: bool
    log_path: str | None = None
    lines: list[LogLine] = Field(default_factory=list)


class JobDetail(JobView):
    """Full job representation used by ``GET /jobs/{ref}``."""

    tasks: list[TaskView] = Field(default_factory=list)
    duration_ms: int | None = None


class ClearTaskView(BaseModel):
    """Result of ``POST /tasks/{id}/clear`` — the reactivated job plus the
    full set of tasks that were reset to ``PENDING`` (target + downstream)."""

    job: JobView
    cleared_task_ids: list[SnowflakeId] = Field(default_factory=list)


class TaskStatsView(BaseModel):
    """Per-task execution stats exposed inside ``JobStatsView``."""

    id: SnowflakeId
    entrypoint: str
    status: TaskStatus
    queue_time_ms: int | None = None
    exec_time_ms: int | None = None
    error: str | None = None


class JobStatsView(BaseModel):
    """Execution stats for a job and all its tasks."""

    job_id: SnowflakeId
    job_name: str
    job_status: JobStatus
    total_tasks: int
    status_counts: dict[str, int]
    wall_time_ms: int | None = None
    exec_time_ms: int | None = None
    tasks: list[TaskStatsView] = Field(default_factory=list)


class ExecutionWorkerView(BaseModel):
    """ExecutionWorker representation used by ``GET /execution-workers``."""

    id: SnowflakeId
    hostname: str
    pid: int
    status: ExecutionWorkerStatus
    started_at: datetime
    last_heartbeat: datetime
    tasks_completed: int
    tasks_failed: int


class RegisteredJobView(BaseModel):
    """Registered job representation used by ``GET /registered-jobs``."""

    id: SnowflakeId
    name: str
    entrypoint: str
    enabled: bool
    schedule: str | None = None
    default_kwargs: dict[str, Any] | None = None
    preservation_mode: PreservationMode | None = None
    next_run_at: datetime | None = None
    created_at: datetime
    updated_at: datetime


def _ms_between(start: datetime | None, end: datetime | None) -> int | None:
    if start is None or end is None:
        return None
    return int((end - start).total_seconds() * 1000)


def job_to_view(job: Job, *, total_tasks: int = 0, completed_tasks: int = 0) -> JobView:
    return JobView(
        id=job.id,
        name=job.name,
        status=job.status,
        run_type=job.run_type,
        preservation_mode=job.preservation_mode,
        registered_job_id=job.registered_job_id,
        created_at=job.created_at,
        started_at=job.started_at,
        completed_at=job.completed_at,
        error=job.error,
        total_tasks=total_tasks,
        completed_tasks=completed_tasks,
    )


def task_to_view(task: Task) -> TaskView:
    return TaskView(
        id=task.id,
        job_id=task.job_id,
        entrypoint=task.entrypoint,
        name=task.name,
        status=task.status,
        attempt=task.attempt,
        created_at=task.created_at,
        started_at=task.started_at,
        completed_at=task.completed_at,
        error=task.error,
    )


def task_to_detail(task: Task) -> TaskDetail:
    return TaskDetail(
        id=task.id,
        job_id=task.job_id,
        entrypoint=task.entrypoint,
        name=task.name,
        status=task.status,
        attempt=task.attempt,
        created_at=task.created_at,
        started_at=task.started_at,
        completed_at=task.completed_at,
        kwargs=task.kwargs,
        result=task.result,
        log_path=task.log_path,
        execution_worker_id=task.execution_worker_id,
        error=task.error,
        max_retries=task.max_retries,
    )


def job_to_detail(job: Job, tasks: list[Task]) -> JobDetail:
    completed = sum(1 for t in tasks if t.status == TASK_COMPLETED)
    return JobDetail(
        id=job.id,
        name=job.name,
        status=job.status,
        run_type=job.run_type,
        preservation_mode=job.preservation_mode,
        registered_job_id=job.registered_job_id,
        created_at=job.created_at,
        started_at=job.started_at,
        completed_at=job.completed_at,
        error=job.error,
        tasks=[task_to_view(t) for t in tasks],
        duration_ms=_ms_between(job.started_at, job.completed_at),
        total_tasks=len(tasks),
        completed_tasks=completed,
    )


def clear_to_view(job: Job, cleared_task_ids: list[int]) -> ClearTaskView:
    return ClearTaskView(job=job_to_view(job), cleared_task_ids=cleared_task_ids)


def execution_worker_to_view(worker: ExecutionWorker) -> ExecutionWorkerView:
    return ExecutionWorkerView(
        id=worker.id,
        hostname=worker.hostname,
        pid=worker.pid,
        status=worker.status,
        started_at=worker.started_at,
        last_heartbeat=worker.last_heartbeat,
        tasks_completed=worker.tasks_completed,
        tasks_failed=worker.tasks_failed,
    )


def registered_job_to_view(rj: RegisteredJob) -> RegisteredJobView:
    return RegisteredJobView(
        id=rj.id,
        name=rj.name,
        entrypoint=rj.entrypoint,
        enabled=rj.enabled,
        schedule=rj.schedule,
        default_kwargs=rj.default_kwargs,
        preservation_mode=rj.preservation_mode,
        next_run_at=rj.next_run_at,
        created_at=rj.created_at,
        updated_at=rj.updated_at,
    )


def _short_entrypoint(entrypoint: str) -> str:
    """Extract the short function name from a fully-qualified entrypoint."""
    if ":" in entrypoint:
        return entrypoint.rsplit(":", 1)[-1]
    if "." in entrypoint:
        return entrypoint.rsplit(".", 1)[-1]
    return entrypoint


def task_to_stats_view(task: Task) -> TaskStatsView:
    return TaskStatsView(
        id=task.id,
        entrypoint=_short_entrypoint(task.entrypoint),
        status=task.status,
        queue_time_ms=_ms_between(task.created_at, task.started_at),
        exec_time_ms=_ms_between(task.started_at, task.completed_at),
        error=task.error,
    )


def compute_job_stats_view(job: Job, tasks: list[Task]) -> JobStatsView:
    """Compute the execution-stats view model for a job and its tasks."""
    return JobStatsView(
        job_id=job.id,
        job_name=job.name,
        job_status=job.status,
        total_tasks=len(tasks),
        status_counts=dict(Counter(t.status for t in tasks)),
        wall_time_ms=_ms_between(job.created_at, job.completed_at),
        exec_time_ms=_ms_between(job.started_at, job.completed_at),
        tasks=[task_to_stats_view(t) for t in tasks],
    )
