import queue
import threading
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path

from app.db.session import SessionLocal
from app.models import Certificate, CertificateAccessToken, Learner, ProjectCourse
from app.services.pdf import cached_pdf_is_fresh, certificate_template_path, pdf_cache_path, render_certificate_pdf

MAX_PDF_PREGENERATION_JOBS = 30


def _utc_now() -> datetime:
    return datetime.now(UTC).replace(tzinfo=None)


@dataclass
class PdfPregenerationJob:
    id: str
    certificate_ids: list[int]
    concurrency_limit: int
    status: str = "running"
    total: int = 0
    completed: int = 0
    generated: int = 0
    cached: int = 0
    skipped: int = 0
    failed: int = 0
    errors: list[str] = field(default_factory=list)
    created_at: datetime = field(default_factory=_utc_now)
    finished_at: datetime | None = None

    def to_dict(self) -> dict[str, object]:
        percent = round((self.completed / self.total) * 100) if self.total else 0
        return {
            "id": self.id,
            "status": self.status,
            "total": self.total,
            "completed": self.completed,
            "generated": self.generated,
            "cached": self.cached,
            "skipped": self.skipped,
            "failed": self.failed,
            "percent": percent,
            "concurrency_limit": self.concurrency_limit,
            "errors": self.errors[-10:],
            "created_at": self.created_at,
            "finished_at": self.finished_at,
        }


class PdfPregenerationManager:
    def __init__(self) -> None:
        self._jobs: dict[str, PdfPregenerationJob] = {}
        self._lock = threading.Lock()

    def start(self, certificate_ids: list[int], concurrency_limit: int) -> PdfPregenerationJob:
        unique_ids = _unique_ids(certificate_ids)
        if not unique_ids:
            raise ValueError("请选择需要预生成PDF的证书")
        job = PdfPregenerationJob(
            id=uuid.uuid4().hex,
            certificate_ids=unique_ids,
            concurrency_limit=max(1, concurrency_limit),
            total=len(unique_ids),
        )
        with self._lock:
            self._jobs[job.id] = job
            self._trim_jobs()
        thread = threading.Thread(target=self._run_job, args=(job.id,), daemon=True)
        thread.start()
        return job

    def get(self, job_id: str) -> PdfPregenerationJob | None:
        with self._lock:
            return self._jobs.get(job_id)

    def list(self) -> list[PdfPregenerationJob]:
        with self._lock:
            return sorted(self._jobs.values(), key=lambda item: item.created_at, reverse=True)

    def _run_job(self, job_id: str) -> None:
        job = self.get(job_id)
        if not job:
            return

        work_queue: queue.Queue[int] = queue.Queue()
        for certificate_id in job.certificate_ids:
            work_queue.put(certificate_id)

        workers = [
            threading.Thread(target=self._worker, args=(job_id, work_queue), daemon=True)
            for _ in range(min(job.concurrency_limit, job.total))
        ]
        for worker in workers:
            worker.start()
        for worker in workers:
            worker.join()

        with self._lock:
            job.status = "completed_with_errors" if job.failed else "completed"
            job.finished_at = _utc_now()

    def _worker(self, job_id: str, work_queue: queue.Queue[int]) -> None:
        while True:
            try:
                certificate_id = work_queue.get_nowait()
            except queue.Empty:
                return
            try:
                job = self.get(job_id)
                if not job:
                    return
                result = pre_generate_certificate_pdf(certificate_id, job.concurrency_limit)
                self._record(job_id, result)
            except Exception as exc:
                self._record(job_id, "failed", f"证书ID {certificate_id}：{exc}")
            finally:
                work_queue.task_done()

    def _record(self, job_id: str, result: str, error: str | None = None) -> None:
        job = self.get(job_id)
        if not job:
            return
        with self._lock:
            job.completed += 1
            if result == "generated":
                job.generated += 1
            elif result == "cached":
                job.cached += 1
            elif result == "skipped":
                job.skipped += 1
            else:
                job.failed += 1
                if error:
                    job.errors.append(error)

    def _trim_jobs(self) -> None:
        if len(self._jobs) <= MAX_PDF_PREGENERATION_JOBS:
            return
        old_jobs = sorted(self._jobs.values(), key=lambda item: item.created_at)
        for job in old_jobs[: len(self._jobs) - MAX_PDF_PREGENERATION_JOBS]:
            self._jobs.pop(job.id, None)


def pre_generate_certificate_pdf(certificate_id: int, concurrency_limit: int) -> str:
    with SessionLocal() as db:
        certificate = db.get(Certificate, certificate_id)
        if not certificate or certificate.status != "valid":
            return "skipped"
        learner = db.get(Learner, certificate.learner_id)
        token = db.get(CertificateAccessToken, certificate.qr_token_id or certificate.public_token_id)
        project = db.query(ProjectCourse).filter(ProjectCourse.code == certificate.project_code).first()
        if not learner or not token:
            raise ValueError("证书数据不完整")

        was_cached = certificate_pdf_is_ready(certificate)
        render_certificate_pdf(certificate, learner, project, token, concurrency_limit=concurrency_limit)
        db.commit()
        return "cached" if was_cached else "generated"


def certificate_pdf_is_ready(certificate: Certificate) -> bool:
    output_path = pdf_cache_path(certificate.certificate_no)
    template_path = certificate_template_path()
    latest_renderer_mtime = max(template_path.stat().st_mtime, Path(__file__).with_name("pdf.py").stat().st_mtime)
    return cached_pdf_is_fresh(output_path) and output_path.stat().st_mtime >= latest_renderer_mtime


def _unique_ids(values: list[int]) -> list[int]:
    seen: set[int] = set()
    result: list[int] = []
    for value in values:
        if value not in seen:
            seen.add(value)
            result.append(value)
    return result


pdf_pregeneration_manager = PdfPregenerationManager()
