"""
Review API — the side-by-side review foundation.

Endpoints (per translation job):
  GET   /api/v1/translations/{job_id}/segments      list segments for review
  PATCH /api/v1/segments/{segment_id}               edit / approve / reject
  POST  /api/v1/translations/{job_id}/rebuild       rebuild the document with
                                                    approved/edited segments
  GET   /api/v1/translations/{job_id}/xliff         export XLIFF 1.2
  POST  /api/v1/translations/{job_id}/xliff         import XLIFF back

Access mirrors the job endpoints: owner (JWT/API key) or the job's secret
token. Approvals also feed the per-user translation memory so future jobs
reuse human-reviewed translations.
"""

import asyncio
from pathlib import Path
from typing import Optional, List

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import JSONResponse, PlainTextResponse
from pydantic import BaseModel, Field

from core.logging import get_logger
from database.connection import get_sync_session
from database.models import TranslationSegment
from middleware.api_key_auth import get_authenticated_user
from routes.translate_routes import (
    _check_job_access,
    _translation_jobs,
)

logger = get_logger(__name__)

router = APIRouter(prefix="/api/v1", tags=["Review"])

XLIFF_NS = "urn:oasis:names:tc:xliff:document:1.2"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _load_job(job_id: str) -> Optional[dict]:
    job = _translation_jobs.get(job_id)
    if job:
        return job
    from core.redis import get_job_status_async

    return await get_job_status_async(job_id)


async def _job_or_403(job_id: str, current_user, token: Optional[str]) -> dict:
    job = await _load_job(job_id)
    if not job:
        raise HTTPException(status_code=404, detail={"error": "JOB_NOT_FOUND"})
    denied = _check_job_access(job, current_user, token)
    if denied:
        # Convert JSONResponse to HTTPException semantics via raise
        raise HTTPException(
            status_code=denied.status_code, detail=denied.body
        )
    return job


def _fetch_segments(job_id: str) -> List[TranslationSegment]:
    with get_sync_session() as session:
        rows = (
            session.query(TranslationSegment)
            .filter(TranslationSegment.job_id == job_id)
            .order_by(TranslationSegment.segment_index.asc())
            .all()
        )
        # Detach into plain dicts before the session closes
        return [r.to_dict() for r in rows]


# ---------------------------------------------------------------------------
# Segment list
# ---------------------------------------------------------------------------
@router.get("/translations/{job_id}/segments")
async def list_segments(
    job_id: str,
    token: Optional[str] = Query(None),
    current_user=Depends(get_authenticated_user),
):
    job = await _job_or_403(job_id, current_user, token)
    segments = await asyncio.to_thread(_fetch_segments, job_id)
    return JSONResponse(
        status_code=200,
        content={
            "data": {
                "job_id": job_id,
                "file_name": job.get("file_name"),
                "status": job.get("status"),
                "segments": segments,
                "counts": {
                    "total": len(segments),
                    "pending": sum(1 for s in segments if s["status"] == "pending"),
                    "approved": sum(1 for s in segments if s["status"] == "approved"),
                    "edited": sum(1 for s in segments if s["status"] == "edited"),
                },
            },
            "meta": {},
        },
    )


# ---------------------------------------------------------------------------
# Segment edit / approve
# ---------------------------------------------------------------------------
class SegmentUpdate(BaseModel):
    reviewed_text: Optional[str] = None
    status: Optional[str] = Field(None, description="pending | approved | edited")


VALID_STATUSES = {"pending", "approved", "edited"}


