from __future__ import annotations

import json
import os
import time
import uuid
from dataclasses import asdict
from pathlib import Path
from typing import Protocol

import pdfplumber
import pypdfium2 as pdfium
import pytesseract

from .analyzer import analyze_pdf
from .dependencies import check_tesseract
from .models import ConversionResult, DependencyStatus
from .paths import ensure_dir, output_stem


class CancellationToken(Protocol):
    def is_set(self) -> bool: ...


class ConversionCancelled(RuntimeError):
    pass


def raise_if_cancelled(cancel_event: CancellationToken | None) -> None:
    if cancel_event and cancel_event.is_set():
        raise ConversionCancelled("OCR conversion cancelled by user.")


def normalize_markdown(source_file: str, pages: list[str], warnings: list[str]) -> str:
    body: list[str] = [
        "---",
        f"source_file: {source_file}",
        "converter: pdf-to-md-local-prototype",
        "---",
        "",
    ]
    if warnings:
        body.extend(["## OCR review warnings", ""])
        body.extend(f"- {warning}" for warning in warnings)
        body.append("")
    for index, text in enumerate(pages, start=1):
        body.extend([f"## Page {index}", "", text.strip() or "[No text extracted]", ""])
    return "\n".join(body).replace("\r\n", "\n")


def write_quality_report(output_dir: Path, quality: dict) -> str:
    path = ensure_dir(output_dir) / "quality-report.json"
    path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
    return str(path)


def download_filename(source_file: str) -> str:
    return f"{output_stem(Path(source_file))} for AI.md"


def _extract_digital_text(input_path: Path) -> list[str]:
    with pdfplumber.open(str(input_path)) as pdf:
        return [(page.extract_text() or "") for page in pdf.pages]


def _extract_ocr_text(input_path: Path, deps: DependencyStatus, cancel_event: CancellationToken | None = None) -> list[str]:
    if deps.tesseract_path:
        pytesseract.pytesseract.tesseract_cmd = deps.tesseract_path
    if deps.tessdata_prefix:
        os.environ["TESSDATA_PREFIX"] = deps.tessdata_prefix
    document = pdfium.PdfDocument(str(input_path))
    pages: list[str] = []
    for index in range(len(document)):
        raise_if_cancelled(cancel_event)
        bitmap = document[index].render(scale=2).to_pil()
        pages.append(pytesseract.image_to_string(bitmap, lang="vie"))
        raise_if_cancelled(cancel_event)
    return pages


def convert_pdf(
    input_path: Path,
    output_dir: Path,
    mode: str = "auto",
    job_id: str | None = None,
    cancel_event: CancellationToken | None = None,
) -> ConversionResult:
    started = time.perf_counter()
    input_path = Path(input_path)
    output_dir = ensure_dir(Path(output_dir))
    job_id = job_id or uuid.uuid4().hex[:12]
    analysis = analyze_pdf(input_path)
    route = analysis.recommended_route if mode == "auto" else ("digital_text" if mode == "digital_text" else "local_ocr")
    deps = check_tesseract()
    warnings = list(analysis.warnings)
    if route == "local_ocr":
        warnings.extend(deps.warnings)

    stem = output_stem(input_path)
    log_path = output_dir / f"{stem} conversion.log"
    report_path = output_dir / f"{stem} quality-report.json"
    markdown_filename = download_filename(input_path.name)
    markdown_path = output_dir / markdown_filename

    try:
        raise_if_cancelled(cancel_event)
        if route == "local_ocr" and not (deps.tesseract_available and deps.vie_available):
            raise RuntimeError("Local OCR requires Tesseract with Vietnamese language data.")
        pages = _extract_ocr_text(input_path, deps, cancel_event) if route == "local_ocr" else _extract_digital_text(input_path)
        raise_if_cancelled(cancel_event)
        low_output = sum(len(page.strip()) for page in pages) < max(100, analysis.page_count * 20)
        if low_output:
            warnings.append("low_output_text")
        markdown = normalize_markdown(input_path.name, pages, warnings)
        markdown_path.write_text(markdown, encoding="utf-8")
        quality = {
            "status": "ok",
            "input_file": str(input_path),
            "route": route,
            "page_count": analysis.page_count,
            "output_chars": len(markdown),
            "seconds": round(time.perf_counter() - started, 2),
            "analysis": asdict(analysis),
            "dependencies": asdict(deps),
            "warnings": warnings,
        }
        report_path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
        log_path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
        return ConversionResult(
            "ok",
            job_id,
            route,
            str(markdown_path),
            str(report_path),
            str(log_path),
            warnings,
            quality,
            markdown=markdown,
            markdown_filename=markdown_filename,
        )
    except Exception as exc:
        warnings.append("manual_review_required")
        error = "OCR conversion cancelled by user." if isinstance(exc, ConversionCancelled) else str(exc)
        quality = {
            "status": "failed",
            "input_file": str(input_path),
            "route": route,
            "page_count": analysis.page_count,
            "seconds": round(time.perf_counter() - started, 2),
            "analysis": asdict(analysis),
            "dependencies": asdict(deps),
            "warnings": warnings,
            "error": error,
        }
        report_path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
        log_path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
        return ConversionResult("failed", job_id, route, None, str(report_path), str(log_path), warnings, quality, error)
