"""Admin-managed document templates: upload a .docx, extract its {{placeholders}}, and let
applicants render a filled DOCX/PDF by template id.

- Storage: MinIO bucket ``s3_bucket_templates`` (the .docx file).
- Fields:  Jinja placeholder names extracted from the .docx, persisted as JSONB on the row.
- Render:  docxtpl fills the template with submitted values; LibreOffice converts to PDF.

Mounted under ``/api/v1`` in main.py → routes live at ``/api/v1/templates``.
"""
from __future__ import annotations

import io
import re
import uuid
import zipfile
from datetime import datetime, timezone
from typing import Any, Optional

from fastapi import APIRouter, Body, File, Form, Header, HTTPException, Response, UploadFile
from pydantic import BaseModel, Field
from sqlalchemy import select

from src.auth_jwt import decode_access_token_user_id, decode_bearer_token
from src.initiative_db.engine import get_session, is_postgres_enabled
from src.initiative_db.models import DocumentTemplate

router = APIRouter(prefix="/templates", tags=["templates"])

_DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"


# --------------------------------------------------------------------------- #
# Auth (mirrors the extracted admin routers)
# --------------------------------------------------------------------------- #
def _jwt_roles(authorization: str | None) -> list[str]:
    p = decode_bearer_token(authorization)
    if not p:
        return []
    r = p.get("roles")
    return [str(x) for x in r] if isinstance(r, list) else []


def _require_authed_uid(authorization: str | None) -> uuid.UUID:
    uid = decode_access_token_user_id(authorization)
    if uid is None:
        raise HTTPException(status_code=401, detail="Đăng nhập để thực hiện thao tác.")
    return uid


def _require_admin_uid(authorization: str | None) -> uuid.UUID:
    uid = _require_authed_uid(authorization)
    if "admin" not in _jwt_roles(authorization):
        raise HTTPException(status_code=403, detail="Chỉ tài khoản quản trị mới thực hiện được.")
    return uid


def _require_db() -> None:
    if not is_postgres_enabled():
        raise HTTPException(status_code=503, detail="Cơ sở dữ liệu chưa sẵn sàng.")


# --------------------------------------------------------------------------- #
# Storage (lazy — MinIO config may be absent in some environments)
# --------------------------------------------------------------------------- #
def _templates_storage():
    """Return (storage, bucket). Raises 503 if MinIO/S3 is not configured."""
    try:
        from src.minio.storage import S3Storage, settings as s3_settings

        bucket = getattr(s3_settings, "s3_bucket_templates", None) or "initiative-templates"
        return S3Storage(), bucket
    except Exception as exc:  # noqa: BLE001 — surface a clean 503
        raise HTTPException(status_code=503, detail=f"Lưu trữ tệp chưa sẵn sàng: {exc}") from exc


# --------------------------------------------------------------------------- #
# Placeholder extraction
# --------------------------------------------------------------------------- #
_VAR_RE = re.compile(r"\{\{\s*([a-zA-Z_][\w]*)")


def _humanize(key: str) -> str:
    words = re.sub(r"[_.]+", " ", key).split()
    return " ".join(w[:1].upper() + w[1:] for w in words) if words else key


def _regex_extract(docx_bytes: bytes) -> set[str]:
    """Fallback: strip XML tags (placeholders may split across runs) then scan for {{ var }}."""
    found: set[str] = set()
    try:
        with zipfile.ZipFile(io.BytesIO(docx_bytes)) as z:
            for name in z.namelist():
                if not name.endswith(".xml"):
                    continue
                try:
                    flat = re.sub(r"<[^>]+>", "", z.read(name).decode("utf-8", "ignore"))
                except Exception:  # noqa: BLE001
                    continue
                for m in _VAR_RE.finditer(flat):
                    found.add(m.group(1))
    except (zipfile.BadZipFile, OSError):
        pass
    return found


def _extract_fields(docx_bytes: bytes) -> list[dict[str, str]]:
    keys: set[str] = set()
    try:
        from docxtpl import DocxTemplate

        tpl = DocxTemplate(io.BytesIO(docx_bytes))
        keys = {str(v) for v in tpl.get_undeclared_template_variables()}
    except Exception:  # noqa: BLE001 — jinja control tags can trip the parser; fall back
        keys = set()
    if not keys:
        keys = _regex_extract(docx_bytes)
    roots = sorted({k.split(".")[0].split("[")[0].strip() for k in keys if k and k.strip()})
    return [{"key": k, "label": _humanize(k), "type": "text"} for k in roots]


def _safe_filename(name: str) -> str:
    base = (name or "template.docx").strip().replace("\\", "/").split("/")[-1]
    base = re.sub(r"[^A-Za-z0-9._-]+", "_", base) or "template.docx"
    return base if base.lower().endswith(".docx") else f"{base}.docx"


# --------------------------------------------------------------------------- #
# Schemas
# --------------------------------------------------------------------------- #
class TemplateField(BaseModel):
    key: str
    label: str
    type: str = "text"