@router.patch("/segments/{segment_id}")
async def update_segment(
    segment_id: str,
    update: SegmentUpdate,
    token: Optional[str] = Query(None),
    current_user=Depends(get_authenticated_user),
):
    def _apply():
        with get_sync_session() as session:
            seg = (
                session.query(TranslationSegment)
                .filter(TranslationSegment.id == segment_id)
                .first()
            )
            if not seg:
                return None
            # Segments are only persisted for authenticated users —
            # ownership is checked directly on the row.
            if not current_user or str(seg.user_id) != str(current_user.id):
                raise PermissionError("access denied")

            if update.reviewed_text is not None:
                seg.reviewed_text = update.reviewed_text
            if update.status is not None:
                if update.status not in VALID_STATUSES:
                    raise ValueError("invalid status")
                seg.status = update.status
                # Editing implies the reviewed text is the new translation
                if update.status == "edited" and seg.reviewed_text is None:
                    raise ValueError("reviewed_text required for status=edited")
                if update.status == "pending":
                    seg.reviewed_text = None
            session.commit()
            refreshed = (
                session.query(TranslationSegment)
                .filter(TranslationSegment.id == segment_id)
                .first()
            )
            return refreshed.to_dict()

    try:
        result = await asyncio.to_thread(_apply)
    except PermissionError:
        raise HTTPException(status_code=403, detail={"error": "ACCESS_DENIED"})
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail={"error": "SEGMENT_NOT_FOUND"})
    except ValueError as e:
        raise HTTPException(status_code=422, detail={"error": str(e)})

    if result is None:
        raise HTTPException(status_code=404, detail={"error": "SEGMENT_NOT_FOUND"})

    # Approved/edited segments feed the per-user translation memory so the
    # next jobs reuse human-reviewed translations (broad context: these are
    # explicit human approvals).
    if result["status"] in ("approved", "edited"):
        try:
            from services.translation_tm import store_tm, TMScope

            job = _translation_jobs.get(result["job_id"]) or {}
            provider_name = job.get("provider") or "google"
            final_text = result["reviewed_text"] or result["translated_text"]
            store_tm(
                [result["source_text"]],
                [final_text],
                target_language=(job or {}).get("target_lang", "en"),
                source_language=(job or {}).get("source_lang", "auto"),
                provider_name=provider_name,
                scope=TMScope.from_prompt(result["user_id"], None),
            )
        except Exception as tm_err:
            logger.warning("review_tm_sync_failed", error=str(tm_err))

    return JSONResponse(status_code=200, content={"data": result, "meta": {}})


# ---------------------------------------------------------------------------
# Rebuild
# ---------------------------------------------------------------------------
def _build_overrides(job_id: str) -> dict:
    with get_sync_session() as session:
        rows = (
            session.query(TranslationSegment)
            .filter(
                TranslationSegment.job_id == job_id,
                TranslationSegment.status.in_(["approved", "edited"]),
            )
            .all()
        )
        return {
            r.source_text: (r.reviewed_text or r.translated_text)
            for r in rows
            if (r.reviewed_text or r.translated_text)
        }


@router.post("/translations/{job_id}/rebuild")
async def rebuild_translation(
    job_id: str,
    token: Optional[str] = Query(None),
    current_user=Depends(get_authenticated_user),
):
    job = await _job_or_403(job_id, current_user, token)

    if job.get("status") != "completed":
        raise HTTPException(
            status_code=409,
            detail={
                "error": "JOB_NOT_COMPLETED",
                "message": "Rebuild is only available for completed jobs.",
            },
        )

    overrides = await asyncio.to_thread(_build_overrides, job_id)
    if not overrides:
        raise HTTPException(
            status_code=422,
            detail={
                "error": "NO_REVIEWED_SEGMENTS",
                "message": "Approve or edit at least one segment before rebuilding.",
            },
        )

    input_path = Path(job.get("input_path", ""))
    if not input_path.exists():
        raise HTTPException(
            status_code=410,
            detail={
                "error": "SOURCE_FILE_EXPIRED",
                "message": (
                    "Le fichier source a été supprimé (rétention). "
                    "Re-téléversez-le : les segments approuvés sont dans la "
                    "mémoire de traduction et seront réutilisés automatiquement."
                ),
            },
        )

    file_extension = job.get("file_extension") or input_path.suffix.lower()
    target_lang = job.get("target_lang", "en")
    source_lang = job.get("source_lang", "auto")

    output_filename = (
        f"rebuilt_{job_id[3:]}_{Path(job.get('file_name') or input_path.name).name}"
    )
    from utils.file_handler import FileHandler

    output_path = Path(
        FileHandler().generate_unique_filename(output_filename, "translated")
    )
    output_path = Path(job.get("input_path", "")).parent.parent / "outputs" / output_path.name
    output_path.parent.mkdir(parents=True, exist_ok=True)

    def _rebuild():
        from translators import ExcelTranslator, WordTranslator, PowerPointTranslator

        ext = file_extension
        if ext == ".docx":
            translator = WordTranslator(provider=None)
        elif ext == ".xlsx":
            translator = ExcelTranslator(provider=None)
        elif ext == ".pptx":
            translator = PowerPointTranslator(provider=None)
        elif ext == ".pdf":
            from translators.pdf_translator import PDFTranslator

            translator = PDFTranslator(provider=None)
        else:
            raise ValueError(f"unsupported extension {ext}")

        translator.set_segment_overrides(overrides)
        kwargs = {}
        if ext == ".pdf":
            kwargs["pdf_mode"] = job.get("pdf_mode") or "layout"
        result = translator.translate_file(
            input_path, output_path, target_lang, source_lang, **kwargs
        )
        return Path(result), translator.get_translation_stats()

    try:
        result_path, stats = await asyncio.to_thread(_rebuild)
    except Exception as e:
        logger.error("rebuild_failed", job_id=job_id, error=str(e))
        raise HTTPException(
            status_code=500,
            detail={"error": "REBUILD_FAILED", "message": str(e)[:300]},
        )

    if not result_path.exists() or result_path.stat().st_size == 0:
        raise HTTPException(
            status_code=500, detail={"error": "REBUILD_EMPTY_OUTPUT"}
        )

    # Point the job's download at the rebuilt file
    job["output_path"] = str(result_path)
    job["rebuilt_at"] = asyncio.get_event_loop().time()
    _translation_jobs[job_id] = job

    return JSONResponse(
        status_code=200,
        content={
            "data": {
                "job_id": job_id,
                "rebuilt": True,
                "segments_applied": len(overrides),
                "stats": stats,
                "download_url": f"/api/v1/download/{job_id}",
            },
            "meta": {},
        },
    )


