# ==============================================================================
# PORTED FROM STARSEND: lib/jobs.py
# DATE: 2026-03-29
# NOTE: For historical git blame prior to this date, see the starsend repository.
# ==============================================================================
"""
jobs.py — Job object pattern for crash-recoverable generation.

A Job represents an in-flight or completed generation request. For synchronous
APIs (Google genai), the Job is returned already complete. For async APIs
(Kling REST, fal.ai), the Job tracks submission → processing → completion.

Jobs can be serialized to/from dicts for manifest persistence, enabling
crash recovery by reconnecting to in-flight API jobs on restart.
"""

import time
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
    from typing import Any
    GenerationResult = Any
    ModelClient = Any


@dataclass
class Job:
    """A generation job — submitted, processing, or complete.

    For sync APIs (Google), jobs are born with status="complete" and a result.
    For async APIs (Kling, SeedDance), jobs start as "submitted" and progress
    through "processing" → "complete" or "failed".
    """
    job_id: str
    model: str
    status: str  # "submitted", "processing", "complete", "failed"
    result: Optional["GenerationResult"] = None
    error: Optional[str] = None
    submitted_at: float = field(default_factory=time.time)
    completed_at: Optional[float] = None
    _client: Optional["ModelClient"] = field(default=None, repr=False)

    @classmethod
    def create(cls, model: str, client: Optional["ModelClient"] = None) -> "Job":
        """Create a new submitted job."""
        return cls(
            job_id=uuid.uuid4().hex[:12],
            model=model,
            status="submitted",
            _client=client,
        )

    def wait(self, timeout_s: int = 300) -> "GenerationResult":
        """Block until complete. Concrete behavior defined by client.

        For sync APIs this returns immediately (already complete).
        For async APIs the client polls until done or timeout.

        Raises:
            RuntimeError: If the job has no client (detached) or failed.
        """
        if self.status == "complete" and self.result:
            return self.result
        if self.status == "failed":
            raise RuntimeError(f"Job {self.job_id} failed: {self.error}")
        if self._client is None:
            raise RuntimeError("Cannot wait on a detached Job (no client)")
        return self._client.wait_for_job(self, timeout_s)

    def mark_complete(self, result: "GenerationResult") -> None:
        """Mark the job as complete with a result."""
        self.status = "complete"
        self.result = result
        self.completed_at = time.time()

    def mark_failed(self, error: str) -> None:
        """Mark the job as failed."""
        self.status = "failed"
        self.error = error
        self.completed_at = time.time()

    def to_dict(self) -> dict:
        """Serialize for manifest persistence.

        Does not serialize the result (image bytes are too large) or client.
        The manifest records status + metadata for crash recovery.
        """
        d = {
            "job_id": self.job_id,
            "model": self.model,
            "status": self.status,
            "submitted_at": self.submitted_at,
            "completed_at": self.completed_at,
        }
        if self.error:
            d["error"] = self.error
        if self.result:
            d["result_success"] = self.result.success
            d["result_cost"] = self.result.cost
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "Job":
        """Reconstruct from manifest (without client — needs reconnect).

        The reconstructed job is detached (no client). Call
        client.reconnect_job() to reattach for async polling.
        """
        return cls(
            job_id=data["job_id"],
            model=data["model"],
            status=data["status"],
            submitted_at=data.get("submitted_at", 0.0),
            completed_at=data.get("completed_at"),
            error=data.get("error"),
        )


__all__ = [
    # Public symbols (Phase D — MF-3 + DEBT-9).
    "Job",
]