class TemplateOut(BaseModel):
    id: str
    name: str
    description: Optional[str] = None
    fields: list[TemplateField]
    originalFilename: Optional[str] = None
    isActive: bool
    createdAt: Optional[datetime] = None
    updatedAt: Optional[datetime] = None


class TemplateUpdateIn(BaseModel):
    name: Optional[str] = Field(default=None, max_length=300)
    description: Optional[str] = None
    isActive: Optional[bool] = None


def _to_out(row: DocumentTemplate) -> TemplateOut:
    raw_fields = row.fields if isinstance(row.fields, list) else []
    return TemplateOut(
        id=str(row.id),
        name=row.name,
        description=row.description,
        fields=[TemplateField(**f) for f in raw_fields if isinstance(f, dict) and f.get("key")],
        originalFilename=row.original_filename,
        isActive=bool(row.is_active),
        createdAt=row.created_at,
        updatedAt=row.updated_at,
    )


# --------------------------------------------------------------------------- #
# Endpoints
# --------------------------------------------------------------------------- #
@router.post("", response_model=TemplateOut)
async def create_template(
    name: str = Form(..., max_length=300),
    description: str = Form(""),
    file: UploadFile = File(...),
    authorization: Optional[str] = Header(None),
) -> TemplateOut:
    """Admin: upload a .docx template; extract its placeholder fields; store in MinIO."""
    _require_db()
    admin_uid = _require_admin_uid(authorization)
    if not (name or "").strip():
        raise HTTPException(status_code=422, detail="Tên mẫu không được để trống.")

    data = await file.read()
    if len(data) < 100:
        raise HTTPException(status_code=422, detail="Tệp .docx rỗng hoặc quá nhỏ.")
    ctype = (file.content_type or "").strip()
    fname = file.filename or "template.docx"
    if ctype != _DOCX_MIME and not fname.lower().endswith(".docx"):
        raise HTTPException(status_code=422, detail="Chỉ chấp nhận tệp .docx (Word).")

    fields = _extract_fields(data)
    template_id = uuid.uuid4()
    safe = _safe_filename(fname)
    key = f"{template_id}/{safe}"

    storage, bucket = _templates_storage()
    try:
        result = await storage.upload(bucket, key, io.BytesIO(data), _DOCX_MIME)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    except Exception as exc:  # noqa: BLE001
        raise HTTPException(status_code=502, detail=f"Tải tệp lên thất bại: {exc}") from exc

    async with get_session() as session:
        row = DocumentTemplate(
            id=template_id,
            name=name.strip(),
            description=(description or "").strip() or None,
            storage_key=key,
            original_filename=safe,
            content_sha256=result.get("sha256"),
            fields=fields,
            is_active=True,
            created_by=admin_uid,
        )
        session.add(row)
        await session.commit()
        await session.refresh(row)
        return _to_out(row)


@router.get("", response_model=list[TemplateOut])
async def list_templates(authorization: Optional[str] = Header(None)) -> list[TemplateOut]:
    """Authed: list templates. Non-admin sees active only; admin sees all."""
    _require_db()
    _require_authed_uid(authorization)
    is_admin = "admin" in _jwt_roles(authorization)
    async with get_session() as session:
        stmt = select(DocumentTemplate).order_by(DocumentTemplate.created_at.desc())
        if not is_admin:
            stmt = stmt.where(DocumentTemplate.is_active.is_(True))
        rows = (await session.execute(stmt)).scalars().all()
        return [_to_out(r) for r in rows]


async def _get_row_or_404(session, template_id: str, *, is_admin: bool) -> DocumentTemplate:
    try:
        tid = uuid.UUID(template_id)
    except (ValueError, TypeError):
        raise HTTPException(status_code=404, detail="Không tìm thấy mẫu.")
    row = (
        await session.execute(select(DocumentTemplate).where(DocumentTemplate.id == tid))
    ).scalar_one_or_none()
    if row is None or (not is_admin and not row.is_active):
        raise HTTPException(status_code=404, detail="Không tìm thấy mẫu.")
    return row


@router.get("/{template_id}", response_model=TemplateOut)
async def get_template(template_id: str, authorization: Optional[str] = Header(None)) -> TemplateOut:
    _require_db()
    _require_authed_uid(authorization)
    is_admin = "admin" in _jwt_roles(authorization)
    async with get_session() as session:
        return _to_out(await _get_row_or_404(session, template_id, is_admin=is_admin))


@router.get("/{template_id}/file")
async def download_template_file(template_id: str, authorization: Optional[str] = Header(None)) -> Response:
    """Admin: download the raw .docx (for preview/editing in the admin UI)."""
    _require_db()
    _require_admin_uid(authorization)
    async with get_session() as session:
        row = await _get_row_or_404(session, template_id, is_admin=True)
        key, fname = row.storage_key, row.original_filename or "template.docx"
    storage, bucket = _templates_storage()
    chunks: list[bytes] = []
    try:
        async for chunk in storage.download_stream(bucket, key):
            chunks.append(chunk)
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="Tệp mẫu không tồn tại trong kho.")
    except Exception as exc:  # noqa: BLE001
        raise HTTPException(status_code=502, detail=f"Không tải được tệp mẫu: {exc}") from exc
    return Response(
        content=b"".join(chunks),
        media_type=_DOCX_MIME,
        headers={"Content-Disposition": f'attachment; filename="{fname}"'},
    )