# ---------------------------------------------------------------------------
# XLIFF 1.2 export / import
# ---------------------------------------------------------------------------
def _xliff_export(job: dict, segments: List[dict]) -> str:
    from xml.sax.saxutils import escape, quoteattr

    src_lang = job.get("source_lang") or "auto"
    tgt_lang = job.get("target_lang") or "en"
    file_name = job.get("file_name") or "document"

    lines = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        f'<xliff xmlns="{XLIFF_NS}" version="1.2">',
        f'  <file original={quoteattr(file_name)} source-language={quoteattr(src_lang)}'
        f' target-language={quoteattr(tgt_lang)} datatype="plaintext">',
        "    <body>",
    ]
    for seg in segments:
        target = (
            seg["reviewed_text"] if seg["status"] == "edited" and seg["reviewed_text"]
            else seg["translated_text"]
        )
        lines.append(f'      <trans-unit id={quoteattr(seg["id"])} resname="seg-{seg["segment_index"]}">')
        lines.append(f"        <source>{escape(seg['source_text'])}</source>")
        lines.append(f"        <target>{escape(target)}</target>")
        if seg["status"] != "pending":
            lines.append(f'        <note>status: {seg["status"]}</note>')
        lines.append("      </trans-unit>")
    lines.extend(["    </body>", "  </file>", "</xliff>"])
    return "\n".join(lines)


@router.get("/translations/{job_id}/xliff")
async def export_xliff(
    job_id: str,
    token: Optional[str] = Query(None),
    current_user=Depends(get_authenticated_user),
):
    job = await _job_or_403(job_id, current_user, token)
    segments = await asyncio.to_thread(_fetch_segments, job_id)
    if not segments:
        raise HTTPException(
            status_code=404,
            detail={"error": "NO_SEGMENTS", "message": "Aucun segment pour ce job."},
        )
    xml = _xliff_export(job, segments)
    return PlainTextResponse(
        content=xml,
        media_type="application/xliff+xml",
        headers={
            "Content-Disposition": f'attachment; filename="{job_id}.xliff"'
        },
    )


@router.post("/translations/{job_id}/xliff")
async def import_xliff(
    job_id: str,
    request: Request,
    token: Optional[str] = Query(None),
    current_user=Depends(get_authenticated_user),
):
    job = await _job_or_403(job_id, current_user, token)
    body = await request.body()

    from lxml import etree

    try:
        root = etree.fromstring(body)
    except Exception as e:
        raise HTTPException(
            status_code=422,
            detail={"error": "INVALID_XLIFF", "message": str(e)[:200]},
        )

    updates = {}
    for unit in root.iter(f"{{{XLIFF_NS}}}trans-unit"):
        unit_id = unit.get("id")
        target_el = unit.find(f"{{{XLIFF_NS}}}target")
        if not unit_id or target_el is None or target_el.text is None:
            continue
        updates[unit_id] = target_el.text

    if not updates:
        raise HTTPException(
            status_code=422,
            detail={"error": "NO_TRANS_UNITS", "message": "Aucun trans-unit avec cible trouvé."},
        )

    def _apply():
        applied = 0
        with get_sync_session() as session:
            segs = (
                session.query(TranslationSegment)
                .filter(TranslationSegment.job_id == job_id)
                .all()
            )
            by_id = {s.id: s for s in segs}
            for seg_id, new_target in updates.items():
                seg = by_id.get(seg_id)
                if not seg:
                    continue
                seg.reviewed_text = new_target
                seg.status = "edited" if new_target.strip() != seg.translated_text.strip() else "approved"
                applied += 1
            session.commit()
        return applied

    applied = await asyncio.to_thread(_apply)
    return JSONResponse(
        status_code=200,
        content={
            "data": {"job_id": job_id, "segments_updated": applied},
            "meta": {},
        },
    )
