"""PostgreSQL job queue (design §5). Shared with Laravel, which enqueues by plain INSERT."""
from __future__ import annotations
from datetime import timedelta
from typing import Any

from .db import Jsonb
from .ids import new_id

# Seconds before a running job is considered stale and re-queued (design §5.5).
TIMEOUTS = {"noop": 60, "rasterize": 600, "extract_page": 300, "assemble": 120,
            "score": 900, "pdf": 300, "structure_qs": 600, "rescore": 900}
MAX_ATTEMPTS = {"structure_qs": 2}


class RetryableError(Exception):
    """Transient failure (provider 429/5xx, network). Job is retried with backoff."""


class FatalError(Exception):
    """Permanent failure for this input. Job fails immediately and a flag is raised."""


def enqueue(conn, type_: str, payload: dict[str, Any], *, tenant_id: str | None,
            priority: int = 100, dedupe_key: str | None = None,
            correlation_id: str | None = None) -> str | None:
    """Insert a queued job. With dedupe_key, a second live job for the same key is skipped
    (returns None) — this is what makes 'last page enqueues assemble' race-free."""
    job_id = new_id()
    row = conn.execute(
        """INSERT INTO jobs (id, tenant_id, type, payload_json, priority, max_attempts, dedupe_key, correlation_id)
           VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
           ON CONFLICT (dedupe_key) WHERE status IN ('queued','running') DO NOTHING
           RETURNING id""",
        (job_id, tenant_id, type_, Jsonb(payload), priority, MAX_ATTEMPTS.get(type_, 3),
         dedupe_key, correlation_id or job_id),
    ).fetchone()
    return row["id"] if row else None


def claim(conn, worker_id: str, types: list[str] | None = None) -> dict | None:
    """Claim the next due job atomically. Caller must be in autocommit or commit afterwards."""
    sql = """UPDATE jobs SET status='running', locked_by=%s, locked_at=now(),
                    attempts=attempts+1, updated_at=now()
             WHERE id = (SELECT id FROM jobs
                         WHERE status='queued' AND run_after <= now() {types}
                         ORDER BY priority DESC, created_at
                         LIMIT 1 FOR UPDATE SKIP LOCKED)
             RETURNING *"""
    params: list[Any] = [worker_id]
    if types:
        sql = sql.format(types="AND type = ANY(%s)")
        params.append(types)
    else:
        sql = sql.format(types="")
    return conn.execute(sql, params).fetchone()


def complete(conn, job_id: str, result: dict | None = None) -> None:
    conn.execute("UPDATE jobs SET status='done', result_json=%s, updated_at=now() WHERE id=%s",
                 (Jsonb(result or {}), job_id))


def backoff_seconds(attempts: int) -> int:
    return min(30 * 2 ** (attempts - 1), 900)   # 30s, 60s, 120s ... capped at 15 min


def fail(conn, job: dict, error: str, retryable: bool) -> str:
    """Returns the new status ('queued' for retry, or 'failed')."""
    if retryable and job["attempts"] < job["max_attempts"]:
        conn.execute("""UPDATE jobs SET status='queued', run_after=now() + %s, last_error=%s,
                        locked_by=NULL, locked_at=NULL, updated_at=now() WHERE id=%s""",
                     (timedelta(seconds=backoff_seconds(job["attempts"])), error[:4000], job["id"]))
        return "queued"
    conn.execute("UPDATE jobs SET status='failed', last_error=%s, updated_at=now() WHERE id=%s",
                 (error[:4000], job["id"]))
    return "failed"


def reap_stale(conn) -> int:
    """Re-queue running jobs whose lock is older than their timeout (run every minute)."""
    n = 0
    for type_, secs in TIMEOUTS.items():
        cur = conn.execute("""UPDATE jobs SET status='queued', locked_by=NULL, locked_at=NULL,
                                last_error='stale lock reaped', updated_at=now()
                              WHERE status='running' AND type=%s AND locked_at < now() - %s""",
                           (type_, timedelta(seconds=secs)))
        n += cur.rowcount
    return n