@router.put("/{template_id}", response_model=TemplateOut)
async def update_template(
    template_id: str,
    payload: TemplateUpdateIn = Body(...),
    authorization: Optional[str] = Header(None),
) -> TemplateOut:
    """Admin: update metadata (name / description / active). File replace is a separate step."""
    _require_db()
    _require_admin_uid(authorization)
    async with get_session() as session:
        row = await _get_row_or_404(session, template_id, is_admin=True)
        if payload.name is not None:
            if not payload.name.strip():
                raise HTTPException(status_code=422, detail="Tên mẫu không được để trống.")
            row.name = payload.name.strip()
        if payload.description is not None:
            row.description = payload.description.strip() or None
        if payload.isActive is not None:
            row.is_active = payload.isActive
        row.updated_at = datetime.now(tz=timezone.utc)
        await session.commit()
        await session.refresh(row)
        return _to_out(row)


@router.delete("/{template_id}")
async def delete_template(
    template_id: str,
    hard: bool = False,
    authorization: Optional[str] = Header(None),
) -> dict[str, Any]:
    """Admin: soft-delete (is_active=false) by default; ?hard=true removes the row + MinIO object."""
    _require_db()
    _require_admin_uid(authorization)
    async with get_session() as session:
        row = await _get_row_or_404(session, template_id, is_admin=True)
        key = row.storage_key
        if hard:
            await session.delete(row)
        else:
            row.is_active = False
            row.updated_at = datetime.now(tz=timezone.utc)
        await session.commit()
    if hard:
        try:
            storage, bucket = _templates_storage()
            async with storage._client() as s3:  # noqa: SLF001 — single-object delete
                await s3.delete_object(Bucket=bucket, Key=key)
        except Exception:  # noqa: BLE001 — row already gone; object cleanup is best-effort
            pass
    return {"ok": True, "hardDeleted": hard}


class RenderIn(BaseModel):
    values: dict[str, Any] = Field(default_factory=dict)
    format: str = "pdf"  # "pdf" | "docx"


@router.post("/{template_id}/render")
async def render_template(
    template_id: str,
    payload: RenderIn = Body(...),
    authorization: Optional[str] = Header(None),
) -> Response:
    """Authed: fill the template with `values` → DOCX (docxtpl), optionally PDF (LibreOffice)."""
    _require_db()
    _require_authed_uid(authorization)
    fmt = (payload.format or "pdf").lower()
    if fmt not in ("pdf", "docx"):
        raise HTTPException(status_code=422, detail="format phải là 'pdf' hoặc 'docx'.")

    async with get_session() as session:
        row = await _get_row_or_404(session, template_id, is_admin="admin" in _jwt_roles(authorization))
        key, base = row.storage_key, (row.original_filename or "document.docx").rsplit(".", 1)[0]

    storage, bucket = _templates_storage()
    chunks: list[bytes] = []
    try:
        async for chunk in storage.download_stream(bucket, key):
            chunks.append(chunk)
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="Tệp mẫu không tồn tại trong kho.")
    except Exception as exc:  # noqa: BLE001
        raise HTTPException(status_code=502, detail=f"Không tải được tệp mẫu: {exc}") from exc
    template_bytes = b"".join(chunks)

    try:
        from docxtpl import DocxTemplate
        from jinja2.exceptions import TemplateError

        doc = DocxTemplate(io.BytesIO(template_bytes))
        try:
            doc.render(payload.values or {})
        except TemplateError as exc:
            raise HTTPException(status_code=400, detail=f"Mẫu có cú pháp không hợp lệ: {exc}") from exc
        out = io.BytesIO()
        doc.docx.save(out)
        docx_bytes = out.getvalue()
    except HTTPException:
        raise
    except Exception as exc:  # noqa: BLE001
        raise HTTPException(status_code=500, detail=f"Điền mẫu thất bại: {exc}") from exc

    if fmt == "docx":
        return Response(
            content=docx_bytes,
            media_type=_DOCX_MIME,
            headers={"Content-Disposition": f'attachment; filename="{base}.docx"'},
        )

    try:
        from src.be01.docx_to_pdf import convert_docx_bytes_to_pdf

        pdf_bytes = convert_docx_bytes_to_pdf(docx_bytes)
    except Exception as exc:  # noqa: BLE001 — LibreOffice missing / convert failure
        raise HTTPException(status_code=502, detail=f"Chuyển PDF thất bại: {exc}") from exc
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f'inline; filename="{base}.pdf"'},
    )
