"""Durable scheduled-job queue."""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import Any

from sqlalchemy import func, select, update

from app.database.base import utcnow
from app.database.models import JobStatus, JobType, ScheduledJob
from app.database.repositories.base import BaseRepository


class JobRepository(BaseRepository):
    async def enqueue(
        self,
        *,
        job_type: JobType,
        run_at: datetime,
        payload: dict[str, Any],
        max_attempts: int = 5,
        correlation_id: str | None = None,
        idempotency_key: str | None = None,
        post_id: int | None = None,
        delivery_id: int | None = None,
    ) -> tuple[ScheduledJob, bool]:
        async with self.db.session() as session:
            if idempotency_key:
                existing = await session.scalar(
                    select(ScheduledJob).where(ScheduledJob.idempotency_key == idempotency_key)
                )
                if existing is not None:
                    return existing, False
            job = ScheduledJob(
                job_type=job_type,
                status=JobStatus.PENDING,
                run_at=run_at,
                payload=payload,
                max_attempts=max_attempts,
                correlation_id=correlation_id,
                idempotency_key=idempotency_key,
                post_id=post_id,
                delivery_id=delivery_id,
            )
            session.add(job)
            await session.flush()
            return job, True

    async def get(self, job_id: int) -> ScheduledJob | None:
        async with self.db.session() as session:
            return await session.get(ScheduledJob, job_id)

    async def claim_due(self, now: datetime) -> ScheduledJob | None:
        async with self.db.session() as session:
            candidate = await session.scalar(
                select(ScheduledJob)
                .where(ScheduledJob.status == JobStatus.PENDING, ScheduledJob.run_at <= now)
                .order_by(ScheduledJob.run_at, ScheduledJob.id)
                .limit(1)
            )
            if candidate is None:
                return None
            result = await session.execute(
                update(ScheduledJob)
                .where(
                    ScheduledJob.id == candidate.id,
                    ScheduledJob.status == JobStatus.PENDING,
                )
                .values(
                    status=JobStatus.RUNNING,
                    started_at=now,
                    attempts=ScheduledJob.attempts + 1,
                )
            )
            if not result.rowcount:
                return None
            await session.flush()
            return await session.get(ScheduledJob, candidate.id)

    async def complete(self, job_id: int, *, error: str | None = None) -> None:
        async with self.db.session() as session:
            job = await session.get(ScheduledJob, job_id)
            if job is None:
                return
            job.status = JobStatus.DONE
            job.finished_at = utcnow()
            job.last_error = error

    async def fail(
        self,
        job_id: int,
        *,
        error: str,
        retry_at: datetime | None = None,
    ) -> JobStatus:
        async with self.db.session() as session:
            job = await session.get(ScheduledJob, job_id)
            if job is None:
                return JobStatus.FAILED
            job.last_error = error[:2000]
            if retry_at is not None and job.attempts < job.max_attempts:
                job.status = JobStatus.PENDING
                job.run_at = retry_at
                job.finished_at = None
                return JobStatus.PENDING
            job.status = JobStatus.FAILED
            job.finished_at = utcnow()
            return JobStatus.FAILED

    async def cancel(self, job_id: int) -> bool:
        async with self.db.session() as session:
            job = await session.get(ScheduledJob, job_id)
            if job is None or job.status in {JobStatus.DONE, JobStatus.CANCELLED}:
                return False
            job.status = JobStatus.CANCELLED
            job.finished_at = utcnow()
            return True

    async def cancel_for_post(self, post_id: int, job_type: JobType) -> int:
        async with self.db.session() as session:
            result = await session.execute(
                update(ScheduledJob)
                .where(
                    ScheduledJob.post_id == post_id,
                    ScheduledJob.job_type == job_type,
                    ScheduledJob.status == JobStatus.PENDING,
                )
                .values(status=JobStatus.CANCELLED, finished_at=utcnow())
            )
            return int(result.rowcount or 0)

    async def list_pending(self, limit: int = 20) -> list[ScheduledJob]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(ScheduledJob)
                .where(ScheduledJob.status == JobStatus.PENDING)
                .order_by(ScheduledJob.run_at)
                .limit(limit)
            )
            return list(rows)

    async def list_recent(self, limit: int = 20) -> list[ScheduledJob]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(ScheduledJob).order_by(ScheduledJob.created_at.desc()).limit(limit)
            )
            return list(rows)

    async def count_pending(self) -> int:
        async with self.db.session() as session:
            return int(
                await session.scalar(
                    select(func.count(ScheduledJob.id)).where(
                        ScheduledJob.status == JobStatus.PENDING
                    )
                )
                or 0
            )

    async def recover_running(self) -> int:
        async with self.db.session() as session:
            exhausted = await session.execute(
                update(ScheduledJob)
                .where(
                    ScheduledJob.status == JobStatus.RUNNING,
                    ScheduledJob.attempts >= ScheduledJob.max_attempts,
                )
                .values(
                    status=JobStatus.FAILED,
                    last_error="تعداد تلاش‌ها پس از توقف ناگهانی به پایان رسید.",
                    finished_at=utcnow(),
                )
            )
            recovered = await session.execute(
                update(ScheduledJob)
                .where(
                    ScheduledJob.status == JobStatus.RUNNING,
                    ScheduledJob.attempts < ScheduledJob.max_attempts,
                )
                .values(
                    status=JobStatus.PENDING,
                    last_error="پس از راه‌اندازی مجدد بازیابی شد.",
                    run_at=func.coalesce(ScheduledJob.run_at, utcnow()),
                )
            )
            return int((exhausted.rowcount or 0) + (recovered.rowcount or 0))

    async def reset_for_retry(self, job_id: int, delay_seconds: int = 0) -> bool:
        async with self.db.session() as session:
            job = await session.get(ScheduledJob, job_id)
            if job is None:
                return False
            job.status = JobStatus.PENDING
            job.run_at = utcnow() + timedelta(seconds=delay_seconds)
            job.finished_at = None
            return True
