"""Fill `fe0/public/assets/template_application_form.docx` (docxtpl) from be01 `data_blank` context."""

from __future__ import annotations

import io
import os
import re
import zipfile
from pathlib import Path
from typing import Any, Dict


def get_application_template_path() -> Path:
    """Resolve the Word file used by the applicant « application form » preview."""
    env = (os.environ.get("TEMPLATE_APPLICATION_FORM_DOCX") or "").strip()
    if env:
        p = Path(env)
        if p.is_file():
            return p
    # be0/src/be01/thisfile → repo root = parents[3] (be01, src, be0, workspace)
    # be0/src/be01/thisfile → parents[3] = monorepo root (…/be0 → …/src → …/be0 → project root)
    here = Path(__file__).resolve()
    root = here.parents[3]
    candidate = root / "fe0" / "public" / "assets" / "template_application_form.docx"
    # In Docker, project root is often not mounted; set TEMPLATE_APPLICATION_FORM_DOCX (see docker-compose).
    if candidate.is_file():
        return candidate
    raise FileNotFoundError(
        f"Không tìm thấy template_application_form.docx (đã tìm {candidate}. "
        "Đặt biến môi trường TEMPLATE_APPLICATION_FORM_DOCX tới file .docx hợp lệ.)"
    )


def fill_application_form_docx(context: Dict[str, Any]) -> bytes:
    """Run docxtpl render in memory; `context` is the be01 / `data_blank.json` object tree."""
    from docxtpl import DocxTemplate
    from jinja2.exceptions import TemplateSyntaxError
    from src.be01.docx_normalize import (
        collapse_empty_page_break_paragraphs_in_docx,
        force_times_new_roman_in_styles_docx,
        move_signature_date_to_top_row,
        normalize_bo_y_te_header_lines,
        normalize_mau_02_body_alignment_spacing,
        normalize_mau_02_letterhead_table,
        normalize_mau_02_signature_unit_prefix,
        normalize_subsequent_letterhead_tables,
        relax_justified_softbreak_paragraphs_in_docx,
        shrink_overflow_sensitive_text_half_point,
        shift_selected_header_lines_left,
        strip_mau_04_evaluation_section_in_docx,
        strip_table_row_height_rules_from_docx,
        style_national_header_line,
    )

    def _rewrite_structural_docxtpl_tags(template_bytes: bytes) -> bytes:
        """
        Convert docxtpl structural tags (`{%tr ... %}`, `{%tc ... %}`, etc.) to
        plain Jinja tags so rendering can proceed when control tags were placed
        in paragraphs instead of the expected OOXML container.
        """
        marker = re.compile(r"\{%\s*(tr|tc|p|r)\s+", flags=re.IGNORECASE)
        in_buf = io.BytesIO(template_bytes)
        out_buf = io.BytesIO()
        with zipfile.ZipFile(in_buf, "r") as zin, zipfile.ZipFile(out_buf, "w") as zout:
            for info in zin.infolist():
                data = zin.read(info.filename)
                if info.filename.endswith(".xml"):
                    text = data.decode("utf-8", errors="ignore")
                    text = marker.sub("{% ", text)
                    data = text.encode("utf-8")
                zout.writestr(info, data)
        return out_buf.getvalue()

    template_path = get_application_template_path()
    doc = DocxTemplate(str(template_path))
    try:
        doc.render(context)
    except TemplateSyntaxError as exc:
        msg = str(exc).lower()
        if not any(f"unknown tag '{tag}'" in msg for tag in ("tr", "tc", "p", "r")):
            raise
        patched_template = _rewrite_structural_docxtpl_tags(template_path.read_bytes())
        doc = DocxTemplate(io.BytesIO(patched_template))
        doc.render(context)
    buf = io.BytesIO()
    doc.docx.save(buf)
    raw = buf.getvalue()
    # Mẫu số 04 (« PHIẾU ĐÁNH GIÁ SÁNG KIẾN ») is filled by council members in the
    # dedicated council UI (InitiativeEvaluationForm.tsx under /council). The applicant
    # submission package must not embed a blank scorecard, so strip the section out of
    # the rendered DOCX before any other normalization pass touches its letterhead.
    raw = strip_mau_04_evaluation_section_in_docx(raw)
    # docx-preview (the browser-side viewer) renders an empty paragraph that only hosts
    # a <w:br w:type="page"/> as occupying its own blank page when the next sibling is a
    # table — which the rendered application form does between sections (e.g. between
    # Mẫu số 01 sig table and Mẫu số 02 letterhead table). Collapse those page-break
    # paragraphs into <w:pageBreakBefore/> on the next content so docx-preview no longer
    # allocates a blank page in between. LibreOffice/Word are unaffected since they
    # treat both encodings the same way at render time.
    raw = collapse_empty_page_break_paragraphs_in_docx(raw)
    raw = shrink_overflow_sensitive_text_half_point(raw)
    raw = style_national_header_line(raw)
    raw = shift_selected_header_lines_left(raw)
    raw = normalize_bo_y_te_header_lines(raw)
    raw = normalize_mau_02_body_alignment_spacing(raw)
    raw = normalize_mau_02_letterhead_table(raw)
    raw = normalize_mau_02_signature_unit_prefix(raw)
    raw = normalize_subsequent_letterhead_tables(raw)
    raw = move_signature_date_to_top_row(raw)
    raw = force_times_new_roman_in_styles_docx(raw)
    # Justified paragraphs that contain soft line breaks (or distribute alignment) stretch
    # short trailing lines to fill the column, producing gaps of many spaces between every
    # word. Split such paragraphs at soft breaks (keeping justification, both -> last line
    # naturally unstretched) and rewrite distribute -> both so word spacing stays compact.
    raw = relax_justified_softbreak_paragraphs_in_docx(raw)

    # Strict-fidelity default: do not mutate layout-critical OOXML after render.
    # This normalization is opt-in via env flag for specific browser fallback issues.
    if (os.environ.get("DOCX_STRIP_TABLE_ROW_HEIGHTS") or "").strip() in {"1", "true", "TRUE", "yes", "YES"}:
        raw = strip_table_row_height_rules_from_docx(raw)
    return raw
