"""
CA Automator — report engine.

Every module can render to Excel (openpyxl) and Word (python-docx) from the same
computed result, so "as per the requirement" means picking a format, not a
different workflow. Both writers share one house style and both stamp the
verification warning when the computation touched an unverified rate.
"""
import os
import re
from datetime import datetime

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter

from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT

# ------------------------------------------------------------------ house style
INK = "16211C"
TEAL = "0E5C55"
CRIMSON = "A8202E"
RULE = Side(style="thin", color="C9D0C9")
MONEY = '#,##0.00;(#,##0.00);"–"'
PCT = '0.00%'

H1 = Font(name="Arial", size=14, bold=True, color=INK)
H2 = Font(name="Arial", size=11, bold=True, color="FFFFFF")
LBL = Font(name="Arial", size=10, bold=True, color=INK)
TXT = Font(name="Arial", size=10, color=INK)
SMALL = Font(name="Arial", size=8, color="6B7A6B")
WARN = Font(name="Arial", size=9, bold=True, color=CRIMSON)
HEAD_FILL = PatternFill("solid", fgColor=TEAL)
BAND_FILL = PatternFill("solid", fgColor="EEF2EE")


def _stamp(ws, meta, row=1):
    ws.cell(row=row, column=1, value=meta.get("firm_name", "")).font = H1
    ws.cell(row=row + 1, column=1, value=meta.get("title", "")).font = LBL
    ws.cell(row=row + 2, column=1,
            value=f"Client: {meta.get('client','')}   |   FY {meta.get('fy','')}   |   "
                  f"Prepared: {datetime.now().strftime('%d %b %Y %H:%M')}").font = SMALL
    r = row + 3
    if meta.get("unverified"):
        ws.cell(row=r, column=1,
                value="UNVERIFIED RATES USED: " + ", ".join(meta["unverified"][:12]) +
                      ("  …" if len(meta["unverified"]) > 12 else "") +
                      "  — confirm against the Act before issuing this working paper.").font = WARN
        r += 1
    return r + 1


def _table(ws, start_row, headers, rows, widths=None, money_cols=(), pct_cols=()):
    for j, h in enumerate(headers, start=1):
        c = ws.cell(row=start_row, column=j, value=h)
        c.font = H2
        c.fill = HEAD_FILL
        c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
        c.border = Border(bottom=RULE)
    for i, row in enumerate(rows):
        for j, v in enumerate(row, start=1):
            c = ws.cell(row=start_row + 1 + i, column=j, value=v)
            c.font = TXT
            c.border = Border(bottom=RULE)
            if i % 2 == 1:
                c.fill = BAND_FILL
            if j in money_cols:
                c.number_format = MONEY
                c.alignment = Alignment(horizontal="right")
            elif j in pct_cols:
                c.number_format = PCT
                c.alignment = Alignment(horizontal="right")
    for j, w in enumerate(widths or [], start=1):
        ws.column_dimensions[get_column_letter(j)].width = w
    ws.freeze_panes = ws.cell(row=start_row + 1, column=1)
    return start_row + 1 + len(rows)


def _total_row(ws, row, label, values, first_col=1, label_span=1):
    c = ws.cell(row=row, column=first_col, value=label)
    c.font = LBL
    for col, v in values.items():
        cc = ws.cell(row=row, column=col, value=v)
        cc.font = LBL
        cc.number_format = MONEY
        cc.border = Border(top=RULE, bottom=Side(style="double", color=INK))
    return row + 1


# ================================================================== EXCEL BUILDS

def xlsx_depreciation(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Pool computation"
    r = _stamp(ws, meta)
    headers = ["Pool", "Rate", "Opening base", "Absorbed additions", "Repairs capitalised",
               "Disposals", "Depreciation base", "Depreciation", "Closing base", "Note"]
    rows = [[p["pool"], p["rate"], p["opening_base"], p["absorbed_additions"],
             p["repairs_capitalised"], p["disposals"], p["depreciation_base"],
             p["depreciation"], p["closing_base"], p["note"]] for p in res["pools"]]
    end = _table(ws, r, headers, rows, [10, 9, 16, 18, 18, 14, 18, 16, 16, 46],
                 money_cols=(3, 4, 5, 6, 7, 8, 9), pct_cols=(2,))
    t = res["totals"]
    _total_row(ws, end, "Total", {3: t["opening"], 4: t["absorbed"], 6: t["disposals"],
                                  8: t["depreciation"], 9: t["closing"]})

    ws2 = wb.create_sheet("Additions")
    r2 = _stamp(ws2, dict(meta, title="Additions and absorption tranches"))
    add_rows = []
    for p in res["pools"]:
        for a in p["additions"]:
            add_rows.append([p["pool"], a["desc"], a["bs_month"], a["tranche"].upper(),
                             a["factor"], a["amount"], a["absorbed"]])
    _table(ws2, r2, ["Pool", "Description", "BS month", "Tranche", "Factor", "Cost", "Absorbed"],
           add_rows, [10, 40, 12, 10, 10, 16, 16], money_cols=(6, 7))
    wb.save(path); return path


def xlsx_salary_tds(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "TDS register"
    r = _stamp(ws, meta)
    headers = ["Employee", "Designation", "Cash pay", "Perquisites", "Employer contrib.",
               "Gross assessable", "Total deductions", "Taxable income", "Annual tax",
               "Retirement tax (Sec 65)", "Total tax payable", "TDS to date", "Balance", "Monthly TDS"]
    rows = [[e["name"], e["designation"], e.get("cash_pay", e["basic"] + e["allowance"] + e["other"] + e["bonus"]),
             e.get("perquisites", {}).get("total", 0), e["employer_contribution"], e["gross_assessable"],
             e["deductions"]["total"], e["taxable_income"], e["annual_tax"],
             e.get("retirement", {}).get("tax", 0), e.get("total_tax_payable", e["annual_tax"]),
             e["tds_already_deducted"], e["balance_to_deduct"], e["monthly_tds"]] for e in res["employees"]]
    end = _table(ws, r, headers, rows, [26, 18] + [15] * 12, money_cols=tuple(range(3, 15)))
    _total_row(ws, end, "Total", {
        6: sum(e["gross_assessable"] for e in res["employees"]),
        8: sum(e["taxable_income"] for e in res["employees"]),
        9: sum(e["annual_tax"] for e in res["employees"]),
        11: sum(e.get("total_tax_payable", e["annual_tax"]) for e in res["employees"]),
        14: sum(e["monthly_tds"] for e in res["employees"])})

    for e in res["employees"][:40]:
        name = (e["name"] or "Employee")[:28].replace("/", "-").replace("*", "")
        ws2 = wb.create_sheet(name or "Employee")
        rr = _stamp(ws2, dict(meta, title=f"Computation — {e['name']}"))
        perq = e.get("perquisites", {})
        foreign = e.get("foreign", {})
        detail = [["Basic salary", e["basic"]], ["Allowances", e["allowance"]],
                  ["Other income", e["other"]], ["Bonus", e["bonus"]],
                  ["Overtime", e.get("overtime", 0)], ["Commission", e.get("commission", 0)],
                  ["Leave encashment", e.get("leave_encashment", 0)],
                  ["Employer fund contribution", e["employer_contribution"]]]
        if perq.get("vehicle"): detail.append(["Perquisite — vehicle facility", perq["vehicle"]])
        if perq.get("accommodation"): detail.append(["Perquisite — accommodation facility", perq["accommodation"]])
        if perq.get("employer_loan"): detail.append(["Perquisite — concessional loan", perq["employer_loan"]])
        if perq.get("other"): detail.append(["Perquisite — other", perq["other"]])
        if foreign.get("taxable"): detail.append(["Foreign allowance — taxable portion", foreign["taxable"]])
        detail += [["Gross assessable income", e["gross_assessable"]],
                  ["Less: retirement contribution", -e["deductions"]["retirement"]],
                  ["Less: life insurance", -e["deductions"]["life_insurance"]],
                  ["Less: health insurance", -e["deductions"]["health_insurance"]],
                  ["Less: remote area allowance", -e["deductions"]["remote_area"]],
                  ["Taxable income (this employer)", e["taxable_income"]]]
        if e.get("other_employer_income"):
            detail.append(["Other employer's taxable income", e["other_employer_income"]])
            detail.append(["Combined taxable income", e.get("combined_taxable_income", e["taxable_income"])])
        rr = _table(ws2, rr, ["Particulars", "Amount"], detail, [42, 18], money_cols=(2,))
        rr += 1
        slab = [[s["band"], s["rate"], s["income"], s["tax"]] for s in e["slab_lines"]]
        rr = _table(ws2, rr, ["Slab (combined income)", "Rate", "Income", "Tax"], slab, [30, 10, 18, 18],
                    money_cols=(3, 4), pct_cols=(2,))
        rr += 1
        tax_lines = [["Tax on combined income", e.get("tax_on_combined", e["annual_tax"])]]
        if e.get("tax_on_other_employer"):
            tax_lines.append(["Less: tax attributable to other employer", -e["tax_on_other_employer"]])
        if e.get("medical_credit"):
            tax_lines.append(["Less: medical tax credit (Sec 51)", -e["medical_credit"]])
        tax_lines.append(["Annual tax — this employer", e["annual_tax"]])
        retirement = e.get("retirement", {})
        if retirement.get("applicable"):
            tax_lines += [["Retirement lump sum received", retirement["lumpsum"]],
                          ["Less: exempt portion (Sec 65)", -retirement["exempt"]],
                          [f"Taxable lump sum @ {retirement['rate'] * 100:.0f}% flat", retirement["tax"]]]
        tax_lines.append(["Total tax payable", e.get("total_tax_payable", e["annual_tax"])])
        rr = _table(ws2, rr, ["Tax computation", "Amount"], tax_lines, [42, 18], money_cols=(2,))
        _total_row(ws2, rr, "Monthly TDS", {2: e["monthly_tds"]})
    wb.save(path); return path


def xlsx_biz_tax(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Tax computation"
    r = _stamp(ws, meta)

    if res.get("regime") == "presumptive":
        p = res["presumptive"]
        rows = [["Turnover", p["turnover"]], ["Area category", p["area_category"]],
                ["Bracket", p.get("bracket_label") or ""], ["Flat tax (Sec 4)", p["flat_tax"]],
                ["Less: TDS credit", -res["tds_credit"]], ["Less: advance tax paid", -res["advance_tax_paid"]],
                ["Net tax payable", res["net_payable"]], ["Refund due", res["refund_due"]]]
        _table(ws, r, ["Particulars", "Amount (NPR)"], rows, [58, 20], money_cols=(2,))
        if p.get("note"):
            ws.cell(row=r + len(rows) + 1, column=1, value=p["note"]).font = Font(italic=True, color="A8202E")
        wb.save(path); return path

    rows = [["Profit before tax as per financial statements", res["profit_before_tax"]]]
    for a in res["additions"]:
        rows.append([f"  Add: {a['desc']}", a["amount"]])
    rows.append(["  Add: book depreciation", res["book_depreciation"]])
    for d in res["deductions"]:
        rows.append([f"  Less: {d['desc']}", -d["amount"]])
    rows.append(["  Less: depreciation per Schedule 2", -res["tax_depreciation"]])
    don = res["donations"]
    rows += [["Adjusted taxable income", res["adjusted_income"]],
             [f"  Less: donations allowed (of {don['actual']:,.2f} claimed)", -don["allowed"]],
             ["  Less: brought-forward loss utilised", -res["loss_utilised"]],
             ["Taxable income", res["taxable_income"]],
             [f"Tax at {res['rate_label']}", res["tax_before_holiday"]],
             [f"  Less: tax holiday relief ({res['tax_holiday_pct']*100:.0f}%)", -res["tax_holiday_relief"]],
             ["Tax liability", res["tax_liability"]],
             ["  Less: TDS credit", -res["tds_credit"]],
             ["  Less: advance tax paid", -res["advance_tax_paid"]],
             ["Net tax payable", res["net_payable"]],
             ["Refund due", res["refund_due"]]]
    end = _table(ws, r, ["Particulars", "Amount (NPR)"], rows, [58, 20], money_cols=(2,))

    if res.get("losses"):
        end += 2
        end = _table(ws, end, ["Loss year", "Amount", "Age (years)", "Status", "Utilised"],
                     [[l["fy_label"], l["amount"], l["age_years"], "Expired" if l["expired"] else "Usable", l["utilised"]]
                      for l in res["losses"]], [16, 16, 12, 12, 16], money_cols=(2, 5))
    wb.save(path); return path


_PENALTY_CAT_LABEL = {"income_tax": "Income tax", "tds": "TDS", "vat": "VAT"}


def xlsx_penalty(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Exposure"
    r = _stamp(ws, meta)
    rows = [[_PENALTY_CAT_LABEL.get(l.get("category"), l.get("category", "")), l["head"], l["basis"], l["amount"]]
            for l in res["lines"]]
    end = _table(ws, r, ["Category", "Head of charge", "Basis of computation", "Amount"], rows,
                 [14, 44, 60, 18], money_cols=(4,))
    _total_row(ws, end, "Total exposure", {4: res["total"]})
    wb.save(path); return path


def _journal_sheet(wb, title, journals):
    ws = wb.create_sheet(title[:31])
    for j, h in enumerate(["Period", "Narration", "Account", "Debit", "Credit"], start=1):
        c = ws.cell(row=1, column=j, value=h)
        c.font = H2; c.fill = HEAD_FILL; c.border = Border(bottom=RULE)
    r = 2
    for entry in journals:
        lines = entry["debit"] + entry["credit"]
        for i, line in enumerate(lines):
            is_credit = i >= len(entry["debit"])
            if i == 0:
                ws.cell(row=r, column=1, value=entry["period"]).font = TXT
                ws.cell(row=r, column=2, value=entry["narration"]).font = TXT
            acct = ws.cell(row=r, column=3, value=("    " + line["account"]) if is_credit else line["account"])
            acct.font = TXT
            cell = ws.cell(row=r, column=5 if is_credit else 4, value=line["amount"])
            cell.font = TXT; cell.number_format = MONEY
            r += 1
    for col, w in zip("ABCDE", [10, 44, 34, 16, 16]):
        ws.column_dimensions[col].width = w
    return ws


def xlsx_emi(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Amortisation"
    r = _stamp(ws, meta)
    s = res["summary"]
    summary_lines = [("Principal", s["principal"]), ("Instalment (EMI)", s.get("emi")),
                     ("Total interest (contractual)", s["total_interest"]),
                     ("Total paid", s["total_paid"]), ("Processing fee", s["processing_fee"]),
                     ("Total cost of borrowing", s["total_cost"])]
    if s.get("net_proceeds") is not None:
        summary_lines += [("Net proceeds (carrying amount)", s["net_proceeds"]),
                          ("Effective annual rate", s["effective_annual_rate"])]
    for i, (k, v) in enumerate(summary_lines):
        ws.cell(row=r + i, column=1, value=k).font = LBL
        c = ws.cell(row=r + i, column=2, value=v); c.font = TXT
        c.number_format = "0.00%" if k == "Effective annual rate" else MONEY
    has_dates = bool(res["rows"] and res["rows"][0].get("date_ad"))
    r += len(summary_lines) + 2
    if has_dates:
        rows = [[x["period"], x["date_ad"], x["opening"], x["instalment"], x["interest"], x["principal"],
                 x["closing"], x["current_portion"], x["non_current_portion"], x["note"]] for x in res["rows"]]
        _table(ws, r, ["Period", "Date", "Opening", "Instalment", "Interest", "Principal", "Closing",
                       "Current", "Non-current", "Note"], rows, [10, 12, 16, 16, 16, 16, 16, 14, 14, 34],
               money_cols=(3, 4, 5, 6, 7, 8, 9))
    else:
        rows = [[x["period"], x["opening"], x["instalment"], x["interest"], x["principal"],
                 x["closing"], x["current_portion"], x["non_current_portion"], x["note"]] for x in res["rows"]]
        _table(ws, r, ["Period", "Opening", "Instalment", "Interest", "Principal", "Closing",
                       "Current", "Non-current", "Note"], rows, [10, 16, 16, 16, 16, 16, 14, 14, 34],
               money_cols=(2, 3, 4, 5, 6, 7, 8))

    if res.get("eir_rows"):
        ws2 = wb.create_sheet("Amortised cost (EIR)")
        r2 = _stamp(ws2, dict(meta, title="Amortised cost — effective interest rate method (NFRS 9)"))
        rows2 = [[x["period"], x["opening"], x["effective_interest"], x["cash_flow"], x["principal_reduction"],
                  x["closing"], x["current_portion"], x["non_current_portion"]] for x in res["eir_rows"]]
        _table(ws2, r2, ["Period", "Opening", "Effective interest", "Cash paid", "Principal reduction",
                         "Closing", "Current", "Non-current"], rows2, [10, 16, 18, 16, 18, 16, 14, 14],
               money_cols=(2, 3, 4, 5, 6, 7, 8))

    if res.get("modifications_applied"):
        ws3 = wb.create_sheet("Modifications")
        r3 = _stamp(ws3, dict(meta, title="Modification gain/loss (NFRS 9)"))
        rows3 = [[m["at_period"], m["carrying_amount"], m["revised_pv_at_original_rate"], m["gain_or_loss"]]
                 for m in res["modifications_applied"]]
        _table(ws3, r3, ["After period", "Carrying amount", "Revised PV at original rate", "Gain / (loss)"],
               rows3, [14, 20, 24, 18], money_cols=(2, 3, 4))

    journals_by_fy = res.get("journals_by_fy") or {}
    if journals_by_fy:
        for fy_label in sorted(journals_by_fy):
            _journal_sheet(wb, f"Journal FY {fy_label}", journals_by_fy[fy_label])
    else:
        _journal_sheet(wb, "Journal entries", res["journals"])
    wb.save(path); return path


def xlsx_lease(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Lease schedule"
    r = _stamp(ws, meta)
    s = res["summary"]

    has_dates = bool(res["rows"] and res["rows"][0].get("date_ad"))

    if res.get("regime") == "exempt":
        ws.cell(row=r, column=1, value=s["note"]).font = LBL
        r += 2
        if has_dates:
            rows = [[x["period"], x["date_ad"], x["expense"]] for x in res["rows"]]
            _table(ws, r, ["Period", "Date", "Lease expense"], rows, [10, 12, 18], money_cols=(3,))
        else:
            rows = [[x["period"], x["expense"]] for x in res["rows"]]
            _table(ws, r, ["Period", "Lease expense"], rows, [10, 18], money_cols=(2,))
        for fy_label in sorted(res.get("journals_by_fy") or {}):
            _journal_sheet(wb, f"Journal FY {fy_label}", res["journals_by_fy"][fy_label])
        if not res.get("journals_by_fy"):
            _journal_sheet(wb, "Journal entries", res["journals"])
        wb.save(path); return path

    for i, (k, v) in enumerate([("Initial lease liability", s["initial_liability"]),
                                ("Right-of-use asset", s["rou_asset"]),
                                ("Total payments", s["total_payments"]),
                                ("Total interest", s["total_interest"])] +
                               ([("Rental TDS rate", s["tds_rate"]), ("TDS borne by", s["tds_borne_by"])]
                                if s.get("tds_rate") is not None else [])):
        ws.cell(row=r + i, column=1, value=k).font = LBL
        c = ws.cell(row=r + i, column=2, value=v); c.font = TXT
        c.number_format = "0.00%" if k == "Rental TDS rate" else (MONEY if isinstance(v, (int, float)) else "General")
    r += 8
    if has_dates:
        rows = [[x["period"], x["date_ad"], x["opening_liability"], x["payment"], x["interest"], x["principal"],
                 x["closing_liability"], x["current_portion"], x["non_current_portion"],
                 x["rou_amortisation"], x["rou_carrying"]] for x in res["rows"]]
        _table(ws, r, ["Period", "Date", "Opening liability", "Payment", "Interest", "Principal",
                       "Closing liability", "Current", "Non-current", "ROU amortisation", "ROU carrying"],
               rows, [10, 12] + [16] * 9, money_cols=tuple(range(3, 12)))
    else:
        rows = [[x["period"], x["opening_liability"], x["payment"], x["interest"], x["principal"],
                 x["closing_liability"], x["current_portion"], x["non_current_portion"],
                 x["rou_amortisation"], x["rou_carrying"]] for x in res["rows"]]
        _table(ws, r, ["Period", "Opening liability", "Payment", "Interest", "Principal",
                       "Closing liability", "Current", "Non-current", "ROU amortisation", "ROU carrying"],
               rows, [10] + [16] * 9, money_cols=tuple(range(2, 11)))

    if res.get("remeasurements"):
        ws2 = wb.create_sheet("Remeasurements")
        r2 = _stamp(ws2, dict(meta, title="Lease remeasurement (NFRS 16)"))
        rows2 = [[m["at_period"], m["old_liability"], m["new_liability"], m["rou_adjustment"],
                  m["rou_carrying_after"], m["pl_gain"]] for m in res["remeasurements"]]
        _table(ws2, r2, ["After period", "Old liability", "New liability", "ROU adjustment",
                         "ROU carrying after", "P&L gain (if ROU floored)"], rows2,
               [14, 16, 16, 16, 18, 20], money_cols=(2, 3, 4, 5, 6))

    for fy_label in sorted(res.get("journals_by_fy") or {}):
        _journal_sheet(wb, f"Journal FY {fy_label}", res["journals_by_fy"][fy_label])
    if not res.get("journals_by_fy"):
        _journal_sheet(wb, "Journal entries", res["journals"])
    wb.save(path); return path


def _stmt_sheet(ws, start_row, title, rows, any_prev, cy, py, any_opening=False):
    ws.cell(row=start_row, column=1, value=title).font = LBL
    r = start_row + 1
    # Opening column comes first (oldest figure, reading left to right toward
    # the current position) — only meaningful on the balance sheet, so callers
    # pass any_opening=False for the P&L (its rows carry no "o" figure anyway).
    headers = ["Particulars", "Note"] + (["Opening"] if any_opening else []) + [cy] + ([py] if any_prev else [])
    for j, h in enumerate(headers, start=1):
        c = ws.cell(row=r, column=j, value=h)
        c.font = H2; c.fill = HEAD_FILL; c.border = Border(bottom=RULE)
    r += 1
    ncols = len(headers)
    for row in rows:
        t = row.get("t")
        if t in ("sec", "sub"):
            c = ws.cell(row=r, column=1, value=row["l"])
            c.font = LBL if t == "sec" else TXT
            if t == "sec":
                c.fill = BAND_FILL
        else:
            ws.cell(row=r, column=1, value=("    " + row["l"]) if row.get("sub") else row["l"]).font = TXT
            ws.cell(row=r, column=2, value=row.get("n") or "").font = SMALL
            col = 3
            if any_opening:
                oc = ws.cell(row=r, column=col, value=row.get("o"))
                oc.font = LBL if t == "tot" else TXT; oc.number_format = MONEY
                col += 1
            vc = ws.cell(row=r, column=col, value=row.get("v"))
            vc.font = LBL if t == "tot" else TXT; vc.number_format = MONEY
            col += 1
            if any_prev:
                pc_ = ws.cell(row=r, column=col, value=row.get("p"))
                pc_.font = LBL if t == "tot" else TXT; pc_.number_format = MONEY
            if t == "tot":
                for c in range(1, ncols + 1):
                    ws.cell(row=r, column=c).border = Border(top=RULE, bottom=Side(style="double", color=INK))
        r += 1
    widths = [46, 8] + ([18] if any_opening else []) + [18, 18]
    for j, w in enumerate(widths, start=1):
        ws.column_dimensions[get_column_letter(j)].width = w
    return r + 1


_FS_MOVEMENT_LABELS = {
    "ppe": [("opening_nbv", "Opening net book value"), ("additions_cost", "Additions"),
            ("depreciation", "Depreciation for the year"), ("disposal_proceeds", "Disposals (proceeds)"),
            ("closing_nbv", "Closing net book value")],
    "inventories": [("closing_raw_materials", "Raw materials"), ("closing_wip", "Work in progress"),
                     ("closing_finished_goods", "Finished goods"), ("closing_inventory_value", "Total closing inventory"),
                     ("cogs", "Cost of goods sold for the year")],
}


def xlsx_fin_statements(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Balance sheet"
    r = _stamp(ws, meta)
    e = res.get("entity") or {}
    cy = e.get("fy_label") or meta.get("fy") or "Current year"
    py = e.get("prev_label") or "Previous year"
    any_prev = res.get("any_prev")
    bs = res["balance_sheet"]
    any_opening = bs.get("any_opening")
    r = _stmt_sheet(ws, r, f"Balance sheet as at {cy}", bs["rows"], any_prev, cy, py, any_opening)
    tie_msg = "Balance sheet ties." if bs["tied"] else f"Does not tie by {bs['tie']:,.2f} — check the head mapping."
    ws.cell(row=r, column=1, value=tie_msg).font = (TXT if bs["tied"] else WARN)
    r += 1
    oc = res.get("opening_control") or {}
    if any_opening:
        open_msg = ("Opening balances are internally consistent (debit = credit)." if oc.get("tied")
                    else f"Opening balances do not tie by {oc.get('diff', 0):,.2f} — check the opening figures. "
                         "This checks internal consistency only, not agreement with last year's filed statements.")
        ws.cell(row=r, column=1, value=open_msg).font = (TXT if oc.get("tied") else WARN)

    ws2 = wb.create_sheet("Profit and loss")
    r2 = _stamp(ws2, dict(meta, title="Statement of profit and loss"))
    _stmt_sheet(ws2, r2, f"Statement of profit and loss for {cy}", res["profit_and_loss"]["rows"], any_prev, cy, py)

    ws3 = wb.create_sheet("Notes")
    r3 = _stamp(ws3, dict(meta, title="Notes to accounts"))
    policy = (res.get("accounting_policy") or "").strip()
    if policy:
        ws3.cell(row=r3, column=1, value="Significant accounting policies").font = LBL
        r3 += 1
        c = ws3.cell(row=r3, column=1, value=policy)
        c.font = TXT; c.alignment = Alignment(wrap_text=True, vertical="top")
        ws3.merge_cells(start_row=r3, start_column=1, end_row=r3, end_column=4)
        ws3.row_dimensions[r3].height = max(30, 14 * (policy.count("\n") + 1 + len(policy) // 90))
        r3 += 2
    for note in res["notes"]:
        ws3.cell(row=r3, column=1, value=f"Note {note['n']} — {note['label']}").font = LBL
        r3 += 1
        movement = note.get("movement")
        if movement:
            labels = _FS_MOVEMENT_LABELS.get(note["id"], [])
            move_rows = [[lbl, movement.get(k)] for k, lbl in labels if movement.get(k) is not None]
            if move_rows:
                r3 = _table(ws3, r3, ["Movement (from source module)", "Amount"], move_rows, [46, 18], money_cols=(2,)) + 1
        item_rows = [[it["name"], it["net"]] for it in note["items"]]
        r3 = _table(ws3, r3, ["Particulars", "Amount"], item_rows, [46, 18], money_cols=(2,)) + 1
    if res.get("unmapped"):
        ws3.cell(row=r3, column=1, value="Unclassified — mapping pending").font = WARN
        r3 += 1
        r3 = _table(ws3, r3, ["Ledger", "Net"], [[u["name"], u["net"]] for u in res["unmapped"]], [46, 18], money_cols=(2,)) + 1

    disclosures = res.get("disclosures") or {}
    disclosure_labels = [("related_party", "Related party transactions"), ("contingent_liabilities", "Contingent liabilities"),
                         ("capital_commitments", "Capital commitments"), ("subsequent_events", "Subsequent events")]
    active = [(lbl, disclosures.get(k)) for k, lbl in disclosure_labels if (disclosures.get(k) or "").strip()]
    if active:
        r3 += 1
        ws3.cell(row=r3, column=1, value="Other disclosures").font = LBL
        r3 += 1
        for lbl, text in active:
            ws3.cell(row=r3, column=1, value=lbl).font = LBL
            r3 += 1
            c = ws3.cell(row=r3, column=1, value=text.strip())
            c.font = TXT; c.alignment = Alignment(wrap_text=True, vertical="top")
            ws3.merge_cells(start_row=r3, start_column=1, end_row=r3, end_column=4)
            ws3.row_dimensions[r3].height = max(24, 14 * (text.count("\n") + 1 + len(text) // 90))
            r3 += 2
    wb.save(path); return path


def xlsx_grant(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Consolidated"
    r = _stamp(ws, meta)
    rows = [[c["fy_label"], c["income"], c["dep"], c["interest"], c["deferred_income_closing"], c["cash"]]
            for c in res["consolidated"]]
    _table(ws, r, ["FY", "Grant income", "Depreciation", "Interest", "Deferred income closing", "Cash movement"],
           rows, [12, 18, 18, 18, 22, 18], money_cols=(2, 3, 4, 5, 6))

    for g in res["grants"][:40]:
        name = (g.get("name") or "Grant")[:28].replace("/", "-").replace("*", "")
        ws2 = wb.create_sheet(name or "Grant")
        r2 = _stamp(ws2, dict(meta, title=f"Grant schedule — {g.get('name','')}"))
        fy_years = sorted(g.get("deferred_income_closing_by_fy", {}))
        sched_rows = [[y, R2Val(g.get("fy", {}).get("income", {}).get(str(y))),
                      g["deferred_income_closing_by_fy"][y]] for y in fy_years]
        r2 = _table(ws2, r2, ["FY", "Income recognised", "Deferred income closing"], sched_rows,
                    [12, 20, 22], money_cols=(2, 3)) + 1
        ws2.cell(row=r2, column=1, value="Journal entries").font = LBL
        r2 += 1
        jrows = []
        for j in g.get("journals", []):
            for line in j["lines"]:
                jrows.append([j["date"], j["title"], line["ac"], line["dr"] or "", line["cr"] or ""])
        _table(ws2, r2, ["Date", "Transaction", "Account", "Debit", "Credit"], jrows,
               [12, 32, 34, 14, 14], money_cols=(4, 5))
    wb.save(path); return path


def R2Val(v):
    return round(float(v or 0), 2)


def _year_headers(n, fy1=None):
    if fy1:
        try:
            base = int(str(fy1).split("-")[0])
            return [f"FY {base+t}-{str(base+t+1)[-2:]}" for t in range(n)]
        except (ValueError, IndexError):
            pass
    return [f"Year {t+1}" for t in range(n)]


def _year_sheet(ws, start_row, title, line_items, years):
    ws.cell(row=start_row, column=1, value=title).font = LBL
    headers = ["Particulars"] + years
    rows = [[label] + values for label, values in line_items]
    return _table(ws, start_row + 1, headers, rows, [30] + [14] * len(years),
                  money_cols=tuple(range(2, len(years) + 2))) + 1


def xlsx_projection(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    v = res["verdict"]
    ws.cell(row=r, column=1, value=v["t"]).font = LBL
    r += 1
    ws.cell(row=r, column=1, value=v["s"]).font = TXT; r += 2
    kpis = [("Industry", res["industry"]), ("Projection period (years)", res["N"]),
            ("Total project cost", res["T"]), ("Gearing", res["gearing"]),
            ("NPV", res["npv"]), ("Project IRR", res["irr"]), ("Equity IRR", res["eirr"]),
            ("Minimum DSCR", res["min_dscr"]), ("Average DSCR", res["avg_dscr"]),
            ("Loan Life Coverage Ratio", res.get("llcr")), ("Project Life Coverage Ratio", res.get("plcr")),
            ("Payback (years)", res["payback"]), ("Terminal value", res["tv"])]
    for label, val in kpis:
        ws.cell(row=r, column=1, value=label).font = LBL
        c = ws.cell(row=r, column=2, value=val); c.font = TXT
        r += 1
    r += 1
    err = [c for c in res["checks"] if c["s"] == "err"]
    warn = [c for c in res["checks"] if c["s"] == "warn"]
    ws.cell(row=r, column=1, value=f"Validation — {len(err)} error(s), {len(warn)} warning(s)").font = LBL
    r += 1
    check_rows = [[c["s"].upper(), c["t"], c.get("d", "")] for c in res["checks"] if c["s"] != "ok"]
    _table(ws, r, ["Status", "Point", "Detail"], check_rows, [10, 40, 60])

    years = _year_headers(res["N"], meta.get("fy1"))

    ws2 = wb.create_sheet("Profit and loss")
    r2 = _stamp(ws2, dict(meta, title="Projected profit and loss"))
    _year_sheet(ws2, r2, "Profit and loss", [
        ("Sales", res["sales"]), ("Other income", [R2Val(x - y) for x, y in zip(res["income"], res["sales"])]),
        ("Total income", res["income"]),
        ("Raw material", res["cost_lines"]["rm"]), ("Labour", res["cost_lines"]["labour"]),
        ("Power", res["cost_lines"]["power"]), ("Factory overhead", res["cost_lines"]["mfgoh"]),
        ("Repairs", res["cost_lines"]["repairs"]), ("Admin", res["cost_lines"]["admin"]),
        ("Selling", res["cost_lines"]["selling"]), ("Total operating cost", res["opex"]),
        ("EBITDA", res["ebitda"]), ("Book depreciation", res["book_dep"]), ("Amortisation", res["amort"]),
        ("EBIT", res["ebit"]), ("Finance cost", res["fin"]), ("Profit before tax", res["pbt"]),
        ("Tax", res["tax"]), ("Profit after tax", res["pat"]), ("Dividend", res["div"]),
    ], years)

    ws3 = wb.create_sheet("Balance sheet")
    r3 = _stamp(ws3, dict(meta, title="Projected balance sheet"))
    _year_sheet(ws3, r3, "Balance sheet", [
        ("Net block", res["nb"]), ("Preoperative expenses", res["preop_close"]),
        ("Net trading assets", res["gap"]), ("Cash / bank", res["cash"]),
        ("Total assets", res["ta"]), ("Net worth", res["nw"]), ("Term loan", res["tl_close"]),
        ("Working capital limit drawn", res["wcl"]), ("Overdraft", res["od"]),
        ("Trade creditors and other CL", res["cred"]), ("Total equity and liabilities", res["tel"]),
        ("Tie check (should be ~0)", res["chk"]),
    ], years)

    ws4 = wb.create_sheet("Cash flow")
    r4 = _stamp(ws4, dict(meta, title="Projected cash flow"))
    _year_sheet(ws4, r4, "Cash flow", [
        ("Operating cash flow", res["ocf"]), ("Investing cash flow", res["icf"]),
        ("Financing cash flow", res["fcf"]), ("Closing cash", res["cash"]), ("Closing overdraft", res["od"]),
    ], years)

    ws5 = wb.create_sheet("Loan schedule")
    r5 = _stamp(ws5, dict(meta, title=f"Loan schedule — {res['tenure']:g} years, {res['moratorium']:g} year moratorium"))
    _year_sheet(ws5, r5, "Term loan", [
        ("Opening balance", res["tl_open"]), ("Interest", res["tl_int"]),
        ("Principal repaid", res["tl_prin"]), ("Closing balance", res["tl_close"]),
    ], years)

    ws6 = wb.create_sheet("Net trading assets")
    r6 = _stamp(ws6, dict(meta, title="Net trading assets and bank finance"))
    r6 = _year_sheet(ws6, r6, "Net trading assets", [
        ("Current assets (operating)", res["ca_ops"]), ("Current liabilities", res["cred"]),
        ("Net trading assets (gap)", res["gap"]), ("Margin method", res["mpbf"]["margin"]),
        ("Tandon I", res["mpbf"]["tandon1"]), ("Tandon II", res["mpbf"]["tandon2"]),
        ("Turnover method", res["mpbf"]["turnover"]), ("Drawing power", res["dp"]),
        ("Limit assessed", res["limit"]), ("Amount drawn", res["wcl"]),
    ], years)
    _year_sheet(ws6, r6, "Operating cycle", [("Cash conversion cycle (days)", res["ccc"])], years)

    ws7 = wb.create_sheet("Ratios and appraisal")
    r7 = _stamp(ws7, dict(meta, title="Ratios and investment appraisal"))
    r7 = _year_sheet(ws7, r7, "Ratios", [
        ("DSCR", res["dscr"]), ("Current ratio", res["cr"]), ("Interest cover", res["icr"]),
        ("Return on capital employed", res["roce"]), ("Return on equity", res["roe"]),
        ("Debt / equity", res["de"]), ("Break-even (fraction of sales)", res["bep"]),
    ], years)
    r7 += 1
    for label, val in [("NPV", res["npv"]), ("Project IRR", res["irr"]), ("Equity IRR", res["eirr"]),
                       ("Profitability index", res["pi"]), ("Payback (years)", res["payback"]),
                       ("Discounted payback (years)", res["disc_payback"]), ("Terminal value", res["tv"])]:
        ws7.cell(row=r7, column=1, value=label).font = LBL
        ws7.cell(row=r7, column=2, value=val).font = TXT
        r7 += 1
    r7 += 1
    sens_rows = [[sc["scenario"], sc["avg_dscr"], sc["min_dscr"], sc["irr"], sc["cum_pat"], sc["npv"]]
                 for sc in res["sensitivity"]]
    _table(ws7, r7, ["Scenario", "Average DSCR", "Minimum DSCR", "Project IRR", "Cumulative PAT", "NPV"],
           sens_rows, [30, 14, 14, 12, 18, 18], money_cols=(5, 6), pct_cols=(4,))
    wb.save(path); return path


_DD_RAG_LABEL = {"R": "Red", "A": "Amber", "G": "Green", "N": "—"}


def xlsx_dd(res, meta, path):
    meta = dict(meta, title=_DD_PERSPECTIVE_TITLE.get(res.get("perspective"), meta.get("title", "Due Diligence Report")))
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    ws.cell(row=r, column=1, value=f"Overall rating: {_DD_RAG_LABEL.get(res['overall'], res['overall'])}").font = LBL
    r += 1
    ws.cell(row=r, column=1, value=f"Red {res['rag']['R']} · Amber {res['rag']['A']} · Green {res['rag']['G']} "
                                  f"· Quantified exposure {res['exposure']:,.0f}").font = TXT
    r += 2
    d = res["deal"]
    ws.cell(row=r, column=1, value="Deal impact bridge").font = LBL; r += 1
    bridge_rows = [["Enterprise value", d["ev"]], ["Less: net debt (adjusted)", -d["netdebt"]],
                   ["Working capital adjustment", d["wc_adj"]], ["Less: tax exposure", d["tax"]],
                   ["Less: contingent liabilities (weighted)", d["cont"]],
                   ["Accounting adjustments", d["acct"]], ["Indicative equity value", d["equity"]],
                   ["Suggested escrow / holdback", d["escrow"]]]
    _table(ws, r, ["Particulars", "Amount"], bridge_rows, [40, 18], money_cols=(2,))
    r += len(bridge_rows) + 3

    ws.cell(row=r, column=1, value="Materiality").font = LBL
    ws.cell(row=r, column=2, value=res["mat"]).font = TXT; r += 2

    comm = res.get("comm") or {}
    if any(comm.values()):
        ws.cell(row=r, column=1, value="Commercial due diligence").font = LBL; r += 1
        for label, key in (("Market size", "market_size"), ("Market growth", "market_growth_pct"),
                          ("Competitive position", "competitive_position"),
                          ("Key customer / supplier dependency", "key_customers_note"),
                          ("Growth drivers", "growth_drivers"), ("Commercial risks", "commercial_risks"),
                          ("Notes", "notes")):
            if comm.get(key):
                ws.cell(row=r, column=1, value=label).font = LBL
                ws.cell(row=r, column=2, value=str(comm[key])).font = TXT
                r += 1
        r += 1

    ws2 = wb.create_sheet("Financial summary")
    r2 = _stamp(ws2, dict(meta, title="Financial summary — 5-year"))
    ys = res["ys"]
    metrics = [("Revenue", "rev"), ("EBITDA", "ebitda"), ("+/- normalisation adjustments", "norm_adj"),
               ("+/- one-off / non-recurring items", "oneoff_adj"), ("Normalised (quality-of-earnings) EBITDA", "nebitda"), ("PAT", "pat"),
               ("Net working capital", "nwc"), ("Net debt", "netdebt"), ("EBITDA margin %", "em"),
               ("Current ratio", "cr"), ("Net debt / EBITDA", "lev"), ("Interest cover", "icr"),
               ("Cash conversion %", "conv"), ("DSO (days)", "dso"), ("DIO (days)", "dio"), ("DPO (days)", "dpo")]
    rows = [[label] + [res["y"][yr].get(key) for yr in ys] for label, key in metrics]
    _table(ws2, r2, ["Metric"] + ys, rows, [26] + [14] * len(ys), money_cols=tuple(range(2, len(ys) + 2)))

    ws3 = wb.create_sheet("Risk register")
    r3 = _stamp(ws3, dict(meta, title="Risk and findings register"))
    find_rows = [[f["n"], _DD_RAG_LABEL.get(f["rag"], f["rag"]), f["ws"], f["title"], f["detail"],
                 f["qty"], f["lik"], f["imp"], f["prot"], f["status"]] for f in res["find"]]
    _table(ws3, r3, ["#", "Rating", "Workstream", "Finding", "Detail", "Quantum", "Likelihood", "Impact", "Protection", "Status"],
           find_rows, [4, 8, 14, 28, 50, 14, 10, 10, 16, 10], money_cols=(6,))

    ws4 = wb.create_sheet("Data room")
    r4 = _stamp(ws4, dict(meta, title="Data room / document register"))
    dr_rows = [[r_["doc"], r_.get("cat", ""), r_.get("per", ""), r_.get("crit", ""), r_.get("status", ""),
               r_.get("recd", ""), r_.get("src", "")] for r_ in (res.get("registers", {}).get("dr") or [])]
    _table(ws4, r4, ["Document", "Set", "Period", "Critical", "Status", "Received", "Source"], dr_rows, [30, 10, 12, 8, 12, 12, 20])

    ws5 = wb.create_sheet("Tax exposure")
    r5 = _stamp(ws5, dict(meta, title="Tax exposure"))
    ws5.cell(row=r5, column=1, value="Aggregate exposure").font = LBL; r5 += 1
    tax_rows = [["Income tax", res["tax_it"]], ["TDS", res["tax_tds"]], ["VAT", res["tax_vat"]],
                ["Open notices (probability-weighted)", res["tax_not"]], ["Total", res["tax_total"]]]
    _table(ws5, r5, ["Head", "Amount"], tax_rows, [30, 18], money_cols=(2,))
    wb.save(path); return path


_DD_PERSPECTIVE_TITLE = {"buy_side": "Financial Due Diligence Report", "sell_side": "Vendor Due Diligence Report"}


def docx_dd(res, meta, path):
    meta = dict(meta, title=_DD_PERSPECTIVE_TITLE.get(res.get("perspective"), meta.get("title", "Due Diligence Report")))
    doc = _doc_base(meta)
    doc.add_paragraph(f"Overall rating: {_DD_RAG_LABEL.get(res['overall'], res['overall'])}. "
                      f"Red {res['rag']['R']} · Amber {res['rag']['A']} · Green {res['rag']['G']} findings, "
                      f"quantified exposure {res['exposure']:,.0f}.")

    comm = res.get("comm") or {}
    if any(comm.values()):
        h0 = doc.add_paragraph(); h0r = h0.add_run("Commercial due diligence"); h0r.bold = True
        for label, key in (("Market size", "market_size"), ("Market growth", "market_growth_pct"),
                          ("Competitive position", "competitive_position"),
                          ("Key customer / supplier dependency", "key_customers_note"),
                          ("Growth drivers", "growth_drivers"), ("Commercial risks", "commercial_risks"),
                          ("Notes", "notes")):
            if comm.get(key):
                pp = doc.add_paragraph(); rr = pp.add_run(label + ": "); rr.bold = True
                pp.add_run(str(comm[key]))

    d = res["deal"]
    h = doc.add_paragraph(); hr = h.add_run("Deal impact"); hr.bold = True
    _doc_table(doc, ["Particulars", "Amount"], [
        ["Enterprise value", d["ev"]], ["Less: net debt (adjusted)", -d["netdebt"]],
        ["Working capital adjustment", d["wc_adj"]], ["Less: tax exposure", d["tax"]],
        ["Less: contingent liabilities (weighted)", d["cont"]], ["Accounting adjustments", d["acct"]],
        ["Indicative equity value", d["equity"]], ["Suggested escrow / holdback", d["escrow"]],
    ], [8.0, 7.5], bold_last=False)

    ys = res["ys"]
    h1b = doc.add_paragraph(); h1br = h1b.add_run("Quality of earnings — reported to normalised EBITDA"); h1br.bold = True
    qoe_rows = [["Reported EBITDA"] + [res["y"][yr]["ebitda"] for yr in ys],
               ["+/- normalisation adjustments"] + [res["y"][yr]["norm_adj"] for yr in ys],
               ["+/- one-off / non-recurring items"] + [res["y"][yr]["oneoff_adj"] for yr in ys],
               ["= Normalised EBITDA"] + [res["y"][yr]["nebitda"] for yr in ys]]
    _doc_table(doc, ["Particulars"] + ys, qoe_rows, [6.0] + [9.5 / max(len(ys), 1)] * len(ys))

    h2 = doc.add_paragraph(); h2r = h2.add_run("Key findings"); h2r.bold = True
    top = [f for f in res["find"] if f["rag"] in ("R", "A")][:20]
    _doc_table(doc, ["#", "Rating", "Workstream", "Finding", "Quantum"],
               [[f["n"], _DD_RAG_LABEL.get(f["rag"], f["rag"]), f["ws"], f["title"], f["qty"]] for f in top], [1.2, 2.0, 3.5, 6.5, 2.5])
    for f in top:
        p = doc.add_paragraph(); r = p.add_run(f"{f['n']}. {f['title']}"); r.bold = True
        doc.add_paragraph(f["detail"])

    h3 = doc.add_paragraph(); h3r = h3.add_run("Materiality and basis"); h3r.bold = True
    doc.add_paragraph(f"Materiality applied: {res['mat']:,.0f}.")
    _sign_block(doc)
    doc.save(path); return path


def docx_capital_advisory(res, meta, path):
    doc = _doc_base(dict(meta, title="Capital & Debt Advisory"))
    entity = res.get("entity") or {}
    doc.add_paragraph(f"Capital and debt advisory for {entity.get('name') or 'the entity'}"
                      + (f" — {entity.get('purpose')}" if entity.get("purpose") else "") + ".")

    c = res["capacity"]
    h = doc.add_paragraph(); hr = h.add_run("Debt capacity analyzer"); hr.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["EBITDA / cash accruals base", c["available_for_debt_service"]], ["Target DSCR", c["target_dscr"]],
        ["DSCR-based capacity", c["dscr_based_capacity"]], ["Max Debt/EBITDA leverage", c["max_leverage_multiple"]],
        ["Leverage-based capacity", c["leverage_based_capacity"]],
        ["Recommended max new debt", c["recommended_max_new_debt"]], ["Binding constraint", c["binding_constraint"]],
    ] + ([["Headroom over proposed new debt", c["headroom"]]] if c.get("headroom") is not None else []), [8.0, 7.5])
    _doc_table(doc, ["Scenario", "DSCR-based", "Leverage-based", "Recommended"],
              [[s["scenario"], s["dscr_based_capacity"], s["leverage_based_capacity"], s["recommended_max_new_debt"]]
               for s in c["sensitivity"]], [4.5, 3.5, 3.5, 4.0])

    rs = res["restructuring"]
    if rs.get("options"):
        h2 = doc.add_paragraph(); h2r = h2.add_run("Loan restructuring analyzer"); h2r.bold = True
        rows = [[rs["current"]["label"], rs["current"]["rate_pct"], rs["current"]["tenure_years"],
                rs["current"]["annual_payment"], rs["current"]["total_interest"], rs["current"]["pv_of_payments"], 0, 0]]
        for o in rs["options"]:
            rows.append([o["label"], o["rate_pct"], o["tenure_years"], o["annual_payment"], o["total_interest"],
                        o["pv_of_payments"], o["interest_saving_vs_current"], o["pv_saving_vs_current"]])
        _doc_table(doc, ["Option", "Rate %", "Tenure", "Annual payment", "Total interest", "PV of payments", "Interest saving", "PV saving"],
                  rows, [2.5, 1.3, 1.3, 2.0, 2.0, 2.0, 1.7, 1.7])
        doc.add_paragraph(f"Lowest present-value option: {rs.get('lowest_pv_option') or '—'}.")

    fu = res["funding"]
    if fu.get("scenarios"):
        h3 = doc.add_paragraph(); h3r = h3.add_run("Debt vs equity funding analyzer"); h3r.bold = True
        _doc_table(doc, ["Scenario", "Debt %", "Equity %", "Debt amount", "Equity amount", "Annual interest", "WACC %"],
                  [[s["label"], s["debt_pct"], s["equity_pct"], s["debt_amount"], s["equity_amount"],
                   s["annual_interest"], s["wacc_pct"]] for s in fu["scenarios"]], [2.2, 1.3, 1.3, 2.4, 2.4, 2.4, 1.5])
        doc.add_paragraph(f"Lowest WACC scenario: {fu.get('lowest_wacc_scenario') or '—'}. WACC alone does not "
                          "capture financial-distress risk at high leverage — read alongside the debt capacity analysis above.")

    be = res["breakeven"]
    h4 = doc.add_paragraph(); h4r = h4.add_run("Break-even analysis"); h4r.bold = True
    if be["mode"] == "revenue":
        _doc_table(doc, ["Particulars", "Value"], [
            ["Contribution margin ratio %", be["contribution_margin_ratio_pct"]], ["Break-even revenue", be["bep_revenue"]],
            ["Current revenue", be["current_revenue"]], ["Margin of safety", be.get("margin_of_safety")],
            ["Margin of safety %", be.get("margin_of_safety_pct")],
        ], [8.0, 7.5])
    else:
        _doc_table(doc, ["Particulars", "Value"], [
            ["Contribution margin / unit", be["contribution_margin_per_unit"]],
            ["Contribution margin ratio %", be["contribution_margin_ratio_pct"]],
            ["Break-even units", be.get("bep_units")], ["Break-even revenue", be.get("bep_revenue")],
            ["Current volume", be["current_volume"]], ["Margin of safety %", be.get("margin_of_safety_pct")],
            ["Operating leverage (x)", be.get("operating_leverage")],
        ], [8.0, 7.5])

    _sign_block(doc)
    doc.save(path); return path


def xlsx_capital_advisory(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Debt capacity"
    r = _stamp(ws, dict(meta, title="Debt capacity analyzer"))
    c = res["capacity"]
    cap_rows = [["EBITDA / cash accruals base", c["available_for_debt_service"]], ["Target DSCR", c["target_dscr"]],
               ["DSCR-based capacity", c["dscr_based_capacity"]], ["Max Debt/EBITDA leverage", c["max_leverage_multiple"]],
               ["Leverage-based capacity", c["leverage_based_capacity"]],
               ["Recommended max new debt", c["recommended_max_new_debt"]], ["Binding constraint", c["binding_constraint"]]]
    if c.get("headroom") is not None:
        cap_rows.append(["Headroom over proposed new debt", c["headroom"]])
    r = _table(ws, r, ["Particulars", "Value"], cap_rows, [32, 18]) + 2
    r = _table(ws, r, ["Scenario", "DSCR-based", "Leverage-based", "Recommended"],
              [[s["scenario"], s["dscr_based_capacity"], s["leverage_based_capacity"], s["recommended_max_new_debt"]]
               for s in c["sensitivity"]], [22, 16, 16, 16], money_cols=(2, 3, 4))

    ws2 = wb.create_sheet("Loan restructuring")
    r2 = _stamp(ws2, dict(meta, title="Loan restructuring comparison"))
    rs = res["restructuring"]
    rows = [[rs["current"]["label"], rs["current"]["rate_pct"], rs["current"]["tenure_years"],
            rs["current"]["annual_payment"], rs["current"]["total_interest"], rs["current"]["pv_of_payments"], 0, 0]]
    for o in rs.get("options") or []:
        rows.append([o["label"], o["rate_pct"], o["tenure_years"], o["annual_payment"], o["total_interest"],
                    o["pv_of_payments"], o["interest_saving_vs_current"], o["pv_saving_vs_current"]])
    _table(ws2, r2, ["Option", "Rate %", "Tenure", "Annual payment", "Total interest", "PV of payments", "Interest saving", "PV saving"],
          rows, [22, 10, 10, 16, 16, 16, 14, 14], money_cols=(4, 5, 6, 7, 8))

    ws3 = wb.create_sheet("Debt vs equity")
    r3 = _stamp(ws3, dict(meta, title="Debt vs equity funding mix"))
    fu = res["funding"]
    _table(ws3, r3, ["Scenario", "Debt %", "Equity %", "Debt amount", "Equity amount", "Annual interest", "Annual tax shield", "WACC %"],
          [[s["label"], s["debt_pct"], s["equity_pct"], s["debt_amount"], s["equity_amount"], s["annual_interest"],
           s["annual_tax_shield"], s["wacc_pct"]] for s in fu.get("scenarios") or []],
          [18, 10, 10, 16, 16, 16, 16, 12], money_cols=(4, 5, 6, 7))

    ws4 = wb.create_sheet("Break-even")
    r4 = _stamp(ws4, dict(meta, title="Break-even analysis"))
    be = res["breakeven"]
    if be["mode"] == "revenue":
        be_rows = [["Contribution margin ratio %", be["contribution_margin_ratio_pct"]], ["Break-even revenue", be["bep_revenue"]],
                  ["Current revenue", be["current_revenue"]], ["Margin of safety", be.get("margin_of_safety")],
                  ["Margin of safety %", be.get("margin_of_safety_pct")]]
        r4 = _table(ws4, r4, ["Particulars", "Value"], be_rows, [32, 18]) + 2
        _table(ws4, r4, ["Scenario", "Break-even revenue"], [[s["scenario"], s["bep_revenue"]] for s in be.get("sensitivity") or []], [22, 18])
    else:
        be_rows = [["Contribution margin / unit", be["contribution_margin_per_unit"]],
                  ["Contribution margin ratio %", be["contribution_margin_ratio_pct"]],
                  ["Break-even units", be.get("bep_units")], ["Break-even revenue", be.get("bep_revenue")],
                  ["Current volume", be["current_volume"]], ["Margin of safety %", be.get("margin_of_safety_pct")],
                  ["Operating leverage (x)", be.get("operating_leverage")]]
        r4 = _table(ws4, r4, ["Particulars", "Value"], be_rows, [32, 18]) + 2
        _table(ws4, r4, ["Scenario", "Break-even units", "Break-even revenue"],
              [[s["scenario"], s["bep_units"], s["bep_revenue"]] for s in be.get("sensitivity") or []], [22, 16, 16])

    wb.save(path); return path


def xlsx_fdi(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    rt = res["route"]
    ws.cell(row=r, column=1, value=f"Route: {rt['route']} ({rt['authority']})").font = LBL
    r += 1
    ws.cell(row=r, column=1, value=f"Sector: {res['sector']['name']} — cap {rt['cap']:.0f}% — foreign holding {rt['foreign_pct']:.2f}%").font = TXT
    r += 2
    if rt["flags"]:
        ws.cell(row=r, column=1, value="Route flags").font = LBL
        r += 1
        r = _table(ws, r, ["Severity", "Message"], [[f["t"].upper(), f["m"]] for f in rt["flags"]], [12, 90]) + 1

    fin = res["finance"]
    ws.cell(row=r, column=1, value="Means of finance").font = LBL
    r += 1
    r = _table(ws, r, ["Particulars", "Amount"], [
        ["Equity (all investors)", fin["equity"]], ["Foreign loan (NPR)", fin["f_loan"]],
        ["Local loan", fin["l_loan"]], ["Total sources", fin["sources"]], ["Total project cost", fin["cost"]],
        ["Gap (cost − sources)", fin["gap"]], ["Debt : equity", f"{fin['de_pct']:.1f}% : {fin['eq_pct']:.1f}%"]],
        [30, 20], money_cols=(2,)) + 1

    m = res["model"]
    ws.cell(row=r, column=1, value="Appraisal").font = LBL
    r += 1
    _table(ws, r, ["Particulars", "Value"], [
        ["NPV", m["npv"]], ["Project IRR %", m["irr"]], ["Equity IRR %", m["eirr"]],
        ["Minimum DSCR", m["min_dscr"]], ["Average DSCR", m["avg_dscr"]], ["Payback (years)", m["payback"]]],
        [30, 20], money_cols=(2,))

    ws2 = wb.create_sheet("Financial model")
    r2 = _stamp(ws2, dict(meta, title="Projected financial model"))
    years = [f"Year {y['y']}" for y in m["Y"]]
    line_items = [("Revenue", [y["rev"] for y in m["Y"]]), ("EBITDA", [y["ebitda"] for y in m["Y"]]),
                  ("Depreciation", [y["dep"] for y in m["Y"]]), ("EBIT", [y["ebit"] for y in m["Y"]]),
                  ("Interest", [y["interest"] for y in m["Y"]]), ("PBT", [y["pbt"] for y in m["Y"]]),
                  ("Tax", [y["tax"] for y in m["Y"]]), ("PAT", [y["pat"] for y in m["Y"]]),
                  ("Dividend", [y["div"] for y in m["Y"]]), ("Net block", [y["net_block"] for y in m["Y"]]),
                  ("Cash", [y["cash"] for y in m["Y"]]), ("Overdraft", [y["od"] for y in m["Y"]]),
                  ("Assets", [y["assets"] for y in m["Y"]]), ("Liabilities", [y["liab"] for y in m["Y"]]),
                  ("Tie check", [y["diff"] for y in m["Y"]]), ("DSCR", [y["dscr"] for y in m["Y"]])]
    headers = ["Particulars"] + years
    rows = [[label] + values for label, values in line_items]
    _table(ws2, r2, headers, rows, [22] + [14] * len(years), money_cols=tuple(range(2, len(years) + 2)))

    ws3 = wb.create_sheet("Tax and repatriation")
    r3 = _stamp(ws3, dict(meta, title="Tax and repatriation"))
    tp = res["tax_pack"]
    tp_rows = [[x["y"], x["corp_tax"], x["div_f"], x["wht"], x["net_div"], x["int_f"], x["int_wht"], x["net_int"],
               x["fees"], x["fee_wht"], x["net_fee"], x["total_tax"]] for x in tp["rows"]]
    _table(ws3, r3, ["Year", "Corporate tax", "Dividend (foreign)", "Dividend WHT", "Net dividend",
                     "Interest (foreign)", "Interest WHT", "Net interest", "Fees (foreign)", "Fee WHT",
                     "Net fees", "Total tax"], tp_rows, [6] + [14] * 11, money_cols=tuple(range(2, 13)))

    ws4 = wb.create_sheet("Exit and valuation")
    r4 = _stamp(ws4, dict(meta, title="Exit and valuation"))
    ex = res.get("exit")
    if ex:
        r4 = _table(ws4, r4, ["Exit particulars", "Value"], [
            ["Exit year", ex["ey"]], ["EBITDA at exit", ex["ebitda"]], ["Enterprise value", ex["ev"]],
            ["Net debt", ex["net_debt"]], ["Equity value", ex["eqv"]], ["Foreign proceeds", ex["foreign_proceeds"]],
            ["Capital gains tax", ex["cgt"]], ["Net proceeds to foreign investor", ex["net"]],
            ["MOIC", ex["moic"]]], [30, 20], money_cols=(2,)) + 1
    val = res["valuation"]
    _table(ws4, r4, ["Valuation method", "Equity value"], [
        ["Net asset value", val["nav"]], ["DCF", val["dcf_eq"]], ["P/E multiple", val["pe_eq"]],
        ["EV/EBITDA multiple", val["ev_eq"]], ["Weighted", val["weighted"]]], [30, 20], money_cols=(2,))

    ws5 = wb.create_sheet("Validation")
    r5 = _stamp(ws5, dict(meta, title="Validation"))
    _table(ws5, r5, ["Severity", "Area", "Message"], [[c["sev"].upper(), c["area"], c["msg"]] for c in res["checks"]], [10, 16, 90])
    wb.save(path); return path


def _fn(v):
    try:
        return float(str(v if v is not None else 0).replace(",", ""))
    except (TypeError, ValueError):
        return 0.0


_BOLD_MD = re.compile(r"\*\*(.+?)\*\*")


def _md_para(doc, text, style=None, align_center=False):
    p = doc.add_paragraph(style=style)
    if align_center:
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    pos = 0
    for m in _BOLD_MD.finditer(text):
        if m.start() > pos:
            p.add_run(text[pos:m.start()])
        b = p.add_run(m.group(1))
        b.bold = True
        pos = m.end()
    if pos < len(text):
        p.add_run(text[pos:])
    return p


def _md_h1(doc, text):
    p = doc.add_paragraph()
    r = p.add_run(text)
    r.bold = True
    r.font.size = Pt(12)
    p.paragraph_format.space_before = Pt(10)
    return p


def _fdi_is_company_r(inv):
    return inv.get("kind") in ("FC", "NC")


def _fdi_is_foreign_r(inv):
    return inv.get("kind") in ("FC", "FI")


def _fdi_party_desc(inv):
    if _fdi_is_company_r(inv):
        s = f"{inv.get('name') or '____________'}, a company incorporated under the laws of {inv.get('country') or '____________'}"
        if inv.get("reg_no"):
            s += f" under registration number {inv['reg_no']}"
        if inv.get("reg_date"):
            s += f" on {inv['reg_date']}"
        s += f", having its registered office at {inv.get('office') or '____________'}"
        return s
    s = f"{inv.get('name') or '____________'}, "
    s += f"a citizen of {inv['country']}" if inv.get("country") else "____________"
    if inv.get("passport"):
        s += f" holding passport number {inv['passport']}"
    elif inv.get("citizenship"):
        s += f" holding citizenship certificate number {inv['citizenship']}"
    if inv.get("father"):
        s += f", son / daughter of {inv['father']}"
    s += f", resident of {inv.get('address') or '____________'}"
    return s


def _fdi_party_short(inv):
    return inv.get("name") or "____________"


def _fdi_kind_label(k):
    return {"FC": "Foreign — Company", "FI": "Foreign — Individual", "NC": "Nepali — Company", "NI": "Nepali — Individual"}.get(k, k)


def docx_fdi_jva(res, meta, path):
    doc = _doc_base(dict(meta, title="Joint Venture and Shareholders Agreement"))
    J = res.get("jva") or {}
    co = res.get("client") or {}
    T = res.get("tt") or {}
    parties = res.get("investors") or []
    fo = [i for i in parties if _fdi_is_foreign_r(i)]
    totals = res["totals"]
    capital = res.get("capital") or {}

    _md_para(doc, f"THIS JOINT VENTURE AND SHAREHOLDERS AGREEMENT (this “**Agreement**”) is made at "
                  f"{J.get('place') or 'Kathmandu'}, Nepal on {J.get('effective_date') or '____________'} (the “**Effective Date**”)")
    _md_h1(doc, "BETWEEN")
    for i, inv in enumerate(parties):
        _md_para(doc, f"({i+1}) {_fdi_party_desc(inv)} (hereinafter referred to as “**{_fdi_party_short(inv)}**”, "
                      f"which expression shall include its successors and permitted assigns);")
    _md_para(doc, "The above are referred to individually as a “**Party**” and collectively as the “**Parties**”.")

    _md_h1(doc, "RECITALS")
    status = co.get("status") or ""
    if "Proposed" in status:
        recital_a = f"the Parties wish to incorporate a private limited company in Nepal under the name {co.get('name') or '____________'}"
    else:
        recital_a = f"the Parties wish to participate in {co.get('name') or '____________'}, a company incorporated in Nepal"
    _md_para(doc, f"A. The Parties wish to {recital_a.split('wish to ',1)[1]} (the “**Company**”) for the purpose of carrying on the business described in Clause 3.")
    if fo:
        _md_para(doc, "B. " + " and ".join(_fdi_party_short(i) for i in fo) +
                      " propose to make foreign investment in the Company in accordance with the Foreign Investment and Technology Transfer Act, 2075 and the approval of the competent authority.")
    else:
        _md_para(doc, "B. The Parties propose to subscribe to the share capital of the Company.")
    _md_para(doc, "C. The Parties wish to record the terms on which they will subscribe for shares in the Company, fund it, govern it, and deal with their shares.")
    doc.add_paragraph("NOW THEREFORE the Parties agree as follows:")

    face_value = capital.get("face_value") or 100
    _md_h1(doc, "1. DEFINITIONS AND INTERPRETATION")
    _md_para(doc, f"1.1 In this Agreement, unless the context requires otherwise: “**Act**” means the Companies Act, 2063; "
                  f"“**Applicable Law**” means the laws of Nepal in force from time to time, including the Foreign Investment "
                  f"and Technology Transfer Act, 2075, the Industrial Enterprises Act, the Income Tax Act, 2058 and the directives of "
                  f"Nepal Rastra Bank; “**Approval**” means the foreign investment approval issued by the competent authority; "
                  f"“**Board**” means the board of directors of the Company; “**Business**” has the meaning given in Clause 3; "
                  f"“**Shares**” means the ordinary shares of the Company of a nominal value of NPR {face_value:,.0f} each.")
    _md_para(doc, "1.2 Headings are for convenience only. References to a Clause are to a clause of this Agreement. Words importing the singular include the plural and vice versa.")

    _md_h1(doc, "2. CONDITIONS PRECEDENT")
    _md_para(doc, "2.1 The obligations of the Parties to subscribe for Shares are conditional upon: (a) the foreign investment "
                  "approval of the competent authority being obtained on terms acceptable to the Parties; (b) the approval or "
                  "registration of the investment with Nepal Rastra Bank; (c) the incorporation of the Company and the adoption of "
                  "articles of association consistent with this Agreement; (d) each Party having completed the due diligence it "
                  "considers necessary; and (e) any sector licence required for the Business having been obtained or being reasonably "
                  "capable of being obtained.")
    _md_para(doc, "2.2 If the conditions in Clause 2.1 are not satisfied within twelve (12) months of the Effective Date, any "
                  "Party may terminate this Agreement by notice, and no Party shall have any claim against another save in respect "
                  "of an antecedent breach.")

    _md_h1(doc, "3. BUSINESS OF THE COMPANY")
    location = ", ".join(x for x in [co.get("addr"), co.get("municipality"), co.get("district"), co.get("province")] if x)
    business = J.get("business_desc") or co.get("objects") or (f"manufacture and sale of {co['product']}" if co.get("product") else "____________")
    capacity_txt = f", with an installed capacity of {co['capacity']} {co.get('capacity_unit','')}" if co.get("capacity") else ""
    _md_para(doc, f"3.1 The Company shall carry on the business of {business}{capacity_txt}, at {location or '____________'} (the “**Business**”).")
    _md_para(doc, "3.2 The Company shall not carry on any business outside the Business without the prior written consent of "
                  "each Party, and shall not undertake any activity which is restricted for foreign investment under Applicable Law.")

    _md_h1(doc, "4. SHARE CAPITAL AND SUBSCRIPTION")
    authorised = capital.get("authorised") or totals["total_inv"]
    _md_para(doc, f"4.1 The authorised share capital of the Company shall be NPR {authorised:,.0f} divided into "
                  f"{authorised / face_value:,.0f} Shares of NPR {face_value:,.0f} each.")
    _md_para(doc, "4.2 The Parties shall subscribe for Shares as follows:")
    inv_rows = [[_fdi_party_short(x["inv"]), x["npr"], x["shares"], f"{x['pct']:.2f}%", x["inv"].get("board_seats", 0)] for x in totals["by_inv"]]
    inv_rows.append(["Total", totals["total_inv"], totals["shares"], "100.00%", sum(_fn(i.get("board_seats")) for i in parties)])
    _doc_table(doc, ["Party", "Amount (NPR)", "Shares", "Holding %", "Board seats"], inv_rows, [4.5, 3.2, 2.5, 2.5, 2.5], bold_last=True)
    _md_para(doc, "4.3 Each Party shall remit its subscription in full within thirty (30) days of the last of the conditions in "
                  "Clause 2.1 being satisfied. Foreign subscription shall be remitted in convertible foreign currency through the "
                  "banking channel from the account of the subscribing Party, and shall be recorded with Nepal Rastra Bank.")
    _md_para(doc, "4.4 The Company shall issue share certificates and enter each Party in the register of shareholders promptly "
                  "after allotment, and shall make every filing required with the Office of the Company Registrar and the Department of Industry.")
    _md_para(doc, "4.5 No Party shall be obliged to provide further capital. If further capital is required and agreed, it shall "
                  "first be offered to the Parties in proportion to their holdings; a Party which does not take up its entitlement "
                  "shall be diluted, and the price shall be determined in accordance with Clause 11.")

    _md_h1(doc, "5. GOVERNANCE")
    _md_para(doc, f"5.1 The Board shall consist of {J.get('board_size',5):.0f} directors. Each Party shall be entitled to "
                  f"nominate the number of directors set out in Clause 4.2 for so long as it holds not less than the corresponding "
                  f"percentage of the Shares.")
    _md_para(doc, f"5.2 The quorum for a meeting of the Board shall be {J.get('quorum',3):.0f} directors, including at least one "
                  f"director nominated by each Party holding ten per cent (10%) or more of the Shares. If a quorum is not present, "
                  f"the meeting shall be adjourned to the same day in the following week, when those present shall constitute a quorum.")
    chair = _fdi_party_short(totals["by_inv"][0]["inv"]) if totals["by_inv"] else "the Party holding the largest number of Shares"
    _md_para(doc, f"5.3 The chairperson shall be nominated by {chair}. The chairperson shall not have a casting vote.")
    _md_para(doc, "5.4 The Board shall meet at least four times in each financial year, and not less than once in each quarter. "
                  "Directors may attend by electronic means.")
    if J.get("reserved"):
        _md_para(doc, "5.5 Notwithstanding anything else in this Agreement, none of the following shall be done without the "
                      "affirmative vote or prior written consent of each Party holding not less than ten per cent (10%) of the "
                      "Shares (the “**Reserved Matters**”):")
        for x in ["any alteration to the memorandum or articles of association, or to the rights attaching to any class of shares",
                  "any increase, reduction, reorganisation or buy-back of share capital, or the issue of any instrument convertible into shares",
                  "commencing any business outside the Business, or ceasing any material part of the Business",
                  "incurring borrowing, or giving any guarantee or security, in excess of the limit approved by the Board from time to time",
                  "entering into any contract or arrangement with a Party, a director, or a person related to either",
                  "the acquisition or disposal of any asset, undertaking or shareholding of a material value",
                  "approving the annual budget and business plan, and any material departure from it",
                  "declaring or paying any dividend other than in accordance with Clause 8",
                  "the appointment or removal of the auditor, the chief executive or the chief financial officer, and the terms of their engagement",
                  "any merger, amalgamation, scheme of arrangement, listing, winding up or petition for insolvency",
                  "commencing or settling any litigation or arbitration of material value",
                  "any change to the financial year, the accounting policies, or the dividend policy"]:
            doc.add_paragraph(x, style="List Bullet")
    _md_para(doc, "5.6 The Company shall keep proper books of account, shall be audited annually by a firm of chartered "
                  "accountants appointed with the consent of each Party, and shall furnish to each Party monthly management accounts "
                  "within twenty-one (21) days of each month end and audited accounts within three (3) months of each financial year end.")
    _md_para(doc, "5.7 Each Party and its authorised representatives shall have the right, on reasonable notice, to inspect the "
                  "books, records and premises of the Company.")

    _md_h1(doc, "6. MANAGEMENT")
    _md_para(doc, "6.1 The day to day management of the Company shall vest in a chief executive appointed by the Board, who "
                  "shall report to the Board and shall operate within the annual budget and the delegation of authority approved by the Board.")
    _md_para(doc, "6.2 The Company shall comply at all times with Applicable Law, including in relation to labour, social "
                  "security, environment, taxation, foreign exchange and the terms of the Approval.")

    if T.get("has"):
        _md_h1(doc, "7. TECHNOLOGY TRANSFER AND INTELLECTUAL PROPERTY")
        licensor = T.get("licensor") or (_fdi_party_short(fo[0]) if fo else "")
        excl = "an exclusive" if T.get("exclusive") else "a non-exclusive"
        _md_para(doc, f"7.1 {licensor} shall grant to the Company {excl}, non-transferable licence to use the licensed technology "
                      f"and marks comprising {str(T.get('type','')).lower()} within {T.get('territory') or 'Nepal'} for a term of "
                      f"{_fn(T.get('term')):.0f} years, under a separate technology transfer agreement to be executed and, where "
                      f"required, approved by the competent authority.")
        lump = f"a lump sum fee of NPR {_fn(T.get('lump_sum')):,.0f} and " if _fn(T.get("lump_sum")) else ""
        _md_para(doc, f"7.2 In consideration, the Company shall pay {lump}a royalty of {_fn(T.get('royalty_pct')):.0f}% of "
                      f"{str(T.get('royalty_base','net sales')).lower()}, payable within the ceilings permitted under Applicable Law "
                      f"and subject to withholding tax. No amount in excess of the approved ceiling shall be payable or remittable.")
        sub = "The Company may sub-licence the licensed technology only with prior written consent." if T.get("sublicence") else \
              "The Company shall not sub-licence the licensed technology."
        _md_para(doc, f"7.3 {sub} All improvements made by the Company shall be notified to the licensor; ownership of "
                      f"improvements shall vest as the Parties agree in the technology transfer agreement.")
        _md_para(doc, "7.4 On termination of the licence or of this Agreement, the Company shall cease all use of the licensed "
                      "marks and technology and shall not thereafter hold itself out as connected with the licensor.")

    _md_h1(doc, "8. DIVIDEND POLICY AND REPATRIATION")
    payout = (res.get("model") or {}).get("Y", [{}])
    payout_pct = meta.get("payout_pct", "")
    _md_para(doc, "8.1 Subject to Applicable Law, the requirements of any lender, and the working capital and capital "
                  "expenditure needs of the Company, the Parties intend that the Company shall distribute not less than the agreed "
                  "percentage of its distributable profit in each financial year in which it has distributable profit.")
    _md_para(doc, "8.2 Dividend shall be declared only out of distributable profit certified by the auditor, and shall exclude "
                  "any revaluation surplus or unrealised gain.")
    _md_para(doc, "8.3 The Company shall, at its own cost, take every step necessary to enable a foreign Party to repatriate "
                  "its dividend, the proceeds of any sale of Shares and, on winding up, its capital — including maintaining the "
                  "record of foreign investment with Nepal Rastra Bank, obtaining tax clearance, obtaining the recommendation of the "
                  "Department of Industry, and making the application to Nepal Rastra Bank promptly.")
    _md_para(doc, "8.4 All distributions shall be made net of tax deducted at source as required by Applicable Law.")

    _md_h1(doc, "9. TRANSFER OF SHARES")
    _md_para(doc, "9.1 No Party shall transfer, pledge or otherwise deal with any Share except in accordance with this Clause 9, "
                  "this Agreement, the articles of association and Applicable Law, and no transfer shall be effective until the "
                  "approval of the competent authority has been obtained where required.")
    if _fn(J.get("lock_in")) > 0:
        _md_para(doc, f"9.2 No Party shall transfer any Share for a period of {_fn(J.get('lock_in')):.0f} years from the "
                      f"Effective Date (the “**Lock-in Period**”), except a transfer to an affiliate which agrees in writing "
                      f"to be bound by this Agreement.")
    if J.get("rofr"):
        _md_para(doc, "9.3 **Right of first refusal.** A Party wishing to transfer its Shares (the “**Selling Party**”) "
                      "shall give notice to the other Parties stating the number of Shares, the price and the identity of the "
                      "proposed transferee. The other Parties may, within thirty (30) days, elect to purchase all (but not part) of "
                      "those Shares at that price, in proportion to their holdings. If they do not, the Selling Party may within "
                      "ninety (90) days transfer to the proposed transferee at not less than that price and on no more favourable terms.")
    if J.get("tag"):
        _md_para(doc, "9.4 **Tag-along.** If a Party proposes to transfer Shares which would result in a change of control, no "
                      "such transfer shall be made unless the proposed transferee offers to purchase from each other Party, on the "
                      "same terms, the same proportion of that Party's Shares.")
    if J.get("drag"):
        _md_para(doc, f"9.5 **Drag-along.** If Parties holding not less than {_fn(J.get('drag_pct')):.0f}% of the Shares accept a "
                      f"bona fide offer from a third party for all of the Shares, those Parties may require the remaining Parties to "
                      f"sell all their Shares to that third party on the same terms, and the remaining Parties shall do so.")
    _md_para(doc, "9.6 Any transferee shall, as a condition of registration, execute a deed of adherence agreeing to be bound by this Agreement.")

    _md_h1(doc, "10. DEADLOCK")
    _md_para(doc, "10.1 A deadlock arises if a Reserved Matter is not resolved at two consecutive meetings of the Board or of "
                  "shareholders. On a deadlock: " + (J.get("deadlock") or "the matter shall be referred to the chairpersons of the Parties for resolution within thirty (30) days."))
    _md_para(doc, "10.2 If the deadlock is not resolved, either Party may serve a notice offering to sell all of its Shares to "
                  "the other at a stated price per Share; the recipient shall within thirty (30) days either purchase at that price "
                  "or sell its own Shares to the offeror at the same price per Share.")

    _md_h1(doc, "11. VALUATION")
    _md_para(doc, "11.1 Where a price per Share is to be determined under this Agreement and is not agreed, it shall be "
                  "determined by an independent chartered accountant appointed jointly by the Parties, applying the net asset "
                  "value, discounted cash flow, earnings multiple and enterprise value to EBITDA methods and weighting them as the "
                  "valuer considers appropriate, and stating the weighting used. The valuer shall act as an expert and not as an "
                  "arbitrator, and the determination shall be final and binding save in the case of manifest error.")
    _md_para(doc, "11.2 The cost of the valuation shall be borne equally by the Parties concerned.")

    _md_h1(doc, "12. EXIT")
    ipo = ", including through an initial public offering of the Company where it is eligible and the Parties agree" if J.get("ipo_exit") else ""
    _md_para(doc, f"12.1 The Parties intend to achieve liquidity for the foreign Party by the end of year "
                  f"{_fn(J.get('exit_year')):.0f} from the Effective Date{ipo}.")
    if J.get("put_option"):
        _md_para(doc, f"12.2 **Put option.** At any time after year {_fn(J.get('exit_year')):.0f}, a foreign Party may require "
                      f"the other Parties to purchase all of its Shares at the price determined under Clause 11, subject to "
                      f"Applicable Law and to any approval required for the transfer and for the remittance of the proceeds.")
    if J.get("call_option"):
        _md_para(doc, f"12.3 **Call option.** At any time after year {_fn(J.get('exit_year')):.0f}, the local Party may require "
                      f"a foreign Party to sell all of its Shares at the price determined under Clause 11, on the same conditions.")
    _md_para(doc, "12.4 The Parties acknowledge that the transfer of Shares by a foreign Party and the remittance of the "
                  "proceeds require approval, tax clearance and the record of the investment to be in order, and each Party shall "
                  "do all things necessary to enable that.")

    if _fn(J.get("non_compete")) > 0:
        _md_h1(doc, "13. NON-COMPETE AND CONFIDENTIALITY")
        _md_para(doc, f"13.1 No Party shall, during the term of this Agreement and for {_fn(J.get('non_compete')):.0f} years "
                      f"thereafter, directly or indirectly carry on or be engaged in any business competing with the Business "
                      f"within Nepal, or solicit any customer, supplier or employee of the Company.")
    else:
        _md_h1(doc, "13. CONFIDENTIALITY")
    _md_para(doc, "13.2 Each Party shall keep confidential all information concerning the Company and the other Parties, and "
                  "shall not disclose it except to its professional advisers, as required by Applicable Law, or with consent. This "
                  "obligation survives termination for three (3) years.")

    _md_h1(doc, "14. REPRESENTATIONS, WARRANTIES AND INDEMNITY")
    _md_para(doc, "14.1 Each Party represents and warrants to each other Party that it has full power and authority to enter "
                  "into and perform this Agreement, that this Agreement constitutes binding obligations, that it is not insolvent, "
                  "that the funds it will contribute are from a lawful source and are not the proceeds of any offence, and that it "
                  "is not subject to any sanction or restriction which would prevent it from participating in the Company.")
    _md_para(doc, "14.2 Each Party shall indemnify each other Party against all loss arising from a breach of its "
                  "representations, warranties or obligations under this Agreement.")

    _md_h1(doc, "15. TERM AND TERMINATION")
    _md_para(doc, "15.1 This Agreement takes effect on the Effective Date and continues until the Company is wound up, or "
                  "until only one Party holds Shares, or until the Parties agree in writing to terminate it.")
    _md_para(doc, "15.2 A Party may terminate this Agreement as against a defaulting Party if that Party commits a material "
                  "breach which is not remedied within sixty (60) days of notice, or becomes insolvent. In that event the "
                  "non-defaulting Parties may purchase the defaulting Party's Shares at the price determined under Clause 11, less "
                  "any loss suffered.")
    _md_para(doc, "15.3 Termination is without prejudice to accrued rights, and Clauses 11, 13, 14, 16 and 17 survive termination.")

    _md_h1(doc, "16. GOVERNING LAW AND DISPUTE RESOLUTION")
    _md_para(doc, f"16.1 This Agreement is governed by and shall be construed in accordance with the laws of {J.get('law_state') or 'Nepal'}.")
    _md_para(doc, f"16.2 Any dispute arising out of or in connection with this Agreement shall first be referred to the chief "
                  f"executives of the Parties. If it is not resolved within thirty (30) days, it shall be finally settled by "
                  f"arbitration under the rules of {J.get('arb') or 'the Nepal Council of Arbitration (NEPCA), Kathmandu, in English'}. "
                  f"The award shall be final and binding. Nothing in this Clause prevents a Party from seeking interim relief from a "
                  f"court of competent jurisdiction.")

    _md_h1(doc, "17. GENERAL")
    for x in [
        "17.1 **Articles of association.** The Parties shall procure that the articles of association of the Company are consistent with this Agreement. In the event of a conflict, as between the Parties this Agreement prevails and the Parties shall amend the articles.",
        "17.2 **Notices.** Notices shall be in writing and delivered by hand, by courier or by electronic mail to the addresses set out above or as notified.",
        "17.3 **Entire agreement.** This Agreement, together with the technology transfer agreement and any document referred to in it, constitutes the entire agreement between the Parties on its subject matter and supersedes all prior arrangements.",
        "17.4 **Amendment.** No amendment is effective unless in writing and signed by each Party, and, where required, approved by the competent authority.",
        "17.5 **Severability.** If any provision is held invalid, the remainder continues in effect and the Parties shall replace the invalid provision with one of equivalent commercial effect.",
        "17.6 **Language and counterparts.** This Agreement is executed in the English language in as many counterparts as there are Parties, each of which is an original. Where a translation is required for filing, the English text prevails as between the Parties.",
        "17.7 **Costs.** Each Party bears its own costs. The Company bears the cost of incorporation, approval and registration."]:
        _md_para(doc, x)

    doc.add_paragraph()
    doc.add_paragraph("IN WITNESS WHEREOF the Parties have executed this Agreement on the date first written above.")
    doc.add_paragraph()
    for inv in parties:
        _doc_table(doc, [f"For and on behalf of\n{_fdi_party_short(inv)}", "Witness"],
                   [["\n\n_______________________________\nName:\nDesignation:", "\n\n_______________________________\nName:\nAddress:"]], [7.5, 7.5])
    doc.add_paragraph()
    doc.add_paragraph(f"Drafted by {meta.get('firm_name','')}"
                      f"{(', file reference ' + meta['file_no']) if meta.get('file_no') else ''}. "
                      f"This draft is prepared for the Parties' consideration and must be reviewed by legal counsel and against the "
                      f"current text of the applicable law before execution.")
    doc.save(path); return path


def docx_fdi_poa(res, meta, path):
    doc = _doc_base(dict(meta, title="Power of Attorney"))
    P = res.get("poa") or {}
    co = res.get("client") or {}
    investors = res.get("investors") or []
    scope = P.get("scope") or {}
    grantor_type = P.get("grantor_type", "auto")
    g = None
    if grantor_type != "auto":
        try:
            g = investors[int(grantor_type)]
        except (ValueError, IndexError, TypeError):
            g = None
    if g is None:
        g = next((i for i in investors if _fdi_is_foreign_r(i)), investors[0] if investors else {"kind": "FC", "name": ""})

    powers = []
    if scope.get("doi"):
        powers.append("To apply to the Department of Industry, the Investment Board Nepal, the Ministry of Industry, Commerce and "
                      "Supplies and any other competent authority for approval of foreign investment, for registration of the "
                      "industry, for amendment of any approval, for the transfer of shares and for any recommendation, and to sign, "
                      "submit, amend, withdraw and receive every application, form, undertaking, declaration and letter connected with it.")
    if scope.get("ocr"):
        powers.append(f"To apply to the Office of the Company Registrar for reservation of name, for incorporation of "
                      f"{co.get('name') or 'the Company'}, for registration of any change in capital, objects, directors, "
                      f"registered office, shareholding or articles, and to sign the memorandum and articles of association, the "
                      f"share subscription, the register of shareholders and every return and filing.")
    if scope.get("nrb"):
        powers.append("To apply to Nepal Rastra Bank for approval, registration and recording of the foreign investment and of any "
                      "foreign loan, for approval of the repatriation of dividend, interest, principal, sale proceeds, royalty and "
                      "technology transfer fees, and to sign every application, undertaking and certificate connected with it.")
    if scope.get("ird"):
        powers.append("To apply to the Inland Revenue Department and any tax office for a permanent account number, for value "
                      "added tax registration, for tax clearance, and to file returns, respond to notices, attend assessments and "
                      "receive certificates.")
    if scope.get("bank"):
        powers.append(f"To open, operate and close bank accounts of {co.get('name') or 'the Company'} with any bank in Nepal for "
                      f"the purpose of receiving the investment, to receive inward remittances, and to obtain certificates of "
                      f"receipt of foreign currency.")
    if scope.get("shares"):
        powers.append(f"To subscribe for and accept shares in {co.get('name') or 'the Company'} on my/our behalf, to sign the "
                      f"share application and allotment documents, to receive share certificates, and to sign the register of shareholders.")
    if scope.get("visa"):
        powers.append("To apply for and obtain business visa, residential visa and work permit recommendations for me/us and for "
                      "the personnel deputed by me/us.")
    if scope.get("land"):
        powers.append("To take on lease, acquire and register land and buildings required for the project, and to sign and "
                      "register the deeds and agreements connected with it.")
    if scope.get("litigation"):
        powers.append("To represent me/us before any court, tribunal, arbitral panel or authority in Nepal in connection with the "
                      "investment, to appoint legal counsel, and to sign pleadings and settlements.")
    powers.append("To engage advocates, chartered accountants, auditors and other professionals, to pay fees, charges and taxes, "
                  "and to receive refunds and receipts on my/our behalf.")
    powers.append("Generally to do every act, deed and thing necessary or incidental to the establishment, registration, funding "
                  "and operation of the investment in Nepal, as fully and effectually as I/we could do personally.")

    _md_para(doc, f"**KNOW ALL PERSONS BY THESE PRESENTS** that {_fdi_party_desc(g)} (the “**Grantor**”) do hereby "
                  f"nominate, constitute and appoint:")
    doc.add_paragraph()
    _md_para(doc, f"**{P.get('attorney_name') or '____________________'}**", align_center=True)
    sub = ", ".join(x for x in [P.get("attorney_desig"),
                                (f"holding citizenship / passport number {P['attorney_citizenship']}" if P.get("attorney_citizenship") else ""),
                                (f"resident of {P['attorney_addr']}" if P.get("attorney_addr") else "")] if x)
    if sub:
        _md_para(doc, sub, align_center=True)
    _md_para(doc, "(the “**Attorney**”)", align_center=True)
    doc.add_paragraph()
    status_word = "proposed to be incorporated" if "Proposed" in (co.get("status") or "") else "incorporated"
    _md_para(doc, f"as the true and lawful attorney of the Grantor, to act for and in the name of the Grantor in connection with "
                  f"the foreign investment of the Grantor in {co.get('name') or 'the Company'}, a company {status_word} in Nepal, "
                  f"and to exercise the following powers:")
    for i, pw in enumerate(powers):
        _md_para(doc, f"{i+1}. {pw}")
    _md_h1(doc, "TERMS")
    _md_para(doc, f"1. This Power of Attorney shall remain in force for {_fn(P.get('validity')):.0f} year(s) from the date of "
                  f"execution, or until revoked in writing by the Grantor, whichever is earlier. Any act done by the Attorney "
                  f"before receipt of the revocation shall remain valid and binding on the Grantor.")
    sub_del = ("The Attorney may appoint a substitute attorney to exercise all or any of the powers granted, and may revoke such appointment."
               if P.get("sub_delegate") else "The Attorney shall not delegate or sub-delegate any of the powers granted by this Power of Attorney.")
    _md_para(doc, f"2. {sub_del}")
    _md_para(doc, "3. The Grantor hereby ratifies and confirms, and agrees to ratify and confirm, whatever the Attorney "
                  "lawfully does or causes to be done by virtue of this Power of Attorney.")
    _md_para(doc, "4. The Attorney shall exercise the powers granted in accordance with the laws of Nepal and shall not do "
                  "anything which the Grantor could not lawfully do.")
    _md_para(doc, "5. This Power of Attorney is governed by the laws of Nepal.")
    doc.add_paragraph()
    _md_para(doc, f"**IN WITNESS WHEREOF** the Grantor has executed this Power of Attorney at {P.get('place') or '____________'} on "
                  f"{meta.get('fy', '____________')}.")
    doc.add_paragraph()
    grantor_sig = f"\n\n_______________________________\n{g.get('name','')}\n" + (
        "By its authorised signatory\nName:\nDesignation:" if _fdi_is_company_r(g) else (f"Passport no. {g['passport']}" if g.get("passport") else ""))
    attorney_sig = f"\n\n_______________________________\n{P.get('attorney_name','')}\n{P.get('attorney_desig','')}"
    _doc_table(doc, ["GRANTOR", "ACCEPTED BY THE ATTORNEY"], [[grantor_sig, attorney_sig]], [7.5, 7.5])
    doc.add_paragraph()
    _doc_table(doc, ["WITNESS 1", "WITNESS 2"],
              [["\n\n_______________________________\nName:\nAddress:", "\n\n_______________________________\nName:\nAddress:"]], [7.5, 7.5])
    doc.add_paragraph()
    _md_h1(doc, "Attestation")
    doc.add_paragraph("Signed before me at ____________________ on ____________________.")
    doc.add_paragraph()
    notary_line = "_______________________________\nNotary Public / Consular Officer" + (f", {P['notary_country']}" if P.get("notary_country") else "")
    doc.add_paragraph(notary_line)
    if P.get("apostille"):
        doc.add_paragraph(f"This Power of Attorney is to be notarised in {P.get('notary_country') or 'the country of execution'} "
                          f"and thereafter apostilled, or authenticated by the Ministry of Foreign Affairs of that country and "
                          f"consularised at the Embassy or Consulate of Nepal, before it is submitted to any authority in Nepal.")
    else:
        doc.add_paragraph("This Power of Attorney is to be notarised and authenticated in accordance with the requirements of the authority before which it is to be produced.")
    doc.add_paragraph()
    doc.add_paragraph(f"Drafted by {meta.get('firm_name','')}"
                      f"{(', file reference ' + meta['file_no']) if meta.get('file_no') else ''}. The scope of the powers granted "
                      f"should be checked against the requirements of each office before execution — a power that omits an office "
                      f"cannot be cured without a fresh execution abroad.")
    doc.save(path); return path


def docx_fdi_tta(res, meta, path):
    doc = _doc_base(dict(meta, title="Technology Transfer and Licence Agreement"))
    T = res.get("tt") or {}
    co = res.get("client") or {}
    jva = res.get("jva") or {}
    fo = next((i for i in (res.get("investors") or []) if _fdi_is_foreign_r(i)), {"name": ""})
    lic = T.get("licensor") or fo.get("name") or "the Licensor"

    _md_para(doc, f"THIS TECHNOLOGY TRANSFER AND LICENCE AGREEMENT is made on {jva.get('effective_date') or '____________'} at "
                  f"{jva.get('place') or 'Kathmandu'}, Nepal BETWEEN **{lic}** (the “**Licensor**”) AND "
                  f"**{co.get('name') or '____________'}**, a company incorporated in Nepal (the “**Licensee**”).")
    _md_h1(doc, "1. Grant")
    excl = "an exclusive" if T.get("exclusive") else "a non-exclusive"
    _md_para(doc, f"1.1 The Licensor grants to the Licensee {excl}, non-transferable licence to use the Licensed Technology "
                  f"and the Licensed Marks for the manufacture, marketing and sale of the Products within "
                  f"{T.get('territory') or 'Nepal'} during the Term. The arrangement comprises {str(T.get('type','')).lower()}.")
    _md_para(doc, "1.2 The Licensed Technology comprises the technical know-how, drawings, specifications, formulations, "
                  "processes, quality standards and technical assistance described in Schedule 1, and the Licensed Marks are those "
                  "listed in Schedule 2.")
    sub = ("The Licensee may sub-licence only with the prior written consent of the Licensor." if T.get("sublicence")
           else "The Licensee shall not sub-licence, assign or otherwise part with the benefit of this Agreement.")
    _md_para(doc, f"1.3 {sub}")
    _md_h1(doc, "2. Term")
    _md_para(doc, f"2.1 This Agreement takes effect on the date of approval by the competent authority in Nepal and continues "
                  f"for {_fn(T.get('term')):.0f} years, renewable by written agreement and with fresh approval where required.")
    _md_h1(doc, "3. Consideration")
    lump = f"The Licensee shall pay a lump sum fee of NPR {_fn(T.get('lump_sum')):,.0f} payable in ____ instalments as set out in Schedule 3. " if _fn(T.get("lump_sum")) else ""
    _md_para(doc, f"3.1 {lump}The Licensee shall pay a running royalty of {_fn(T.get('royalty_pct')):.0f}% of "
                  f"{str(T.get('royalty_base','net sales')).lower()} of the Products, computed for each financial year and payable "
                  f"within ninety (90) days of the end of that year.")
    _md_para(doc, "3.2 “Net sales” means the invoiced value of the Products sold, excluding value added tax, excise, "
                  "other indirect taxes, freight, insurance, returns and trade discounts actually allowed.")
    _md_para(doc, "3.3 All payments are subject to deduction of tax at source at the rate required by the Income Tax Act, "
                  "2058, and to the ceilings and approvals applicable to technology transfer fees. No amount exceeding the approved "
                  "ceiling shall be payable or remittable, and any excess paid shall be refunded by the Licensor.")
    _md_para(doc, "3.4 Royalty shall be remitted only after the approval of Nepal Rastra Bank, on production of the audited "
                  "accounts, the auditor's certificate on the computation, the tax clearance and the recommendation of the competent authority.")
    _md_h1(doc, "4. Obligations of the Licensor")
    for x in ["To transfer the Licensed Technology in a usable form, with documentation in English.",
             "To depute technical personnel for training and commissioning as agreed, at the cost provided in Schedule 3.",
             "To provide continuing technical support and to communicate improvements during the Term.",
             "To warrant that it owns or is entitled to license the Licensed Technology and the Licensed Marks, and that their use in the Territory does not infringe the rights of any third party."]:
        doc.add_paragraph(x, style="List Bullet")
    _md_h1(doc, "5. Obligations of the Licensee")
    for x in ["To use the Licensed Technology and Licensed Marks only for the Products and only within the Territory.",
             "To maintain the quality standards specified, and to permit inspection by the Licensor on reasonable notice.",
             "To keep complete records of production and sales, and to permit the Licensor to audit the royalty computation once in each financial year.",
             "To protect the confidentiality of the Licensed Technology during the Term and for five (5) years afterwards.",
             "To obtain and maintain every approval, registration and licence required in Nepal."]:
        doc.add_paragraph(x, style="List Bullet")
    _md_h1(doc, "6. Improvements and intellectual property")
    _md_para(doc, "6.1 The Licensed Technology and the Licensed Marks remain the property of the Licensor. Nothing in this Agreement transfers ownership.")
    _md_para(doc, "6.2 Improvements developed by the Licensee shall be notified to the Licensor and may be used by the "
                  "Licensor within its own group; ownership of such improvements vests in the Licensee unless otherwise agreed in writing.")
    _md_h1(doc, "7. Termination")
    _md_para(doc, "7.1 Either party may terminate on sixty (60) days notice for a material breach not remedied, or on the "
                  "insolvency of the other party. On termination the Licensee shall cease all use of the Licensed Technology and "
                  "Licensed Marks, return all documentation, and pay all royalty accrued to the date of termination.")
    _md_h1(doc, "8. Governing law and disputes")
    _md_para(doc, f"8.1 This Agreement is governed by the laws of Nepal. Any dispute shall be finally settled by arbitration "
                  f"under the rules of {jva.get('arb') or 'the Nepal Council of Arbitration (NEPCA), Kathmandu, in English'}.")
    doc.add_paragraph()
    _doc_table(doc, ["For the Licensor", "For the Licensee"],
              [[f"\n\n_______________________________\n{lic}\nName:\nDesignation:",
                f"\n\n_______________________________\n{co.get('name','')}\nName:\nDesignation:"]], [7.5, 7.5])
    doc.add_paragraph()
    _md_h1(doc, "Schedule 1 — Licensed Technology")
    doc.add_paragraph(T.get("notes") or "To be completed: a precise description of the know-how, drawings, specifications, "
                                        "processes and technical services being supplied. A vague description is neither enforceable nor approvable.")
    _md_h1(doc, "Schedule 2 — Licensed Marks")
    doc.add_paragraph("To be completed: the trade marks, registration numbers and classes licensed.")
    _md_h1(doc, "Schedule 3 — Payments and deputation costs")
    doc.add_paragraph("To be completed: instalments of the lump sum fee, the basis of charging for deputed personnel, and travel and living costs.")
    doc.add_paragraph()
    doc.add_paragraph(f"Drafted by {meta.get('firm_name','')}. The royalty rate and the fee structure must be checked against the "
                      f"ceilings applicable to technology transfer before execution; a rate above the approved ceiling is neither "
                      f"deductible nor remittable.")
    doc.save(path); return path


def _fdi_profile_blocks(doc, inv, idx, res):
    totals = res["totals"]
    me = next((x for x in totals["by_inv"] if x["inv"] is inv), {"npr": 0, "pct": 0, "shares": 0})
    co, fo = _fdi_is_company_r(inv), _fdi_is_foreign_r(inv)
    client = res.get("client") or {}
    capital = res.get("capital") or {}
    p = doc.add_paragraph(); r = p.add_run("Investor Profile"); r.bold = True; r.font.size = Pt(13)
    doc.add_paragraph(("Corporate investor" if co else "Individual investor") + " — " + ("foreign" if fo else "Nepali") +
                      f" · proposed investment in {client.get('name') or 'the Project Company'}")
    _md_h1(doc, "1. Identification")
    if co:
        _doc_table(doc, ["Particulars", "Details"], [
            ["Name of the company", inv.get("name", "")], ["Name in local script", inv.get("name_local", "")],
            ["Country of incorporation", inv.get("country", "")], ["Registration / incorporation number", inv.get("reg_no", "")],
            ["Date of incorporation", inv.get("reg_date", "")], ["Registering authority", inv.get("reg_auth", "")],
            ["Registered office", inv.get("office", "")], ["Tax / VAT registration number", inv.get("pan", "")],
            ["Website", inv.get("website", "")], ["Email", inv.get("email", "")], ["Telephone", inv.get("phone", "")]], [5.0, 10.0])
        _md_h1(doc, "2. Standing and capacity")
        _doc_table(doc, ["Particulars", "Details"], [
            ["Paid-up capital", inv.get("paid_up", "")], ["Latest annual turnover", inv.get("turnover", "")],
            ["Number of employees", inv.get("employees", "")], ["Principal business", inv.get("business", "")]], [5.0, 10.0])
        _md_h1(doc, "2.1 Ownership and ultimate beneficial ownership")
        doc.add_paragraph(inv.get("ubo") or "To be completed: the chain of ownership up to each natural person holding, directly "
                                            "or indirectly, a controlling or material interest, with percentages.")
        _md_h1(doc, "2.2 Directors and authorised representative")
        doc.add_paragraph(inv.get("reps") or "To be completed: names, nationality and designation of the directors, and of the "
                                             "person authorised to sign for the investment.")
    else:
        _doc_table(doc, ["Particulars", "Details"], [
            ["Full name", inv.get("name", "")], ["Name in local script", inv.get("name_local", "")],
            ["Nationality", inv.get("country", "")],
            ["Passport number" if fo else "Citizenship certificate number", (inv.get("passport") if fo else inv.get("citizenship")) or ""],
            ["Place and date of issue", inv.get("pass_issue", "")], ["Date of expiry", inv.get("pass_expiry", "")],
            ["Date of birth", inv.get("dob", "")], ["Father's / spouse's name", inv.get("father", "")],
            ["Permanent address", inv.get("address", "")], ["Email", inv.get("email", "")],
            ["Telephone", inv.get("phone", "")], ["Tax reference / PAN", inv.get("pan", "")]], [5.0, 10.0])
        _md_h1(doc, "2. Background")
        _doc_table(doc, ["Particulars", "Details"], [
            ["Occupation", inv.get("occupation", "")], ["Highest qualification", inv.get("qualification", "")],
            ["Declared net worth", inv.get("net_worth", "")]], [5.0, 10.0])
        _md_h1(doc, "2.1 Business experience")
        doc.add_paragraph(inv.get("experience") or "To be completed: relevant business and sector experience, positions held, and any existing investments.")

    _md_h1(doc, "3. Proposed investment")
    face_value = capital.get("face_value") or 100
    _doc_table(doc, ["Particulars", "Details"], [
        ["Project company", client.get("name", "")], ["Sector", client.get("sector", "")],
        ["Mode of investment", "Subscription to equity shares"],
        ["Amount in original currency", f"{inv.get('currency','NPR')} {_fn(inv.get('amount')):,.0f}"],
        ["Equivalent in NPR", f"{me['npr']:,.0f}"], [f"Number of shares of NPR {face_value:,.0f} each", f"{me['shares']:,.0f}"],
        ["Proposed shareholding", f"{me['pct']:.2f}%"], ["Role in the venture", inv.get("role", "")],
        ["Board seats", str(int(_fn(inv.get("board_seats"))))], ["Total project cost (NPR)", f"{_fdi_cost_total_r(res.get('cost')):,.0f}"],
        ["Total foreign investment (NPR)", f"{totals['foreign']:,.0f}"]], [5.0, 10.0])
    _md_h1(doc, "4. Source of funds")
    doc.add_paragraph(inv.get("source_of_funds") or "To be completed: the origin of the funds to be invested, the account from "
                                                     "which they will be remitted, and the supporting evidence held on file.")
    doc.add_paragraph("The funds will be remitted in convertible foreign currency through the banking channel from an account in "
                      "the name of the investor, and will be recorded with Nepal Rastra Bank following allotment of the shares.")
    _md_h1(doc, "5. Documents accompanying this profile")
    docs = (["Certificate of incorporation, duly notarised and authenticated or apostilled", "Memorandum and articles of association",
            "Board resolution approving the investment and authorising the signatory", "Company profile and brochure",
            "Audited financial statements for the last three financial years", "Bank solvency or credit standing letter",
            "Passport copy and bio-data of the authorised representative and of the directors",
            "Power of attorney in favour of the attorney in Nepal", "Declaration of source of funds and of ultimate beneficial ownership"]
           if co else
           ["Copy of passport or citizenship certificate, duly notarised and authenticated or apostilled", "Bio-data / curriculum vitae",
            "Bank statement or financial credibility certificate", "Declaration of source of funds",
            "Power of attorney in favour of the attorney in Nepal", "Photographs as required by the authority"])
    for d in docs:
        doc.add_paragraph(d, style="List Bullet")
    _md_h1(doc, "6. Declaration")
    doc.add_paragraph("I / We declare that the particulars given above are true and correct to the best of my / our knowledge and "
                      "belief, that the funds proposed to be invested are from a lawful source and are not the proceeds of any "
                      "offence, that I am / we are not subject to any sanction or restriction which would prevent this investment, "
                      "and that I / we undertake to comply with the laws of Nepal and with the conditions of any approval granted.")
    doc.add_paragraph()
    sig = f"\n\n_______________________________\n{inv.get('name','')}" + ("\nBy its authorised signatory\nName:\nDesignation:" if co else "")
    _doc_table(doc, ["", ""], [[sig, "Date: ____________________\n\nPlace: ____________________"]], [7.5, 7.5])
    if inv.get("notes"):
        doc.add_paragraph("Note: " + inv["notes"])


def _fdi_cost_total_r(cost):
    return sum(_fn(c[1]) for c in (cost or []))


def docx_fdi_profiles(res, meta, path):
    doc = _doc_base(dict(meta, title="Investor Profiles"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Prepared for submission with the foreign investment application — {client.get('name') or 'the Project Company'}")
    _md_h1(doc, "Summary of investors")
    totals = res["totals"]
    rows = [[x["inv"].get("name", ""), _fdi_kind_label(x["inv"].get("kind")), x["inv"].get("country", ""), f"{x['npr']:,.0f}", f"{x['pct']:.2f}%"]
            for x in totals["by_inv"]]
    rows.append(["Total", "", "", f"{totals['total_inv']:,.0f}", "100.00%"])
    _doc_table(doc, ["Investor", "Type", "Country", "Amount NPR", "Holding"], rows, [4.0, 3.0, 3.0, 3.0, 2.0], bold_last=True)
    for i, inv in enumerate(res.get("investors") or []):
        doc.add_page_break()
        _fdi_profile_blocks(doc, inv, i, res)
    doc.add_page_break()
    _md_h1(doc, "Prepared by")
    doc.add_paragraph(f"{meta.get('firm_name','')}{(', ' + meta['firm_address']) if meta.get('firm_address') else ''}"
                      f"{('. File reference ' + meta['file_no'] + '.') if meta.get('file_no') else ''}")
    doc.add_paragraph("These profiles are compiled from information and documents furnished by the investors. They have been "
                      "checked for internal consistency and against the documents held on file; they are not an audit of the "
                      "investors, and no assurance is expressed on the accuracy of the information supplied.")
    doc.save(path); return path


_FDI_DOI_STEP_TITLES = [
    "Pre-screening and structuring", "Investor due diligence and KYC", "Company name reservation at OCR",
    "Prepare the FDI application package", "File with DoI or IBN", "Approval and conditions",
    "Company incorporation or share transfer at OCR", "Industry registration at DoI", "PAN / VAT registration at IRD",
    "NRB approval and bank account", "Remit capital through banking channel", "Allot shares and record with OCR",
    "Record the investment with NRB", "Sector licences, visa and local registration",
    "Commence operations and start the compliance clock",
]


def docx_fdi_advisory(res, meta, path):
    doc = _doc_base(dict(meta, title="Foreign direct investment — advisory report"))
    client = res.get("client") or {}
    totals = res["totals"]
    m = res["model"]
    rt = res["route"]
    fin = res["finance"]
    tp = res["tax_pack"]
    val = res["valuation"]
    ex = res.get("exit")
    checks = res["checks"]
    params = res.get("params") or {}
    fx = res.get("fx") or {}
    cs = m["cs"]

    doc.add_paragraph(f"{client.get('name') or 'Project company'} · {client.get('sector','')}")

    _md_h1(doc, "1. Executive summary")
    status = str(client.get("status", "")).lower()
    product = f", with {client['product']} as its principal product" if client.get("product") else ""
    _md_para(doc, f"{client.get('name') or 'The project company'} is {status}, operating in {client.get('sector','')}"
                  f"{product}. The total investment is NPR {totals['total_inv']:,.0f}, of which NPR "
                  f"{totals['foreign']:,.0f} ({totals['foreign_pct']:.2f}%) is foreign. The cost of the project is "
                  f"NPR {cs['total']:,.0f}, funded {fin['eq_pct']:.2f}% by equity and {fin['de_pct']:.2f}% by debt. "
                  f"On the figures in this file the application goes to the {rt['authority']} under the {rt['route']}.")
    _doc_table(doc, ["Metric", "Value"], [
        ["Total investment", totals["total_inv"]], ["Foreign holding", f"{totals['foreign_pct']:.2f}%"],
        ["Project cost", cs["total"]], [f"NPV at {m['wacc']:.0f}%", m["npv"]],
        ["Project IRR", "—" if m["irr"] is None else f"{m['irr']:.2f}%"],
        ["Minimum DSCR", "—" if m["min_dscr"] is None else f"{m['min_dscr']:.2f}"]], [8.0, 7.5])

    _md_h1(doc, "2. Structure and route")
    _doc_table(doc, ["Particulars", "Position"], [
        ["Sector", client.get("sector", "")], ["Approving authority", rt["authority"]], ["Route", rt["route"]],
        ["Indicative foreign equity ceiling", f"{rt['cap']:.0f}%"], ["Foreign holding proposed", f"{totals['foreign_pct']:.2f}%"],
        ["Minimum per foreign investor", f"NPR {rt['min_per_investor']:,.0f}" if rt["min_per_investor"] else "not applied"],
        ["Foreign investors below the minimum", str(len(rt["below_min"]))],
        ["Restricted list items ticked", str(len(rt["neg_hits"]))],
        ["Automatic route ceiling", f"NPR {_fn(params.get('auto_cap')):,.0f}"],
        ["Investment Board threshold", f"NPR {_fn(params.get('ibn_threshold')):,.0f}"]], [7.5, 8.0])
    if rt["flags"]:
        h = doc.add_paragraph(); hr = h.add_run("Matters arising"); hr.bold = True
        for x in rt["flags"]:
            doc.add_paragraph(x["m"], style="List Bullet")

    _md_h1(doc, "3. Investors and shareholding")
    inv_rows = [[x["inv"].get("name", "—"), _fdi_kind_label(x["inv"].get("kind")), x["inv"].get("country", "—"),
                x["npr"], x["shares"], f"{x['pct']:.2f}%"] for x in totals["by_inv"]]
    inv_rows.append(["Total", "", "", totals["total_inv"], totals["shares"], "100.00%"])
    _doc_table(doc, ["Investor", "Type", "Country", "Amount NPR", "Shares", "Holding"], inv_rows, [3.5, 2.5, 2.5, 2.5, 2.0, 2.0], bold_last=True)

    _md_h1(doc, "4. Cost of project and means of finance")
    cost_rows = [[c[0], _fn(c[1]), f"{(_fn(c[1]) / cs['total'] * 100) if cs['total'] else 0:.2f}%"] for c in (res.get("cost") or [])]
    cost_rows.append(["Total", cs["total"], "100.00%"])
    _doc_table(doc, ["Head", "NPR", "% of total"], cost_rows, [8.0, 4.0, 3.5], bold_last=True)
    src = fin["sources"]
    _doc_table(doc, ["Source", "NPR", "% of total"], [
        ["Equity — foreign", totals["foreign"], f"{(totals['foreign']/src*100) if src else 0:.2f}%"],
        ["Equity — Nepali", totals["local"], f"{(totals['local']/src*100) if src else 0:.2f}%"],
        ["Foreign loan", fin["f_loan"], f"{(fin['f_loan']/src*100) if src else 0:.2f}%"],
        ["Local bank loan", fin["l_loan"], f"{(fin['l_loan']/src*100) if src else 0:.2f}%"],
        ["Total", src, "100.00%"],
        ["Cost less means of finance", fin["gap"], "must be nil" if abs(fin["gap"]) > 1 else "ties"]], [8.0, 4.0, 3.5], bold_last=False)

    _md_h1(doc, "5. Projected results")
    years = [f"Year {y['y']}" for y in m["Y"]]
    _doc_table(doc, ["Particulars"] + years, [
        ["Revenue"] + [y["rev"] for y in m["Y"]], ["EBITDA"] + [y["ebitda"] for y in m["Y"]],
        ["Profit before tax"] + [y["pbt"] for y in m["Y"]], ["Income tax"] + [y["tax"] for y in m["Y"]],
        ["Profit after tax"] + [y["pat"] for y in m["Y"]], ["Dividend declared"] + [y["div"] for y in m["Y"]],
        ["Closing cash"] + [y["cash"] for y in m["Y"]],
        ["DSCR"] + ["—" if y["dscr"] is None else f"{y['dscr']:.2f}" for y in m["Y"]]], [3.0] + [1.6] * len(years))
    tie = ("does not tie — maximum difference NPR " + f"{m['max_diff']:,.0f}, which must be investigated before the report is issued"
           if m["max_diff"] > 1 else "ties in every projected year")
    irr_txt = "not computable on these cash flows" if m["irr"] is None else f"{m['irr']:.2f}%"
    _md_para(doc, f"The balance sheet {tie}. Net present value at {m['wacc']:.0f}% is NPR {m['npv']:,.0f} and the "
                  f"project internal rate of return is {irr_txt}.")

    _md_h1(doc, "6. Tax and repatriation")
    pbt_total = sum(r["pbt"] for r in tp["rows"])
    corp_total = sum(r["corp_tax"] for r in tp["rows"])
    div_f_total = sum(r["div_f"] for r in tp["rows"])
    wht_total = sum(r["wht"] for r in tp["rows"])
    net_div_total = sum(r["net_div"] for r in tp["rows"])
    net_int_fee_total = sum(r["net_int"] + r["net_fee"] for r in tp["rows"])
    base_cur = fx.get("base", "USD")
    fx_rate = _fn(fx.get("rate")) or 1
    _doc_table(doc, ["Particulars", "Cumulative NPR", base_cur], [
        ["Profit before tax", pbt_total, pbt_total / fx_rate],
        ["Income tax in the company", corp_total, ""],
        ["Dividend to foreign investors", div_f_total, ""],
        ["Dividend withholding tax", wht_total, ""],
        ["Net dividend remittable", net_div_total, net_div_total / fx_rate],
        ["Net interest and fees remittable", net_int_fee_total, ""],
        ["Repayment of foreign loan principal", tp["total_principal"], ""],
        [f"Total repatriated over {m['years']} years", tp["total_net_out"] + tp["total_principal"],
         (tp["total_net_out"] + tp["total_principal"]) / fx_rate],
        ["Total tax borne in Nepal", tp["total_tax"], ""]], [6.5, 4.5, 4.5], bold_last=False)
    _md_para(doc, f"The withholding rate applied to dividend is {_fn(params.get('dividend_wht')):.0f}%, to interest "
                  f"{_fn(params.get('interest_wht')):.0f}% and to royalty and fees {_fn(params.get('royalty_wht')):.0f}%. "
                  f"Because the domestic rate on dividend is low, a double taxation treaty generally offers no relief "
                  f"on dividends; a treaty is relevant to interest, royalty, technical service fees and the capital "
                  f"gains article.")

    lc = res.get("loan_compliance") or {}
    if lc.get("has_loan"):
        lc_done = sum(1 for i in lc["items"] if i["done"])
        _md_para(doc, f"Foreign loan compliance: {lc_done} of {len(lc['items'])} point(s) confirmed.")
        _doc_table(doc, ["Compliance point", "Status"],
                  [[i["item"], "Confirmed" if i["done"] else "Not yet confirmed"] for i in lc["items"]], [11.0, 4.0])
        for f in lc.get("flags") or []:
            doc.add_paragraph(f["m"], style="List Bullet")

    _md_h1(doc, "7. Valuation and exit")
    _doc_table(doc, ["Method", "Equity value NPR", "Per share NPR", "Weight"], [
        ["Net asset value", val["nav"], val["per_share"]["nav"], f"{val['w']['nav']:.0f}%"],
        ["Discounted cash flow", val["dcf_eq"], val["per_share"]["dcf"], f"{val['w']['dcf']:.0f}%"],
        ["Earnings multiple", val["pe_eq"], val["per_share"]["pe"], f"{val['w']['pe']:.0f}%"],
        ["EV / EBITDA", val["ev_eq"], val["per_share"]["ev"], f"{val['w']['ev']:.0f}%"],
        ["Weighted fair value", val["weighted"], val["per_share"]["weighted"], ""]], [5.0, 4.0, 4.0, 2.5], bold_last=True)
    if ex:
        exit_mult = (ex["ev"] / ex["ebitda"]) if ex["ebitda"] else 0
        _md_para(doc, f"On an exit in year {ex['ey']} at {exit_mult:.1f} "
                      f"times EBITDA, the equity value is NPR {ex['eqv']:,.0f}, of which NPR {ex['foreign_proceeds']:,.0f} "
                      f"is attributable to the foreign investors. After capital gains tax of NPR {ex['cgt']:,.0f} at "
                      f"{_fn(params.get('cgt_non_resident')):.0f}%, the net repatriable proceeds are NPR {ex['net']:,.0f} "
                      f"— {ex['moic']:.2f} times the capital invested, before taking account of the exchange movement "
                      f"over the holding period.")

    fxa = res.get("fx_analysis") or {}
    if fxa.get("rows"):
        _md_para(doc, "Re-expressed in each foreign investor's own currency at the exit rate entered against the "
                      "current headline rate, the exchange-rate movement over the holding period contributes:")
        _doc_table(doc, ["Investor", "Currency", "Entry rate", "Exit rate", "FX move", "Invested", "Proceeds", "MOIC"],
                  [[r["investor"], r["currency"], r["entry_rate"], r["exit_rate"], f"{r['fx_move_pct']:.2f}%",
                    r["invested_fc"], r["proceeds_fc"], f"{r['moic_fc']:.2f}x"] for r in fxa["rows"]],
                  [2.5, 1.5, 1.5, 1.5, 1.5, 2.0, 2.0, 1.5])

    _md_h1(doc, "8. Procedure and status")
    doi = res.get("doi") or {}
    doi_rows = []
    for i, title in enumerate(_FDI_DOI_STEP_TITLES):
        row = doi.get(str(i)) or doi.get(i) or {}
        doi_rows.append([i + 1, title, "Completed" if row.get("done") else "Pending", row.get("date", "")])
    _doc_table(doc, ["#", "Step", "Status", "Date"], doi_rows, [1.0, 9.0, 2.5, 3.0])

    compliance = res.get("compliance") or {}
    cal = res.get("compliance_calendar") or []
    if cal:
        h8b = doc.add_paragraph(); h8br = h8b.add_run("Post-approval compliance calendar"); h8br.bold = True
        cal_rows = []
        for i, item in enumerate(cal):
            row = compliance.get(str(i)) or compliance.get(i) or {}
            cal_rows.append([item[0], item[1], "Current" if row.get("done") else "Due", row.get("date", "")])
        _doc_table(doc, ["Obligation", "Frequency", "Status", "Due / last done"], cal_rows, [7.0, 3.5, 2.0, 3.0])

    _md_h1(doc, "9. Advisory roadmap")
    roadmap = res.get("roadmap") or []
    rm_rows = [[i + 1, r.get("t", ""), r.get("s", ""), r.get("d", ""), r.get("o", "")] for i, r in enumerate(roadmap)]
    _doc_table(doc, ["#", "Work item", "Status", "Date", "Owner"], rm_rows, [1.0, 7.0, 2.5, 2.5, 2.5])

    _md_h1(doc, "10. Matters requiring attention")
    errs = [c for c in checks if c["sev"] == "err"]
    warns = [c for c in checks if c["sev"] == "warn"]
    if errs:
        h = doc.add_paragraph(); hr = h.add_run("Must be resolved"); hr.bold = True
        for e in errs:
            doc.add_paragraph(f"{e['area']} — {e['msg']}", style="List Bullet")
    if warns:
        h = doc.add_paragraph(); hr = h.add_run("For consideration"); hr.bold = True
        for w in warns:
            doc.add_paragraph(f"{w['area']} — {w['msg']}", style="List Bullet")
    if not errs and not warns:
        doc.add_paragraph("No exceptions were raised by the validation checks.")

    _md_h1(doc, "11. Recommendations")
    recs = [
        "Obtain the approval before any money moves, and make sure the remittance narrative matches the approval and names the approved investor.",
        "Complete the recording of the investment with Nepal Rastra Bank immediately after allotment. Repatriation years later depends entirely on this step.",
        "Draft the objects clause after the restricted list has been checked, and keep the memorandum, the approval and the industry registration consistent with each other.",
        "Draw the power of attorney wide enough to cover every office the file will touch, and have it notarised and apostilled or consularised before it is sent.",
        "Agree the exit mechanism and the valuation formula in the shareholders agreement now, not at the point of exit.",
    ]
    if fin["de_pct"] > _fn(params.get("de_ratio")):
        recs.append("Reduce the debt in the funding plan or increase equity — the present gearing is above the working norm and will attract questions from both the lender and the regulator.")
    if m["min_dscr"] is not None and m["min_dscr"] < 1.2:
        recs.append("Restructure the loan — a longer tenure or a longer moratorium — so that the debt service coverage ratio does not fall below 1.20 in any year.")
    recs.append("Verify every statutory threshold, rate and ceiling used in this file against the current Act, Regulation, Finance Act and central bank circular before the report is issued.")
    for i, r in enumerate(recs):
        doc.add_paragraph(f"{i+1}. {r}")

    _md_h1(doc, "12. Basis and limitations")
    verified_txt = (f"and were last verified on {params['last_verified']}" if params.get("last_verified")
                    else "and have not been marked as verified against the current law")
    _md_para(doc, f"This report has been prepared by {meta.get('firm_name','')} on the basis of information "
                  f"furnished by the client and the investors. The projections are estimates based on the "
                  f"assumptions stated and are not a guarantee of performance. The statutory thresholds, tax rates "
                  f"and ceilings used are planning defaults carried by the firm's tool {verified_txt}. Market "
                  f"demand, selling prices and cost estimates have not been independently researched. This report "
                  f"is for the use of the client and is not to be relied upon by any other person.")
    _sign_block(doc)
    doc.save(path); return path


def docx_fdi(res, meta, path):
    doc = _doc_base(meta)
    rt = res["route"]
    p = doc.add_paragraph(); r = p.add_run(f"Route: {rt['route']} — {rt['authority']}"); r.bold = True
    doc.add_paragraph(f"Sector: {res['sector']['name']}. Foreign holding {rt['foreign_pct']:.2f}% against a ceiling of {rt['cap']:.0f}%.")
    for f in rt["flags"]:
        doc.add_paragraph(f"[{f['t'].upper()}] {f['m']}", style="List Bullet")

    fin = res["finance"]
    h = doc.add_paragraph(); hr = h.add_run("Means of finance"); hr.bold = True
    _doc_table(doc, ["Particulars", "Amount"], [
        ["Equity", fin["equity"]], ["Foreign loan", fin["f_loan"]], ["Local loan", fin["l_loan"]],
        ["Total sources", fin["sources"]], ["Total project cost", fin["cost"]], ["Gap", fin["gap"]]], [8.0, 7.5])

    m = res["model"]
    h2 = doc.add_paragraph(); h2r = h2.add_run("Appraisal"); h2r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["NPV", m["npv"]], ["Project IRR %", m["irr"]], ["Equity IRR %", m["eirr"]],
        ["Minimum DSCR", m["min_dscr"]], ["Payback (years)", m["payback"]]], [8.0, 7.5])

    ex = res.get("exit")
    if ex:
        h3 = doc.add_paragraph(); h3r = h3.add_run("Exit"); h3r.bold = True
        _doc_table(doc, ["Particulars", "Value"], [
            ["Exit year", ex["ey"]], ["Equity value", ex["eqv"]], ["Foreign proceeds", ex["foreign_proceeds"]],
            ["Net proceeds", ex["net"]], ["MOIC", ex["moic"]]], [8.0, 7.5])

    errs = [c for c in res["checks"] if c["sev"] == "err"]
    if errs:
        h4 = doc.add_paragraph(); h4r = h4.add_run("Unresolved errors"); h4r.bold = True
        for c in errs:
            doc.add_paragraph(f"{c['area']}: {c['msg']}", style="List Bullet")
    _sign_block(doc)
    doc.save(path); return path


def docx_projection(res, meta, path):
    doc = _doc_base(meta)
    v = res["verdict"]
    p = doc.add_paragraph(); r = p.add_run(v["t"]); r.bold = True; r.font.size = Pt(12)
    doc.add_paragraph(v["s"])
    doc.add_paragraph(f"Prepared for {res['industry']} over a {res['N']}-year projection period.")

    _doc_table(doc, ["Particulars", "Amount"], [
        ["Total project cost", res["T"]], ["Gearing", f"{res['gearing']*100:.1f}%"],
        ["Net present value", res["npv"]], ["Project IRR", f"{res['irr']*100:.1f}%" if res["irr"] is not None else "—"],
        ["Equity IRR", f"{res['eirr']*100:.1f}%" if res["eirr"] is not None else "—"],
        ["Minimum DSCR", res["min_dscr"]], ["Average DSCR", res["avg_dscr"]],
        ["Loan Life Coverage Ratio", res.get("llcr") if res.get("llcr") is not None else "—"],
        ["Project Life Coverage Ratio", res.get("plcr") if res.get("plcr") is not None else "—"],
        ["Payback (years)", res["payback"]], ["Terminal value", res["tv"]],
    ], [8.0, 7.5])

    years = _year_headers(res["N"], meta.get("fy1"))
    pl_rows = [[l] + v for l, v in [("Sales", res["sales"]), ("EBITDA", res["ebitda"]),
               ("Profit before tax", res["pbt"]), ("Tax", res["tax"]), ("Profit after tax", res["pat"])]]
    h = doc.add_paragraph(); hr = h.add_run("Profit and loss summary"); hr.bold = True
    _doc_table(doc, ["Particulars"] + years, pl_rows, [4.0] + [2.0] * len(years))

    dscr_rows = [["DSCR"] + [x if x is not None else "—" for x in res["dscr"]]]
    h2 = doc.add_paragraph(); h2r = h2.add_run("Debt service coverage by year"); h2r.bold = True
    _doc_table(doc, ["Particulars"] + years, dscr_rows, [4.0] + [2.0] * len(years))

    if any(c["s"] != "ok" for c in res["checks"]):
        h3 = doc.add_paragraph(); h3r = h3.add_run("Points arising"); h3r.bold = True
        for c in res["checks"]:
            if c["s"] == "ok":
                continue
            marker = "Error — " if c["s"] == "err" else "Note — "
            doc.add_paragraph(marker + c["t"] + (f": {c['d']}" if c.get("d") else ""), style="List Bullet")
    _sign_block(doc)
    doc.save(path); return path


# =================================================================== WORD BUILDS

def _doc_base(meta):
    doc = Document()
    st = doc.styles["Normal"]
    st.font.name = "Arial"; st.font.size = Pt(10)
    for s in doc.sections:
        s.top_margin = Cm(2.2); s.bottom_margin = Cm(2.0)
        s.left_margin = Cm(2.2); s.right_margin = Cm(2.0)

    h = doc.add_paragraph(); h.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = h.add_run(meta.get("firm_name", "")); run.bold = True; run.font.size = Pt(15)
    run.font.color.rgb = RGBColor(0x16, 0x21, 0x1C)
    if meta.get("firm_address"):
        sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
        sr = sub.add_run(meta["firm_address"]); sr.font.size = Pt(8)

    t = doc.add_paragraph(); t.alignment = WD_ALIGN_PARAGRAPH.CENTER
    tr = t.add_run(meta.get("title", "")); tr.bold = True; tr.font.size = Pt(12)

    info = doc.add_paragraph()
    ir = info.add_run(f"Client: {meta.get('client','')}    |    Fiscal year: {meta.get('fy','')}    |    "
                      f"Prepared on: {datetime.now().strftime('%d %B %Y')}")
    ir.font.size = Pt(8.5)

    if meta.get("unverified"):
        w = doc.add_paragraph()
        wr = w.add_run("Rate verification outstanding: " + ", ".join(meta["unverified"][:14]) +
                       ". These rates are seeded placeholders. Confirm each against the governing Act "
                       "and the current Finance Act before this working paper is issued or relied upon.")
        wr.bold = True; wr.font.size = Pt(8.5); wr.font.color.rgb = RGBColor(0xA8, 0x20, 0x2E)
    return doc


def _doc_table(doc, headers, rows, widths=None, bold_last=False):
    t = doc.add_table(rows=1, cols=len(headers))
    t.style = "Table Grid"
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    hdr = t.rows[0].cells
    for i, h in enumerate(headers):
        hdr[i].text = ""
        p = hdr[i].paragraphs[0]; r = p.add_run(str(h)); r.bold = True; r.font.size = Pt(9)
    for ri, row in enumerate(rows):
        cells = t.add_row().cells
        for i, v in enumerate(row):
            cells[i].text = ""
            p = cells[i].paragraphs[0]
            if isinstance(v, (int, float)):
                p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
                txt = f"{v:,.2f}" if v else "–"
            else:
                txt = str(v if v is not None else "")
            r = p.add_run(txt); r.font.size = Pt(9)
            if bold_last and ri == len(rows) - 1:
                r.bold = True
    if widths:
        for i, w in enumerate(widths):
            for row in t.rows:
                row.cells[i].width = Cm(w)
    doc.add_paragraph()
    return t


def _sign_block(doc):
    doc.add_paragraph()
    t = doc.add_table(rows=2, cols=2); t.style = "Table Grid"
    for i, label in enumerate(["Prepared by", "Reviewed by"]):
        p = t.rows[0].cells[i].paragraphs[0]; r = p.add_run(label); r.bold = True; r.font.size = Pt(9)
        t.rows[1].cells[i].text = "\n\nName:\nDate:"


def docx_depreciation(res, meta, path):
    doc = _doc_base(meta)
    doc.add_paragraph("The depreciation below is computed on the pooling basis under Schedule 2 of "
                      "the Income Tax Act 2058. Additions are absorbed into the depreciation base "
                      "according to the Nepali month of purchase, and repair expenditure above the "
                      "statutory ceiling has been capitalised into the relevant pool.")
    rows = [[p["pool"], f"{p['rate']*100:.0f}%", p["opening_base"], p["absorbed_additions"],
             p["disposals"], p["depreciation_base"], p["depreciation"], p["closing_base"]]
            for p in res["pools"]]
    t = res["totals"]
    rows.append(["Total", "", t["opening"], t["absorbed"], t["disposals"], "", t["depreciation"], t["closing"]])
    _doc_table(doc, ["Pool", "Rate", "Opening base", "Additions absorbed", "Disposals",
                     "Depreciation base", "Depreciation", "Closing base"],
               rows, [1.6, 1.3, 2.4, 2.6, 2.2, 2.4, 2.3, 2.3], bold_last=True)

    notes = [p for p in res["pools"] if p["note"]]
    if notes:
        h = doc.add_paragraph(); hr = h.add_run("Points arising"); hr.bold = True
        for p in notes:
            doc.add_paragraph(f"Pool {p['pool']}: {p['note']}", style="List Bullet")
    _sign_block(doc)
    doc.save(path); return path


def docx_salary_tds(res, meta, path):
    doc = _doc_base(meta)
    doc.add_paragraph("Employee-wise computation of assessable income, allowable deductions and "
                      "tax deductible at source for the year, including perquisites, foreign-posting "
                      "and retirement-separation permutations where they apply.")
    for e in res["employees"]:
        p = doc.add_paragraph(); r = p.add_run(f"{e['name']} — {e['designation'] or ''}")
        r.bold = True; r.font.size = Pt(10.5)
        perq = e.get("perquisites", {})
        foreign = e.get("foreign", {})
        rows = [["Cash pay (basic, allowances, bonus, overtime, commission, leave encashment)",
                 e.get("cash_pay", e["basic"] + e["allowance"] + e["other"] + e["bonus"])],
                ["Employer fund contribution", e["employer_contribution"]]]
        if perq.get("total"):
            rows.append(["Perquisites (vehicle, accommodation, loan, other)", perq["total"]])
        if foreign.get("taxable"):
            rows.append([f"Foreign allowance — taxable {100 - foreign.get('exempt_pct', 0) * 100:.0f}%", foreign["taxable"]])
        rows += [["Gross assessable income", e["gross_assessable"]],
                ["Less: allowable deductions", e["deductions"]["total"]],
                ["Taxable income (this employer)", e["taxable_income"]]]
        if e.get("other_employer_income"):
            rows.append(["Combined taxable income (with other employer)", e.get("combined_taxable_income", e["taxable_income"])])
        rows.append(["Annual tax liability — this employer", e["annual_tax"]])
        retirement = e.get("retirement", {})
        if retirement.get("applicable"):
            rows.append([f"Retirement lump sum — taxable {retirement['taxable']:,.0f} @ {retirement['rate']*100:.0f}% flat (Sec 65)",
                        retirement["tax"]])
            rows.append(["Total tax payable", e.get("total_tax_payable", e["annual_tax"])])
        rows += [["Less: TDS deducted to date", e["tds_already_deducted"]],
                ["Monthly TDS for remaining months", e["monthly_tds"]]]
        _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [11.0, 4.5], bold_last=True)
    _sign_block(doc)
    doc.save(path); return path


def docx_biz_tax(res, meta, path):
    doc = _doc_base(meta)

    if res.get("regime") == "presumptive":
        p = res["presumptive"]
        doc.add_paragraph("Presumptive tax under Sec 4 — a turnover-based flat tax elected in place of "
                          "the standard book-to-tax computation.")
        rows = [["Turnover", p["turnover"]], ["Area category", p["area_category"]],
                ["Bracket", p.get("bracket_label") or ""], ["Flat tax", p["flat_tax"]],
                ["Less: TDS credit", -res["tds_credit"]], ["Less: advance tax paid", -res["advance_tax_paid"]],
                ["Net tax payable", res["net_payable"]]]
        _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [11.5, 4.0], bold_last=True)
        if p.get("note"):
            doc.add_paragraph(p["note"])
        _sign_block(doc)
        doc.save(path); return path

    doc.add_paragraph("Computation of taxable income and tax liability, reconciled from the profit "
                      "reported in the financial statements. Each adjustment is listed separately so "
                      "the reconciliation can be followed line by line at assessment.")
    rows = [["Profit before tax per financial statements", res["profit_before_tax"]]]
    for a in res["additions"]:
        rows.append([f"Add: {a['desc']}", a["amount"]])
    rows.append(["Add: depreciation charged in books", res["book_depreciation"]])
    for d in res["deductions"]:
        rows.append([f"Less: {d['desc']}", -d["amount"]])
    don = res["donations"]
    rows += [["Less: depreciation per Schedule 2", -res["tax_depreciation"]],
             ["Adjusted taxable income", res["adjusted_income"]],
             [f"Less: donations allowed (of {don['actual']:,.2f} claimed)", -don["allowed"]],
             ["Less: brought-forward loss utilised", -res["loss_utilised"]],
             ["Taxable income", res["taxable_income"]],
             [f"Tax — {res['rate_label']}", res["tax_before_holiday"]],
             [f"Less: tax holiday relief ({res['tax_holiday_pct']*100:.0f}%)", -res["tax_holiday_relief"]],
             ["Tax liability", res["tax_liability"]],
             ["Less: TDS credit", -res["tds_credit"]],
             ["Less: advance tax paid", -res["advance_tax_paid"]],
             ["Net tax payable", res["net_payable"]]]
    _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [11.5, 4.0], bold_last=True)
    if res.get("losses"):
        p = doc.add_paragraph(); r = p.add_run("Brought-forward losses"); r.bold = True
        loss_rows = [[l["fy_label"], l["amount"], "Expired" if l["expired"] else f"Usable — {l['utilised']:,.2f}"]
                     for l in res["losses"]]
        _doc_table(doc, ["FY incurred", "Amount", "Status"], loss_rows, [5.0, 4.0, 6.5])
    if res["donations"]["disallowed"]:
        doc.add_paragraph(f"NPR {res['donations']['disallowed']:,.2f} of the donation claimed exceeds the "
                          "deductible cap and remains in taxable income.")
    if res["refund_due"]:
        doc.add_paragraph(f"A refund of NPR {res['refund_due']:,.2f} arises and should be claimed in the return.")
    _sign_block(doc)
    doc.save(path); return path


def docx_penalty(res, meta, path):
    doc = _doc_base(meta)
    doc.add_paragraph("Estimated exposure to fees, interest and penalties on the facts supplied. "
                      "The basis of each charge is shown so the client can see how the figure is built "
                      "and where a waiver application or a corrected filing would reduce it.")
    rows = [[_PENALTY_CAT_LABEL.get(l.get("category"), l.get("category", "")) + " — " + l["head"], l["basis"], l["amount"]]
            for l in res["lines"]]
    rows.append(["Total exposure", "", res["total"]])
    _doc_table(doc, ["Head of charge", "Basis", "Amount (NPR)"], rows, [5.2, 7.0, 3.3], bold_last=True)
    doc.add_paragraph("This estimate is prepared for internal planning. Final amounts are determined "
                      "by the Inland Revenue Department on assessment and may differ.")
    _sign_block(doc)
    doc.save(path); return path


def docx_emi(res, meta, path):
    doc = _doc_base(meta)
    s = res["summary"]
    doc.add_paragraph("Summary of the borrowing, the instalment profile and the total cost over the term.")
    rows = [["Principal", s["principal"]], ["Instalment", s.get("emi") or 0],
            ["Periods", s["periods"]], ["Total interest (contractual)", s["total_interest"]],
            ["Processing fee", s["processing_fee"]], ["Total cost of borrowing", s["total_cost"]]]
    if s.get("net_proceeds") is not None:
        rows += [["Net proceeds (carrying amount, NFRS 9)", s["net_proceeds"]],
                 ["Effective annual rate", f"{s['effective_annual_rate']*100:.2f}%"]]
    _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [11.0, 4.5], bold_last=True)
    if s.get("net_proceeds") is not None:
        doc.add_paragraph("The processing fee is a transaction cost under NFRS 9 — netted against the "
                          "loan's initial carrying amount and recognised in profit or loss over the term "
                          "at the effective interest rate above, not expensed on drawdown.")
    doc.add_paragraph("The full instalment-wise schedule, the amortised-cost view where a fee applies, "
                      "and the journal register are provided in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


def docx_lease(res, meta, path):
    doc = _doc_base(meta)
    s = res["summary"]

    if res.get("regime") == "exempt":
        doc.add_paragraph("The short-term / low-value asset recognition exemption under NFRS 16 has been "
                          "elected for this lease — payments are expensed straight-line over the term "
                          "rather than capitalised as a right-of-use asset and lease liability.")
        _doc_table(doc, ["Particulars", "Amount (NPR)"],
                   [["Payment per period", res["rows"][0]["expense"] if res["rows"] else 0],
                    ["Periods", s["periods"]], ["Total payments", s["total_payments"]]],
                   [11.0, 4.5], bold_last=True)
        doc.add_paragraph("The period-wise expense schedule and journal register are in the accompanying workbook.")
        _sign_block(doc)
        doc.save(path); return path

    doc.add_paragraph("Measurement of the lease liability and right-of-use asset on initial "
                      "recognition, and the basis of subsequent measurement.")
    _doc_table(doc, ["Particulars", "Amount (NPR)"],
               [["Initial lease liability (present value of payments)", s["initial_liability"]],
                ["Right-of-use asset", s["rou_asset"]],
                ["Lease term (periods)", s["periods"]],
                ["Total payments over term", s["total_payments"]],
                ["Total finance cost", s["total_interest"]],
                ["Amortisation per period", s["monthly_amortisation"]]],
               [11.0, 4.5], bold_last=True)
    doc.add_paragraph("The liability is subsequently measured at amortised cost using the effective "
                      "interest method, and the right-of-use asset is amortised on a straight-line "
                      "basis over the lease term. The period-wise schedule and journal register are "
                      "in the accompanying workbook.")
    _sign_block(doc)
    doc.save(path); return path


def _doc_stmt(doc, title, rows, any_prev, cy, py, any_opening=False):
    p = doc.add_paragraph(); r = p.add_run(title); r.bold = True; r.font.size = Pt(11)
    headers = ["Particulars", "Note"] + (["Opening"] if any_opening else []) + [cy] + ([py] if any_prev else [])
    widths = [6.5, 1.3] + ([2.6] if any_opening else []) + [2.9] + ([2.9] if any_prev else [])
    out_rows = []
    for row in rows:
        t = row.get("t")
        if t in ("sec", "sub"):
            line = [row["l"].upper() if t == "sec" else row["l"], ""] + ([""] if any_opening else []) + [""] + ([""] if any_prev else [])
        else:
            label = ("    " + row["l"]) if row.get("sub") else row["l"]
            line = [label, row.get("n") or ""] + ([row.get("o")] if any_opening else []) + [row.get("v")] + ([row.get("p")] if any_prev else [])
        out_rows.append(line)
    _doc_table(doc, headers, out_rows, widths)


def docx_fin_statements(res, meta, path):
    doc = _doc_base(meta)
    e = res.get("entity") or {}
    cy = e.get("fy_label") or meta.get("fy") or "Current year"
    py = e.get("prev_label") or "Previous year"
    any_prev = res.get("any_prev")
    if e.get("name"):
        p = doc.add_paragraph(); r = p.add_run(e["name"]); r.bold = True
    if e.get("address") or e.get("pan"):
        doc.add_paragraph(" · ".join(x for x in [e.get("address"), ("PAN " + e["pan"]) if e.get("pan") else ""] if x))

    policy = (res.get("accounting_policy") or "").strip()
    if policy:
        h = doc.add_paragraph(); hr = h.add_run("Significant accounting policies"); hr.bold = True; hr.font.size = Pt(11)
        doc.add_paragraph(policy)

    bs = res["balance_sheet"]
    any_opening = bs.get("any_opening")
    _doc_stmt(doc, f"Balance sheet as at {cy}", bs["rows"], any_prev, cy, py, any_opening)
    doc.add_paragraph("Balance sheet ties." if bs["tied"] else
                      f"Does not tie by {bs['tie']:,.2f} — a head is likely mapped to the wrong side.")
    oc = res.get("opening_control") or {}
    if any_opening:
        doc.add_paragraph(
            "Opening balances are internally consistent (debit = credit)." if oc.get("tied") else
            f"Opening balances do not tie by {oc.get('diff', 0):,.2f} — check the opening figures. "
            "This checks internal consistency only, not agreement with last year's filed statements.")
    _doc_stmt(doc, f"Statement of profit and loss for {cy}", res["profit_and_loss"]["rows"], any_prev, cy, py)

    if res["notes"]:
        h = doc.add_paragraph(); hr = h.add_run("Notes to accounts"); hr.bold = True
        for note in res["notes"]:
            np_ = doc.add_paragraph(); nr = np_.add_run(f"Note {note['n']} — {note['label']}"); nr.bold = True
            movement = note.get("movement")
            if movement:
                labels = _FS_MOVEMENT_LABELS.get(note["id"], [])
                move_rows = [[lbl, movement.get(k)] for k, lbl in labels if movement.get(k) is not None]
                if move_rows:
                    mp = doc.add_paragraph(); mr = mp.add_run("Movement (from source module)"); mr.italic = True
                    _doc_table(doc, ["Particulars", "Amount"], move_rows, [11.0, 4.5])
            _doc_table(doc, ["Particulars", "Amount"], [[it["name"], it["net"]] for it in note["items"]], [11.0, 4.5])
    if res.get("unmapped"):
        doc.add_paragraph("Ledgers still unclassified — statements above tie only because these are shown "
                          "under an interim head; assign them before this working paper is relied upon.")
        _doc_table(doc, ["Ledger", "Net"], [[u["name"], u["net"]] for u in res["unmapped"]], [11.0, 4.5])

    disclosures = res.get("disclosures") or {}
    disclosure_labels = [("related_party", "Related party transactions"), ("contingent_liabilities", "Contingent liabilities"),
                         ("capital_commitments", "Capital commitments"), ("subsequent_events", "Subsequent events")]
    active = [(lbl, disclosures.get(k)) for k, lbl in disclosure_labels if (disclosures.get(k) or "").strip()]
    if active:
        h = doc.add_paragraph(); hr = h.add_run("Other disclosures"); hr.bold = True
        for lbl, text in active:
            sp = doc.add_paragraph(); sr = sp.add_run(lbl); sr.bold = True; sr.font.size = Pt(10)
            doc.add_paragraph(text.strip())
    _sign_block(doc)
    doc.save(path); return path


def docx_grant(res, meta, path):
    doc = _doc_base(meta)
    doc.add_paragraph("Recognition, measurement and the deferred-income roll-forward for each government "
                      "grant, prepared under NAS 20 / IAS 20 (and NFRS 9 / IFRS 9 for concessional loans).")
    cons_rows = [[c["fy_label"], c["income"], c["dep"], c["interest"], c["deferred_income_closing"]]
                 for c in res["consolidated"]]
    _doc_table(doc, ["FY", "Grant income", "Depreciation", "Interest", "Deferred income closing"], cons_rows,
               [3.0, 3.5, 3.5, 3.5, 4.0])
    for g in res["grants"]:
        p = doc.add_paragraph(); r = p.add_run(g.get("name", "") + f" ({g.get('kind','')})"); r.bold = True
        for w in g.get("warn", []):
            wp = doc.add_paragraph(); wr = wp.add_run(w); wr.font.color.rgb = RGBColor(0xA8, 0x20, 0x2E); wr.font.size = Pt(8.5)
        fy_years = sorted(g.get("deferred_income_closing_by_fy", {}))
        rows = [[y, R2Val(g.get("fy", {}).get("income", {}).get(str(y))), g["deferred_income_closing_by_fy"][y]]
                for y in fy_years]
        _doc_table(doc, ["FY", "Income recognised", "Deferred income closing"], rows, [4.0, 6.0, 6.0])
    _sign_block(doc)
    doc.save(path); return path


# -------------------------------------------------------- INVENTORY (NAS 2)

def xlsx_inventory(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    entity = res.get("entity") or {}
    s = res["summary"]
    rows = [["Entity", entity.get("name", "")], ["Period", entity.get("period_label", "")],
            ["Closing raw materials", s["closing_raw_materials"]], ["Closing WIP", s["closing_wip"]],
            ["Closing finished goods", s["closing_finished_goods"]],
            ["Total closing inventory", s["closing_inventory_value"]],
            ["Total NRV write-down", s["total_nrv_write_down"]], ["Cost of goods sold", s["cogs"]],
            ["Abnormal loss expense", s["abnormal_loss_expense"]],
            ["Unabsorbed overhead expense", s["unabsorbed_overhead_expense"]]]
    end = _table(ws, r, ["Particulars", "Amount"], rows, [34, 22], money_cols=(2,))
    if not s.get("wip_reconciles"):
        ws.cell(row=end + 1, column=1,
                value="WIP units do not reconcile — opening + introduced should equal transferred + "
                      "closing + normal loss + abnormal loss. Check the WIP inputs.").font = WARN

    ws2 = wb.create_sheet("Raw materials")
    r2 = _stamp(ws2, dict(meta, title="Raw materials — closing position"))
    rm_rows = [[m["name"], m["unit"], m["costing_method"], m["closing_qty"], m["closing_unit_cost"],
               m["closing_value"], m["write_down"], m["valuation_basis"]] for m in res["raw_materials"]]
    _table(ws2, r2, ["Material", "Unit", "Method", "Closing qty", "Closing rate", "Closing value",
                     "Write-down", "Valuation basis"], rm_rows, [22, 10, 14, 14, 14, 16, 14, 34],
           money_cols=(5, 6, 7))

    for m in res["raw_materials"][:40]:
        name = (m["name"] or "Material")[:28].replace("/", "-").replace("*", "")
        ws3 = wb.create_sheet(name or "Material")
        r3 = _stamp(ws3, dict(meta, title=f"Ledger — {m['name']}"))
        led_rows = [[l.get("date", ""), l["type"], l.get("desc", ""), l["qty"], l.get("unit_cost"),
                    l.get("total_cost"), l["balance_qty"], l["balance_cost"]] for l in m["ledger"]]
        _table(ws3, r3, ["Date", "Type", "Description", "Qty", "Rate", "Amount", "Balance qty", "Balance value"],
               led_rows, [12, 10, 24, 12, 12, 16, 14, 16], money_cols=(5, 6, 8))

    ws4 = wb.create_sheet("WIP")
    r4 = _stamp(ws4, dict(meta, title="Work in progress — process costing"))
    w = res["wip"]
    wip_rows = [
        ["Opening WIP — units / cost", w["opening_qty"], w["opening_cost"]],
        ["Units introduced", w["units_introduced"], ""],
        ["Material cost", w["material_cost"], ""],
        ["Direct labour", w["direct_labor_cost"], ""],
        ["Variable overhead", w["variable_overhead"], ""],
        ["Fixed overhead — rate per unit (normal capacity basis)", w["fixed_overhead_rate"], ""],
        ["Fixed overhead absorbed", w["fixed_overhead_absorbed"], ""],
        ["Unabsorbed fixed overhead (expensed, not inventoried)", w["unabsorbed_fixed_overhead"], ""],
        ["Total conversion cost", w["total_conversion_cost"], ""],
        ["Normal loss units / scrap value", w["normal_loss_units"], w["normal_scrap_value"]],
        ["Abnormal loss units / net cost expensed", w["abnormal_loss_units"], w["abnormal_loss_cost"]],
        ["Equivalent units — material", w["equivalent_units_material"], ""],
        ["Equivalent units — conversion", w["equivalent_units_conversion"], ""],
        ["Rate per equivalent unit — material", w["material_rate_per_eu"], ""],
        ["Rate per equivalent unit — conversion", w["conversion_rate_per_eu"], ""],
        ["Transferred to FG — units / cost", w["transferred_units"], w["transferred_cost"]],
        ["Closing WIP — units / value before NRV", w["closing_wip_units"], w["closing_wip_value_before_nrv"]],
        ["NRV write-down", "", w["nrv_write_down"]],
        ["Closing WIP value", "", w["closing_wip_value"]],
        ["Units to account for / accounted for", w["units_to_account"], w["units_accounted"]],
    ]
    _table(ws4, r4, ["Particulars", "Units", "Amount (NPR)"], wip_rows, [46, 18, 20], money_cols=(3,))

    ws5 = wb.create_sheet("Finished goods")
    r5 = _stamp(ws5, dict(meta, title="Finished goods"))
    fg = res["fg"]
    fg_summary = [["Costing method", fg["costing_method"]], ["Closing quantity", fg["closing_qty"]],
                  ["Closing rate", fg["closing_unit_cost"]], ["Write-down to NRV", fg["write_down"]],
                  ["Closing value", fg["closing_value"]], ["COGS quantity", fg["cogs_qty"]],
                  ["COGS value", fg["cogs_value"]]]
    r5b = _table(ws5, r5, ["Particulars", "Amount"], fg_summary, [30, 20], money_cols=(2,)) + 2
    fg_led_rows = [[l.get("date", ""), l["type"], l.get("desc", ""), l["qty"], l.get("unit_cost"),
                    l.get("total_cost"), l["balance_qty"], l["balance_cost"]] for l in fg["ledger"]]
    _table(ws5, r5b, ["Date", "Type", "Description", "Qty", "Rate", "Amount", "Balance qty", "Balance value"],
           fg_led_rows, [12, 10, 24, 12, 12, 16, 14, 16], money_cols=(5, 6, 8))

    _journal_sheet(wb, "Journal entries", res["journals"])
    wb.save(path); return path


def docx_inventory(res, meta, path):
    doc = _doc_base(meta)
    entity = res.get("entity") or {}
    s = res["summary"]
    doc.add_paragraph(f"Inventory valuation for {entity.get('name') or 'the entity'} "
                      + (f"— {entity.get('period_label')}" if entity.get("period_label") else "")
                      + ", measured at the lower of cost and net realisable value under NAS 2.")
    rows = [["Closing raw materials", s["closing_raw_materials"]], ["Closing WIP", s["closing_wip"]],
            ["Closing finished goods", s["closing_finished_goods"]],
            ["Total closing inventory", s["closing_inventory_value"]],
            ["Total NRV write-down", s["total_nrv_write_down"]], ["Cost of goods sold", s["cogs"]],
            ["Abnormal loss expense", s["abnormal_loss_expense"]],
            ["Unabsorbed overhead expense", s["unabsorbed_overhead_expense"]]]
    _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [7.5, 5.5], bold_last=True)
    if not s.get("wip_reconciles"):
        doc.add_paragraph("WIP units do not reconcile — check the opening, introduced, transferred, "
                          "closing and loss figures before relying on this working paper.")

    p = doc.add_paragraph(); r = p.add_run("Raw materials"); r.bold = True
    rm_rows = [[m["name"], m["closing_qty"], m["closing_unit_cost"], m["closing_value"], m["valuation_basis"]]
               for m in res["raw_materials"]]
    _doc_table(doc, ["Material", "Closing qty", "Rate", "Value", "Basis"], rm_rows, [4.5, 2.5, 2.5, 3.0, 4.5])

    doc.add_paragraph("Full ledgers for each material, the process-costing workings for WIP and the "
                      "journal register are in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


# -------------------------------------------------------------- VALUATION
# One report, any combination of workings — driven by the same per-method
# summary-field and line-item specs the engine's own result shape defines,
# rather than one hand-written report per method.

_VAL_METHOD_LABELS = {
    "dcf": "Discounted cash flow — FCFF", "fcfe": "Discounted cash flow — FCFE",
    "income_capitalization": "Income capitalisation",
    "market_comparable": "Comparable company analysis", "precedent_transaction": "Precedent transaction analysis",
    "nav": "Net asset value",
    "liquidation": "Liquidation value", "relief_from_royalty": "Relief from royalty",
    "excess_earnings": "Multi-period excess earnings (MEEM)",
    "rcnld": "Replacement cost new less depreciation",
}

_VAL_LINE_SPECS = {
    "dcf": (["Year", "FCF", "Discount factor", "PV"], ["year", "fcf", "discount_factor", "pv"]),
    "fcfe": (["Year", "FCFE", "Discount factor", "PV"], ["year", "fcf", "discount_factor", "pv"]),
    "relief_from_royalty": (["Year", "Revenue", "Royalty saving", "After-tax saving", "PV"],
                            ["year", "revenue", "royalty_saving", "after_tax_saving", "pv"]),
    "excess_earnings": (["Year", "Earnings", "After-tax earnings", "Contributory charges", "Excess earnings", "PV"],
                        ["year", "earnings", "after_tax_earnings", "contributory_charges", "excess_earnings", "pv"]),
}

_VAL_TABLE_SPECS = {
    "nav": [("assets", "Assets", ["Description", "Book value", "Fair value", "Adjustment"],
            ["desc", "book_value", "fair_value", "adjustment"]),
           ("liabilities", "Liabilities", ["Description", "Book value", "Fair value", "Adjustment"],
            ["desc", "book_value", "fair_value", "adjustment"])],
    "market_comparable": [("comparables", "Comparables", ["Name", "Base value", "Adjustment %", "Adjusted value"],
                          ["name", "base_value", "adjustment_pct", "adjusted_value"])],
    "precedent_transaction": [("comparables", "Precedent transactions", ["Name", "Base value", "Adjustment %", "Adjusted value"],
                              ["name", "base_value", "adjustment_pct", "adjusted_value"])],
}

_VAL_SUMMARY_KEYS = {
    "dcf": [("PV of cash flows", "pv_of_cashflows"), ("Discount rate %", "discount_rate_pct"),
           ("Terminal value", "terminal_value"),
           ("PV of terminal value", "pv_of_terminal_value"), ("Enterprise value", "enterprise_value"),
           ("Less: net debt", "net_debt"), ("Add: non-operating assets", "non_operating_assets"),
           ("Less: minority interest", "minority_interest"), ("Equity value", "value"),
           ("Value per share", "value_per_share")],
    "fcfe": [("PV of cash flows", "pv_of_cashflows"), ("Discount rate (cost of equity) %", "discount_rate_pct"),
           ("Terminal value", "terminal_value"), ("PV of terminal value", "pv_of_terminal_value"),
           ("Add: non-operating assets", "non_operating_assets"), ("Less: minority interest", "minority_interest"),
           ("Equity value", "value"), ("Value per share", "value_per_share")],
    "income_capitalization": [("Normalised income", "normalized_income"), ("Cap rate %", "cap_rate_pct"),
           ("Growth %", "growth_pct"), ("Gross value", "gross_value"), ("Less: net debt", "net_debt"),
           ("Value", "value"), ("Value per share", "value_per_share")],
    "market_comparable": [("Chosen multiple/rate", "chosen_multiple_or_rate"), ("Subject metric", "subject_metric"),
           ("Gross value", "gross_value"), ("Less: deductions", "deduction_total"),
           ("Add: premiums", "premium_total"), ("Value", "value"), ("Value per share", "value_per_share")],
    "precedent_transaction": [("Chosen multiple/rate", "chosen_multiple_or_rate"), ("Subject metric", "subject_metric"),
           ("Gross value", "gross_value"), ("Less: deductions", "deduction_total"),
           ("Add: premiums", "premium_total"), ("Value", "value"), ("Value per share", "value_per_share")],
    "nav": [("Total assets (fair value)", "assets_total"), ("Total liabilities (fair value)", "liabilities_total"),
           ("Value", "value"), ("Value per share", "value_per_share")],
    "liquidation": [("NAV", "nav_value"), ("Forced-sale discount %", "forced_sale_discount_pct"),
           ("Value after discount", "value_after_discount"), ("Less: winding-up costs", "winding_up_costs"),
           ("Less: priority claims", "priority_claims"), ("Value", "value")],
    "relief_from_royalty": [("PV of royalty savings", "pv_of_royalty_savings"), ("Terminal value", "terminal_value"),
           ("PV of terminal value", "pv_of_terminal_value"),
           ("Tax amortisation benefit applied", "tax_amortization_benefit_applied"), ("Value", "value")],
    "excess_earnings": [("PV of excess earnings", "pv_of_excess_earnings"), ("Terminal value", "terminal_value"),
           ("PV of terminal value", "pv_of_terminal_value"), ("Value", "value")],
    "rcnld": [("Replacement cost new", "replacement_cost_new"),
           ("Physical deterioration %", "physical_deterioration_pct"),
           ("Value after physical deterioration", "value_after_physical"),
           ("Functional obsolescence %", "functional_obsolescence_pct"),
           ("Value after functional obsolescence", "value_after_functional"),
           ("Economic obsolescence %", "economic_obsolescence_pct"), ("Value", "value")],
}


def _val_title(res, meta):
    subj = res.get("subject") or {}
    if subj.get("subject_type") == "share":
        return dict(meta, title="Share Valuation Report")
    return dict(meta, title=meta.get("title") or "Valuation Report")


def _val_sensitivity_rows(grid):
    """Flattens the engine's nested {rate_key, cells:[{growth_key, value}]} grid into a rectangular table."""
    if not grid:
        return None, None
    rate_key = [k for k in grid[0] if k != "cells"][0]
    growth_key = [k for k in grid[0]["cells"][0] if k != "value"][0]
    growth_vals = [c[growth_key] for c in grid[0]["cells"]]
    headers = [rate_key] + [f"{growth_key} {g}" for g in growth_vals]
    rows = [[row[rate_key]] + [c["value"] for c in row["cells"]] for row in grid]
    return headers, rows


def xlsx_valuation(res, meta, path):
    meta = _val_title(res, meta)
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    subj = res.get("subject") or {}
    rows = [["Subject", subj.get("name", "")], ["Subject type", subj.get("subject_type", "")],
            ["Valuation date", subj.get("valuation_date", "")], ["Purpose", subj.get("purpose", "")]]
    r = _table(ws, r, ["Particulars", "Detail"], rows, [28, 50]) + 2

    working_rows = [[w["label"], _VAL_METHOD_LABELS.get(w["method"], w["method"]), w.get("approach", "") or "",
                     w.get("fair_value_level") or "", w["value"]] for w in res["workings"]]
    r = _table(ws, r, ["Working", "Method", "Approach", "Fair value level (NFRS 13)", "Value"], working_rows,
              [22, 30, 12, 22, 18], money_cols=(5,)) + 2

    cons = res["conclusion"]
    if cons["blend"]:
        blend_rows = [[b["label"], b["value"], b["weight"]] for b in cons["blend"]]
        r = _table(ws, r, ["Working", "Value", "Weight"], blend_rows, [30, 20, 14], money_cols=(2,))
        r = _total_row(ws, r, "Concluded value", {2: cons["concluded_value"]}) + 2
    if cons.get("range_low") is not None:
        ws.cell(row=r, column=1, value="Range").font = LBL
        ws.cell(row=r, column=2, value=f"{cons['range_low']:,.2f} — {cons['range_high']:,.2f}").font = TXT
        r += 2
    if cons.get("notes"):
        ws.cell(row=r, column=1, value=cons["notes"]).font = TXT
        r += 2

    drb = res.get("discount_rate_builder")
    if drb:
        ws.cell(row=r, column=1, value="Discount rate builder (CAPM / WACC)").font = LBL; r += 1
        drb_rows = [["Risk-free rate %", drb["risk_free_rate_pct"]], ["Beta", drb["beta"]],
                   ["Equity risk premium %", drb["equity_risk_premium_pct"]], ["Size premium %", drb["size_premium_pct"]],
                   ["Specific risk premium %", drb["specific_risk_premium_pct"]],
                   ["Cost of equity % (CAPM)", drb["cost_of_equity_pct"]],
                   ["Cost of debt %", drb["cost_of_debt_pct"]], ["Tax rate %", drb["tax_rate_pct"]],
                   ["After-tax cost of debt %", drb["after_tax_cost_of_debt_pct"]],
                   ["Weight of equity %", drb["weight_of_equity_pct"]], ["Weight of debt %", drb["weight_of_debt_pct"]],
                   ["WACC %", drb["wacc_pct"]]]
        r = _table(ws, r, ["Particulars", "Value"], drb_rows, [36, 18]) + 2

    review = res.get("review") or {}
    if review.get("flags") or review.get("reviewed_by"):
        ws.cell(row=r, column=1, value="Valuation review / challenge").font = LBL; r += 1
        for f in review.get("flags") or []:
            ws.cell(row=r, column=1, value=f["severity"]).font = TXT
            ws.cell(row=r, column=2, value=f["message"]).font = TXT
            r += 1
        r += 1
        if review.get("reviewed_by"):
            rev_rows = [["Reviewed by", review.get("reviewed_by")], ["Review date", review.get("review_date")],
                       ["Key assumptions challenged", review.get("key_assumptions_challenged")],
                       ["Independence confirmed", "Yes" if review.get("independence_confirmed") else "No"],
                       ["Review conclusion", review.get("review_conclusion")], ["Review status", review.get("review_status")]]
            r = _table(ws, r, ["Particulars", "Detail"], rev_rows, [30, 50]) + 2

    for w in res["workings"]:
        name = (w["label"] or w["method"])[:28].replace("/", "-").replace("*", "").replace("[", "").replace("]", "")
        ws2 = wb.create_sheet(name or w["method"])
        r2 = _stamp(ws2, dict(meta, title=f"{w['label']} — working"))
        summary = _VAL_SUMMARY_KEYS.get(w["method"], [])
        srows = [[label, w.get(key)] for label, key in summary if w.get(key) is not None]
        r2 = _table(ws2, r2, ["Particulars", "Amount"], srows, [36, 22], money_cols=(2,)) + 2

        if w["method"] in _VAL_LINE_SPECS:
            headers, keys = _VAL_LINE_SPECS[w["method"]]
            lrows = [[l.get(k) for k in keys] for l in w.get("lines", [])]
            r2 = _table(ws2, r2, headers, lrows, [10] + [16] * (len(headers) - 1),
                       money_cols=tuple(range(2, len(headers) + 1))) + 2

        for spec in _VAL_TABLE_SPECS.get(w["method"], []):
            key, title, headers, keys2 = spec
            rows2 = [[x.get(k) for k in keys2] for x in w.get(key, [])]
            if rows2:
                ws2.cell(row=r2, column=1, value=title).font = LBL
                r2 = _table(ws2, r2 + 1, headers, rows2, [24] + [16] * (len(headers) - 1),
                           money_cols=tuple(range(2, len(headers) + 1))) + 2

        sgrid = w.get("sensitivity_grid")
        if sgrid:
            headers, srows2 = _val_sensitivity_rows(sgrid)
            ws2.cell(row=r2, column=1, value="Sensitivity — value at each combination").font = LBL
            r2 = _table(ws2, r2 + 1, headers, srows2, [16] + [16] * (len(headers) - 1),
                       money_cols=tuple(range(2, len(headers) + 1))) + 2
    wb.save(path); return path


def docx_valuation(res, meta, path):
    meta = _val_title(res, meta)
    doc = _doc_base(meta)
    subj = res.get("subject") or {}
    doc.add_paragraph(f"Valuation of {subj.get('name') or 'the subject'} "
                      f"({subj.get('subject_type', '')}) as at {subj.get('valuation_date') or 'the valuation date'}"
                      + (f", for the purpose of {subj['purpose']}" if subj.get("purpose") else "") + ".")

    working_rows = [[w["label"], _VAL_METHOD_LABELS.get(w["method"], w["method"]),
                     w.get("fair_value_level") or "", w["value"]] for w in res["workings"]]
    _doc_table(doc, ["Working", "Method", "FV level", "Value (NPR)"], working_rows, [4.5, 5.5, 2.0, 4.0])

    cons = res["conclusion"]
    if cons["blend"]:
        p = doc.add_paragraph(); r = p.add_run("Conclusion"); r.bold = True
        blend_rows = [[b["label"], b["value"], f"{b['weight']:,.0f}"] for b in cons["blend"]]
        blend_rows.append(["Concluded value", cons["concluded_value"], ""])
        _doc_table(doc, ["Working", "Value (NPR)", "Weight"], blend_rows, [6.0, 5.0, 3.0], bold_last=True)
    if cons.get("range_low") is not None:
        doc.add_paragraph(f"Range of values across all workings: NPR {cons['range_low']:,.2f} "
                          f"to NPR {cons['range_high']:,.2f}.")
    if cons.get("notes"):
        doc.add_paragraph(cons["notes"])

    drb = res.get("discount_rate_builder")
    if drb:
        p = doc.add_paragraph(); r = p.add_run("Discount rate builder (CAPM / WACC)"); r.bold = True
        _doc_table(doc, ["Particulars", "Value"], [
            ["Cost of equity % (CAPM)", drb["cost_of_equity_pct"]], ["After-tax cost of debt %", drb["after_tax_cost_of_debt_pct"]],
            ["Weight of equity %", drb["weight_of_equity_pct"]], ["Weight of debt %", drb["weight_of_debt_pct"]],
            ["WACC %", drb["wacc_pct"]],
        ], [7.5, 5.5])

    for w in res["workings"]:
        p = doc.add_paragraph(); r = p.add_run(f"{w['label']} ({_VAL_METHOD_LABELS.get(w['method'], w['method'])})")
        r.bold = True; r.font.size = Pt(10.5)
        summary = _VAL_SUMMARY_KEYS.get(w["method"], [])
        srows = [[label, w.get(key)] for label, key in summary if w.get(key) is not None]
        _doc_table(doc, ["Particulars", "Amount"], srows, [7.5, 5.5], bold_last=True)
    doc.add_paragraph("Full year-by-year workings and sensitivity grids for each method are in the accompanying Excel workbook.")

    review = res.get("review") or {}
    if review.get("flags") or review.get("reviewed_by"):
        p = doc.add_paragraph(); r = p.add_run("Valuation review / challenge"); r.bold = True
        for f in review.get("flags") or []:
            fp = doc.add_paragraph(); fr = fp.add_run(f["severity"] + ": "); fr.bold = True
            fp.add_run(f["message"])
        if review.get("reviewed_by"):
            _doc_table(doc, ["Particulars", "Detail"], [
                ["Reviewed by", review.get("reviewed_by")], ["Review date", review.get("review_date")],
                ["Key assumptions challenged", review.get("key_assumptions_challenged")],
                ["Independence confirmed", "Yes" if review.get("independence_confirmed") else "No"],
                ["Review conclusion", review.get("review_conclusion")], ["Review status", review.get("review_status")],
            ], [6.0, 7.0])

    _sign_block(doc)
    doc.save(path); return path


# ----------------------------------------------- FIXED ASSETS (ACCOUNTS, NAS 16)

def xlsx_fixed_assets(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    entity = res.get("entity") or {}
    s = res["summary"]
    rows = [["Entity", entity.get("name", "")], ["Period", entity.get("period_label", "")],
            ["Fiscal year — from", res.get("fy_start", "")], ["Fiscal year — to", res.get("fy_end", "")],
            ["Opening net book value", s["opening_nbv"]], ["Additions during the year (at cost)", s["additions_cost"]],
            ["Depreciation charge for the year", s["depreciation"]], ["Closing net book value", s["closing_nbv"]],
            ["Disposal proceeds", s["disposal_proceeds"]], ["Gain / (loss) on disposal", s["gain_loss_on_disposal"]]]
    _table(ws, r, ["Particulars", "Amount"], rows, [34, 22], money_cols=(2,))

    ws2 = wb.create_sheet("Depreciation summary")
    r2 = _stamp(ws2, dict(meta, title="Depreciation summary — by asset class"))
    cls_rows = []
    for c in res["classes"]:
        basis = f"{c['rate_pct']}% WDV" if c["method"] == "wdv" and c.get("rate_pct") is not None \
            else (f"{c['useful_life_years']} yrs SLM" if c["method"] == "slm" and c.get("useful_life_years") is not None else c["method"].upper())
        t = c["totals"]
        cls_rows.append([c["label"], c["method"].upper(), basis, t["opening_nbv"], t["additions_cost"],
                         t["depreciation"], t["closing_nbv"], t["disposal_proceeds"], t["gain_loss"]])
    end = _table(ws2, r2, ["Class", "Method", "Rate / life", "Opening NBV", "Additions", "Depreciation",
                           "Closing NBV", "Disposal proceeds", "Gain/(loss)"], cls_rows,
                [22, 10, 14, 16, 16, 16, 16, 16, 14], money_cols=(4, 5, 6, 7, 8, 9))
    _total_row(ws2, end, "Total", {4: s["opening_nbv"], 5: s["additions_cost"], 6: s["depreciation"],
                                   7: s["closing_nbv"], 8: s["disposal_proceeds"], 9: s["gain_loss_on_disposal"]})

    ws3 = wb.create_sheet("Asset master")
    r3 = _stamp(ws3, dict(meta, title="Asset master — full register"))
    asset_rows = []
    for c in res["classes"]:
        for a in c["assets"]:
            asset_rows.append([c["label"], a["code"], a["name"], a["status"], a["method"],
                               a.get("acquisition_date") or "", a["cost"], a["opening_nbv"], a["depreciation"],
                               a["closing_nbv"], a.get("disposal_date") or "", a["disposal_proceeds"],
                               a["gain_loss_on_disposal"]])
    _table(ws3, r3, ["Class", "Code", "Name", "Status", "Method", "Acquisition date", "Cost", "Opening NBV",
                     "Depreciation", "Closing NBV", "Disposal date", "Disposal proceeds", "Gain/(loss)"],
           asset_rows, [18, 12, 26, 10, 8, 14, 14, 14, 14, 14, 14, 14, 12], money_cols=(7, 8, 9, 10, 12, 13))

    _journal_sheet(wb, "Journal entries", res["journals"])
    wb.save(path); return path


def docx_fixed_assets(res, meta, path):
    doc = _doc_base(meta)
    entity = res.get("entity") or {}
    s = res["summary"]
    doc.add_paragraph(f"Fixed assets and depreciation for {entity.get('name') or 'the entity'} "
                      + (f"— {entity.get('period_label')}" if entity.get("period_label") else "")
                      + f", {res.get('fy_start', '')} to {res.get('fy_end', '')}, computed under the "
                        "entity's own accounting policy (NAS 16), separately from the Schedule 2 tax computation.")
    rows = [["Opening net book value", s["opening_nbv"]], ["Additions during the year", s["additions_cost"]],
            ["Depreciation charge for the year", s["depreciation"]], ["Closing net book value", s["closing_nbv"]],
            ["Disposal proceeds", s["disposal_proceeds"]], ["Gain / (loss) on disposal", s["gain_loss_on_disposal"]]]
    _doc_table(doc, ["Particulars", "Amount (NPR)"], rows, [7.5, 5.5], bold_last=True)

    p = doc.add_paragraph(); r = p.add_run("Depreciation summary by asset class"); r.bold = True
    cls_rows = [[c["label"], c["method"].upper(), c["totals"]["opening_nbv"], c["totals"]["depreciation"],
                c["totals"]["closing_nbv"]] for c in res["classes"]]
    _doc_table(doc, ["Class", "Method", "Opening NBV", "Depreciation", "Closing NBV"], cls_rows,
              [4.5, 2.5, 3.0, 3.0, 3.0])

    doc.add_paragraph("The full asset-by-asset register and the journal register are in the accompanying "
                      "Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------ CLIENT DIRECTORY

_CD_VAT_LABELS = {"monthly": "Monthly", "four_monthly": "Four-monthly", "six_monthly": "Six-monthly",
                  "not_registered": "Not VAT registered"}


def xlsx_client_directory(rows, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Clients"
    r = _stamp(ws, dict(meta, title="Client directory export"))
    table_rows = []
    for c in rows:
        table_rows.append([
            c.get("code", ""), c.get("name", ""), c.get("entity_type", "") or "", c.get("stage", "") or "",
            "Yes" if c.get("urgent") else "", _CD_VAT_LABELS.get(c.get("vat_filing_frequency"), c.get("vat_filing_frequency") or ""),
            "Registered" if c.get("vat_registered") else "Not registered", c.get("pan", "") or "",
            ", ".join(c.get("service_types") or []), c.get("assigned_to_name", "") or "",
            c.get("contact_person", "") or "", c.get("contact_phone", "") or "", c.get("contact_email", "") or "",
            c.get("ird_office", "") or "", c.get("status", "") or "", c.get("address", "") or "", c.get("notes", "") or "",
        ])
    _table(ws, r, ["Code", "Name", "Entity type", "Stage", "Urgent", "VAT frequency", "VAT status", "PAN",
                   "Service types", "Assigned to", "Contact person", "Contact phone", "Contact email",
                   "IRD office", "Status", "Address", "Notes"],
           table_rows, [10, 26, 14, 16, 8, 14, 16, 14, 22, 18, 18, 14, 22, 14, 10, 26, 26])
    wb.save(path); return path


# ------------------------------------------------------------------- BILLING

def docx_invoice(inv, meta, path):
    doc = _doc_base(dict(meta, title=f"Invoice {inv['invoice_no']}"))
    info = doc.add_paragraph()
    ir = info.add_run(f"Bill to: {inv['client_name']}\nInvoice date: {inv['invoice_date']}"
                      + (f"    Due: {inv['due_date']}" if inv.get("due_date") else "")
                      + f"\nStatus: {inv['status'].upper()}")
    ir.font.size = Pt(9.5)

    line_rows = [[l["description"], l.get("hours") or "", l.get("rate") or "", l["amount"]] for l in inv["lines"]]
    _doc_table(doc, ["Description", "Hours", "Rate", "Amount (NPR)"], line_rows, [7.0, 2.0, 2.5, 3.5])

    p = doc.add_paragraph()
    r = p.add_run(f"Subtotal: {inv['subtotal']:,.2f}"
                  + (f"\nVAT @ {inv['vat_rate'] * 100:.0f}%: {inv['vat_amount']:,.2f}" if inv.get("vat_amount") else "")
                  + f"\nTotal due: {inv['total']:,.2f}")
    r.bold = True; r.font.size = Pt(11)

    if inv.get("paid_amount"):
        pp = doc.add_paragraph()
        ppr = pp.add_run(f"Paid: {inv['paid_amount']:,.2f}" + (f" on {inv['paid_date']}" if inv.get("paid_date") else ""))
        ppr.font.size = Pt(9.5)

    if inv.get("notes"):
        doc.add_paragraph(inv["notes"])

    doc.add_paragraph("Thank you for your business.")
    doc.save(path); return path


# ------------------------------------------------------------------- AUDIT

_RISK_ORDER = {"high": 0, "medium": 1, "low": 2}


def _audit_signatory_block(doc, sig):
    doc.add_paragraph()
    p = doc.add_paragraph(); r = p.add_run("For " + (sig.get("firm_name") or "[Firm name]")); r.bold = True
    doc.add_paragraph("Chartered Accountants")
    doc.add_paragraph("\n\n[Signature and firm stamp]\n")
    p2 = doc.add_paragraph(); r2 = p2.add_run(sig.get("name") or "[Signatory name]"); r2.bold = True
    designation = {"ca": "Chartered Accountant", "ra": "Registered Auditor"}.get(
        (sig.get("designation") or "").lower(), sig.get("designation") or "[Designation]")
    doc.add_paragraph(designation + (f" (Membership No. {sig['membership_no']})" if sig.get("membership_no") else ""))
    doc.add_paragraph("Place: " + (sig.get("place") or "[Place]"))
    doc.add_paragraph("Date: " + (sig.get("date") or "[Date]"))
    if sig.get("udin"):
        doc.add_paragraph("UDIN: " + sig["udin"])


def docx_internal_audit(res, meta, path):
    doc = _doc_base(dict(meta, title="Internal Audit Report"))
    entity = res.get("entity") or {}
    s = res["summary"]
    entity_type = entity.get("entity_type") or ""
    if entity_type == "Other":
        entity_type = entity.get("other_entity_type_label") or "Other"
    doc.add_paragraph(f"Internal audit of {entity.get('name') or 'the entity'} "
                      + (f"({entity_type}) " if entity_type else "")
                      + (f"for {entity.get('period_label')}" if entity.get("period_label") else "")
                      + ".")
    if entity.get("scope"):
        p = doc.add_paragraph(); r = p.add_run("Scope: "); r.bold = True
        p.add_run(entity["scope"])
    if entity.get("objectives"):
        p = doc.add_paragraph(); r = p.add_run("Objectives: "); r.bold = True
        p.add_run(entity["objectives"])

    p = doc.add_paragraph(); r = p.add_run("Executive summary"); r.bold = True
    summary_rows = [
        ["Criteria tested", f"{s['tested_criteria']} of {s['total_criteria']} ({s['completion_pct']:.1f}% complete)"],
        ["Exceptions noted", s["exceptions_count"]],
        ["Findings raised", s["findings_total"]],
        ["Findings still open", s["findings_open"]],
        ["Findings by risk — high / medium / low",
         f"{s['findings_by_risk'].get('high', 0)} / {s['findings_by_risk'].get('medium', 0)} / {s['findings_by_risk'].get('low', 0)}"],
    ]
    _doc_table(doc, ["Particulars", "Value"], summary_rows, [7.5, 5.5])

    risk_universe = res.get("risk_universe") or []
    if risk_universe:
        p = doc.add_paragraph(); r = p.add_run("Risk-based audit plan"); r.bold = True
        ru_rows = [[ru["process"], ru["likelihood"], ru["impact"], ru["score"], ru["priority"]] for ru in risk_universe]
        _doc_table(doc, ["Process / area", "Likelihood", "Impact", "Score", "Priority"], ru_rows, [5.5, 2.0, 2.0, 1.5, 2.0])

    control_gaps = res.get("control_gaps") or []
    if control_gaps:
        p = doc.add_paragraph(); r = p.add_run("Control gap analysis"); r.bold = True
        cg_rows = [[g.get("process", ""), g.get("risk_description", ""), g.get("control_description", ""),
                   g.get("design_effectiveness", ""), g.get("operating_effectiveness", "")] for g in control_gaps]
        _doc_table(doc, ["Process", "Risk", "Control", "Design", "Operating"], cg_rows, [2.5, 3.0, 3.0, 2.0, 2.5])

    fraud_risk = res.get("fraud_risk") or {}
    if any(fraud_risk.values()):
        p = doc.add_paragraph(); r = p.add_run("Fraud risk assessment"); r.bold = True
        for label, key in (("Incentives / pressures", "incentives_pressures"), ("Opportunities", "opportunities"),
                           ("Attitudes / rationalization", "attitudes_rationalization"),
                           ("Management override notes", "override_notes"), ("Conclusion", "conclusion")):
            if fraud_risk.get(key):
                pp = doc.add_paragraph(); rr = pp.add_run(label + ": "); rr.bold = True
                pp.add_run(fraud_risk[key])

    conclusion = res.get("conclusion") or {}
    if conclusion.get("summary"):
        p = doc.add_paragraph(); r = p.add_run("Conclusion"); r.bold = True
        doc.add_paragraph(conclusion["summary"]
                          + (f" Overall rating: {conclusion['overall_rating']}." if conclusion.get("overall_rating") else ""))

    findings = sorted(res.get("findings") or [], key=lambda f: _RISK_ORDER.get((f.get("risk_rating") or "low").lower(), 3))
    if findings:
        p = doc.add_paragraph(); r = p.add_run("Findings"); r.bold = True
        f_rows = [[f.get("area", ""), (f.get("risk_rating") or "").upper(), f.get("observation", ""),
                  f.get("recommendation", ""), (f.get("status") or "open").upper()] for f in findings]
        _doc_table(doc, ["Area", "Risk", "Observation", "Recommendation", "Status"], f_rows,
                  [2.5, 1.5, 4.0, 4.0, 1.5])
    else:
        doc.add_paragraph("No findings were raised during this internal audit.")

    walkthroughs = res.get("walkthroughs") or []
    if walkthroughs:
        p = doc.add_paragraph(); r = p.add_run("Process walkthroughs"); r.bold = True
        wt_rows = [[w.get("process", ""), w.get("narrative", ""), w.get("walkthrough_date", ""),
                   w.get("performed_by", ""), w.get("conclusion", "")] for w in walkthroughs]
        _doc_table(doc, ["Process", "Narrative", "Date", "Performed by", "Conclusion"], wt_rows, [2.5, 3.5, 1.8, 2.2, 3.0])

    follow_ups = res.get("follow_ups") or []
    if follow_ups:
        p = doc.add_paragraph(); r = p.add_run("Follow-up audit"); r.bold = True
        fu_rows = [[fu.get("finding_ref", ""), fu.get("follow_up_date", ""), fu.get("action_taken", ""),
                   fu.get("status", ""), fu.get("remarks", "")] for fu in follow_ups]
        _doc_table(doc, ["Finding reference", "Date", "Action taken", "Status", "Remarks"], fu_rows, [3.0, 1.8, 3.5, 2.0, 2.7])

    p = doc.add_paragraph(); r = p.add_run("Area-wise checklist coverage"); r.bold = True
    area_rows = []
    for a in res.get("areas") or []:
        tested = sum(1 for c in a["criteria"] if c["status"] != "not_tested")
        exceptions = sum(1 for c in a["criteria"] if c["status"] == "exception")
        area_rows.append([a["area"], len(a["criteria"]), tested, exceptions])
    _doc_table(doc, ["Area", "Criteria", "Tested", "Exceptions"], area_rows, [6.0, 2.0, 2.0, 2.0])

    doc.add_paragraph("The full criterion-by-criterion checklist, with evidence and remarks, is in the "
                      "accompanying Excel workbook.")
    _audit_signatory_block(doc, res.get("signatory") or {})
    doc.save(path); return path


def xlsx_internal_audit(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Checklist"
    r = _stamp(ws, dict(meta, title="Internal audit checklist"))
    rows = []
    for a in res.get("areas") or []:
        for c in a["criteria"]:
            rows.append([a["area"], c["criterion"], c["status"], c.get("evidence", ""), c.get("remark", "")])
    _table(ws, r, ["Area", "Criterion", "Status", "Evidence", "Remark"], rows, [20, 42, 14, 24, 24])

    ws2 = wb.create_sheet("Findings")
    r2 = _stamp(ws2, dict(meta, title="Findings register"))
    f_rows = [[f.get("area", ""), f.get("category", ""), f.get("observation", ""), (f.get("risk_rating") or "").upper(),
              f.get("recommendation", ""), f.get("management_response", ""), f.get("responsible_person", ""),
              f.get("target_date", ""), (f.get("status") or "open").upper()]
             for f in (res.get("findings") or [])]
    _table(ws2, r2, ["Area", "Category", "Observation", "Risk", "Recommendation", "Management response",
                     "Responsible", "Target date", "Status"], f_rows, [16, 16, 30, 10, 26, 26, 16, 14, 12])

    risk_universe = res.get("risk_universe") or []
    if risk_universe:
        ws3 = wb.create_sheet("Risk universe")
        r3 = _stamp(ws3, dict(meta, title="Risk-based audit plan"))
        ru_rows = [[ru["process"], ru["likelihood"], ru["impact"], ru["score"], ru["priority"], ru.get("notes", "")]
                  for ru in risk_universe]
        _table(ws3, r3, ["Process / area", "Likelihood", "Impact", "Score", "Priority", "Notes"], ru_rows, [24, 12, 10, 10, 12, 30])

    racm = res.get("racm") or []
    if racm:
        ws4 = wb.create_sheet("RACM")
        r4 = _stamp(ws4, dict(meta, title="Process risk & control matrix"))
        racm_rows = [[row.get("process", ""), row.get("risk_description", ""), row.get("control_description", ""),
                     row.get("control_type", ""), row.get("control_frequency", ""), row.get("control_owner", ""),
                     row.get("design_effectiveness", ""), row.get("operating_effectiveness", ""), row.get("testing_result", "")]
                    for row in racm]
        _table(ws4, r4, ["Process", "Risk description", "Control description", "Control type", "Frequency",
                        "Owner", "Design", "Operating", "Testing result"], racm_rows, [18, 26, 26, 16, 14, 16, 16, 16, 22])

    walkthroughs = res.get("walkthroughs") or []
    if walkthroughs:
        ws5 = wb.create_sheet("Walkthroughs")
        r5 = _stamp(ws5, dict(meta, title="Process walkthroughs"))
        wt_rows = [[w.get("process", ""), w.get("narrative", ""), w.get("walkthrough_date", ""),
                   w.get("performed_by", ""), w.get("conclusion", "")] for w in walkthroughs]
        _table(ws5, r5, ["Process", "Narrative", "Date", "Performed by", "Conclusion"], wt_rows, [18, 40, 14, 18, 26])

    follow_ups = res.get("follow_ups") or []
    if follow_ups:
        ws6 = wb.create_sheet("Follow-up")
        r6 = _stamp(ws6, dict(meta, title="Follow-up audit"))
        fu_rows = [[fu.get("finding_ref", ""), fu.get("follow_up_date", ""), fu.get("action_taken", ""),
                   fu.get("status", ""), fu.get("remarks", "")] for fu in follow_ups]
        _table(ws6, r6, ["Finding reference", "Date", "Action taken", "Status", "Remarks"], fu_rows, [26, 14, 34, 16, 26])

    wb.save(path); return path


def docx_stat_audit(res, meta, path):
    doc = _doc_base(dict(meta, title="Statutory Audit — Planning & Program"))
    entity = res.get("entity") or {}
    m = res.get("materiality") or {}
    ss = res.get("sample_size") or {}
    doc.add_paragraph(f"Statutory audit planning for {entity.get('name') or 'the entity'} "
                      + (f"— {entity.get('period_label')}" if entity.get("period_label") else "") + ".")

    p = doc.add_paragraph(); r = p.add_run("Materiality"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Benchmark", m.get("benchmark", "")], ["Benchmark value", m.get("benchmark_value", 0)],
        ["Materiality %", f"{m.get('materiality_pct', 0):.2f}%"], ["Overall materiality", m.get("overall_materiality", 0)],
        ["Performance materiality %", f"{m.get('pm_pct', 0):.0f}% of overall"],
        ["Performance materiality", m.get("performance_materiality", 0)],
        ["Clearly trivial threshold", m.get("clearly_trivial", 0)],
    ], [7.5, 5.5], bold_last=True)

    p = doc.add_paragraph(); r = p.add_run("Sample size (planning estimate)"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Population value", ss.get("population_value", 0)], ["Population count", ss.get("population_count", 0)],
        ["Tolerable misstatement", ss.get("tolerable_misstatement", 0)],
        ["Expected misstatement", ss.get("expected_misstatement", 0)],
        ["Risk level / factor", f"{ss.get('risk_level', '')} / {ss.get('risk_factor', '')}"],
        ["Sampling interval", ss.get("sampling_interval", 0)], ["Sample size", ss.get("sample_size", 0)],
    ], [7.5, 5.5])
    doc.add_paragraph(ss.get("note", ""))

    p = doc.add_paragraph(); r = p.add_run("Risk assessment by area"); r.bold = True
    risk_rows = [[ra["area"], ra["inherent"].upper(), ra["control"].upper(), ra["combined_risk"].upper(),
                 ra["detection_risk_required"].upper(), ra.get("rationale", "")]
                for ra in (res.get("risk_assessment") or [])]
    _doc_table(doc, ["Area", "Inherent", "Control", "Combined risk", "Detection risk needed", "Rationale"],
              risk_rows, [3.0, 1.8, 1.8, 2.2, 2.2, 4.0])

    s = res["summary"]
    p = doc.add_paragraph(); r = p.add_run("Program completion"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Procedures tested", f"{s['tested_criteria']} of {s['total_criteria']} ({s['completion_pct']:.1f}%)"],
        ["Exceptions noted", s["exceptions_count"]], ["Findings raised", s["findings_total"]],
        ["Findings open", s["findings_open"]],
    ], [7.5, 5.5])

    findings = sorted(res.get("findings") or [], key=lambda f: _RISK_ORDER.get((f.get("risk_rating") or "low").lower(), 3))
    if findings:
        p = doc.add_paragraph(); r = p.add_run("Findings"); r.bold = True
        f_rows = [[f.get("area", ""), (f.get("risk_rating") or "").upper(), f.get("observation", ""),
                  f.get("recommendation", ""), (f.get("status") or "open").upper()] for f in findings]
        _doc_table(doc, ["Area", "Risk", "Observation", "Recommendation", "Status"], f_rows,
                  [2.5, 1.5, 4.0, 4.0, 1.5])

    doc.add_paragraph("The full audit program (procedure, sample basis, working-paper reference, evidence and "
                      "result per line) is in the accompanying Excel workbook. Use the Auditor's Report module "
                      "to issue the opinion once fieldwork concludes.")
    _audit_signatory_block(doc, res.get("signatory") or {})
    doc.save(path); return path


def xlsx_stat_audit(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Materiality & Risk"
    r = _stamp(ws, dict(meta, title="Statutory audit — planning"))
    m = res.get("materiality") or {}; ss = res.get("sample_size") or {}
    rows = [
        ["Benchmark", m.get("benchmark", "")], ["Benchmark value", m.get("benchmark_value", 0)],
        ["Materiality %", m.get("materiality_pct", 0)], ["Overall materiality", m.get("overall_materiality", 0)],
        ["Performance materiality %", m.get("pm_pct", 0)], ["Performance materiality", m.get("performance_materiality", 0)],
        ["Clearly trivial threshold", m.get("clearly_trivial", 0)],
        ["", ""],
        ["Sample — population value", ss.get("population_value", 0)], ["Sample — population count", ss.get("population_count", 0)],
        ["Sample — tolerable misstatement", ss.get("tolerable_misstatement", 0)],
        ["Sample — expected misstatement", ss.get("expected_misstatement", 0)],
        ["Sample — risk level", ss.get("risk_level", "")], ["Sample — risk factor", ss.get("risk_factor", "")],
        ["Sample — sampling interval", ss.get("sampling_interval", 0)], ["Sample size", ss.get("sample_size", 0)],
    ]
    end = _table(ws, r, ["Particulars", "Value"], rows, [26, 20], money_cols=(2,))

    ws2 = wb.create_sheet("Risk by area")
    r2 = _stamp(ws2, dict(meta, title="Risk assessment by area"))
    risk_rows = [[ra["area"], ra["inherent"], ra["control"], ra["combined_risk"], ra["detection_risk_required"], ra.get("rationale", "")]
                for ra in (res.get("risk_assessment") or [])]
    _table(ws2, r2, ["Area", "Inherent", "Control", "Combined risk", "Detection risk needed", "Rationale"],
          risk_rows, [22, 12, 12, 14, 18, 34])

    ws3 = wb.create_sheet("Audit program")
    r3 = _stamp(ws3, dict(meta, title="Audit program"))
    prog_rows = []
    for a in res.get("areas") or []:
        for c in a["criteria"]:
            prog_rows.append([a["area"], c["criterion"], c.get("procedure", ""), c.get("sample_basis", ""),
                              c["status"], c.get("evidence", ""), c.get("wp_ref", ""), c.get("performed_by", ""),
                              c.get("result", "")])
    _table(ws3, r3, ["Area", "Criterion", "Procedure", "Sample basis", "Status", "Evidence", "W/P ref",
                     "Performed by", "Result"], prog_rows, [16, 34, 24, 16, 12, 18, 10, 14, 18])

    ws4 = wb.create_sheet("Findings")
    r4 = _stamp(ws4, dict(meta, title="Findings register"))
    f_rows = [[f.get("area", ""), f.get("observation", ""), (f.get("risk_rating") or "").upper(),
              f.get("recommendation", ""), f.get("management_response", ""), f.get("responsible_person", ""),
              f.get("target_date", ""), (f.get("status") or "open").upper()]
             for f in (res.get("findings") or [])]
    _table(ws4, r4, ["Area", "Observation", "Risk", "Recommendation", "Management response",
                     "Responsible", "Target date", "Status"], f_rows, [16, 30, 10, 26, 26, 16, 14, 12])
    wb.save(path); return path


def xlsx_audit_report(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Report"
    r = _stamp(ws, dict(meta, title="Independent auditor's report — summary"))
    sig = res.get("signatory") or {}
    rows = [
        ["Opinion type", res.get("opinion_type_label", "")],
        ["Opinion paragraph", res.get("opinion_paragraph", "")],
        ["Basis paragraph", res.get("basis_paragraph", "")],
        ["Emphasis of Matter", res.get("eom_paragraph", "") if res.get("has_eom") else ""],
        ["Other Matter", res.get("other_matter_paragraph", "") if res.get("has_other_matter") else ""],
        ["Signatory", sig.get("name", "")], ["Designation", sig.get("designation", "")],
        ["Firm", sig.get("firm_name", "")], ["Place", sig.get("place", "")],
        ["Date", sig.get("date", "")], ["UDIN", sig.get("udin", "")],
    ]
    _table(ws, r, ["Particulars", "Value"], rows, [22, 60])
    wb.save(path); return path


def docx_audit_report(res, meta, path):
    entity = res.get("entity") or {}
    doc = _doc_base(dict(meta, title="Independent Auditor's Report"))
    doc.add_paragraph(f"To the Shareholders / Members of {entity.get('name') or '[Entity name]'}")

    opinion_headings = {
        "unqualified": "Opinion", "qualified": "Qualified Opinion",
        "adverse": "Adverse Opinion", "disclaimer": "Disclaimer of Opinion",
    }
    p = doc.add_paragraph(); r = p.add_run(opinion_headings.get(res["opinion_type"], "Opinion")); r.bold = True
    doc.add_paragraph(res.get("opinion_paragraph") or "[Opinion paragraph not entered]")

    basis_headings = {
        "unqualified": "Basis for Opinion", "qualified": "Basis for Qualified Opinion",
        "adverse": "Basis for Adverse Opinion", "disclaimer": "Basis for Disclaimer of Opinion",
    }
    if res.get("basis_paragraph"):
        p = doc.add_paragraph(); r = p.add_run(basis_headings.get(res["opinion_type"], "Basis for Opinion")); r.bold = True
        doc.add_paragraph(res["basis_paragraph"])

    if res.get("has_kam"):
        p = doc.add_paragraph(); r = p.add_run("Key Audit Matters"); r.bold = True
        for kam in res["kam_paragraphs"]:
            if isinstance(kam, dict):
                p2 = doc.add_paragraph(); r2 = p2.add_run(kam.get("heading", "")); r2.bold = True; r2.italic = True
                doc.add_paragraph(kam.get("text", ""))
            else:
                doc.add_paragraph(str(kam))

    if res.get("has_eom"):
        p = doc.add_paragraph(); r = p.add_run("Emphasis of Matter"); r.bold = True
        doc.add_paragraph(res["eom_paragraph"])

    if res.get("has_other_matter"):
        p = doc.add_paragraph(); r = p.add_run("Other Matter"); r.bold = True
        doc.add_paragraph(res["other_matter_paragraph"])

    p = doc.add_paragraph(); r = p.add_run("Responsibilities of Management and Those Charged with Governance for the Financial Statements"); r.bold = True
    doc.add_paragraph(res.get("responsibilities_management") or
                      "Management is responsible for the preparation and fair presentation of these financial "
                      "statements in accordance with the applicable financial reporting framework, and for such "
                      "internal control as management determines is necessary to enable the preparation of "
                      "financial statements that are free from material misstatement, whether due to fraud or "
                      "error. In preparing the financial statements, management is responsible for assessing the "
                      "entity's ability to continue as a going concern, disclosing, as applicable, matters related "
                      "to going concern and using the going concern basis of accounting unless management either "
                      "intends to liquidate the entity or to cease operations, or has no realistic alternative but "
                      "to do so. Those charged with governance are responsible for overseeing the entity's "
                      "financial reporting process.")

    p = doc.add_paragraph(); r = p.add_run("Auditor's Responsibilities for the Audit of the Financial Statements"); r.bold = True
    doc.add_paragraph(res.get("responsibilities_auditor") or
                      "Our objectives are to obtain reasonable assurance about whether the financial statements as "
                      "a whole are free from material misstatement, whether due to fraud or error, and to issue an "
                      "auditor's report that includes our opinion. Reasonable assurance is a high level of "
                      "assurance but is not a guarantee that an audit conducted in accordance with Nepal Standards "
                      "on Auditing will always detect a material misstatement when it exists. Misstatements can "
                      "arise from fraud or error and are considered material if, individually or in the aggregate, "
                      "they could reasonably be expected to influence the economic decisions of users taken on the "
                      "basis of these financial statements.")

    _audit_signatory_block(doc, res.get("signatory") or {})
    doc.save(path); return path


# ------------------------------------------------------------- TDS / WITHHOLDING

def xlsx_tds(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Register"
    r = _stamp(ws, dict(meta, title="TDS / withholding register"))
    rows = [[e["payment_date"], e["payee_name"], e["payee_pan"], e["category_label"], e.get("description", ""),
            e["gross_amount"], e["rate"], e["tds_amount"], e["net_amount"],
            "Yes" if e["deposited"] else "No", e.get("deposit_date", ""), e.get("deposit_ref", "")]
           for e in res["entries"]]
    _table(ws, r, ["Date", "Payee", "PAN", "Category", "Description", "Gross", "Rate %", "TDS", "Net paid",
                   "Deposited", "Deposit date", "Deposit ref"], rows,
           [12, 26, 14, 30, 24, 14, 8, 14, 14, 10, 14, 16], money_cols=(6, 8, 9), pct_cols=())

    ws2 = wb.create_sheet("Summary by category")
    r2 = _stamp(ws2, dict(meta, title="TDS by category"))
    cat_rows = [[c["label"], c["gross"], c["tds"]] for c in res["summary"]["by_category"]]
    _table(ws2, r2, ["Category", "Gross", "TDS"], cat_rows, [40, 18, 18], money_cols=(2, 3))

    ws3 = wb.create_sheet("Summary by payee")
    r3 = _stamp(ws3, dict(meta, title="TDS by payee"))
    payee_rows = [[p["payee_name"], p["payee_pan"], p["gross"], p["tds"]] for p in res["by_payee"]]
    _table(ws3, r3, ["Payee", "PAN", "Gross", "TDS"], payee_rows, [30, 16, 18, 18], money_cols=(3, 4))
    wb.save(path); return path


def docx_tds(res, meta, path):
    doc = _doc_base(dict(meta, title="TDS / Withholding — Register and Certificates"))
    s = res["summary"]
    doc.add_paragraph("Withholding tax deducted on payments made during the period, as a withholding agent "
                      "under the Income Tax Act 2058.")
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Total gross payments", s["total_gross"]], ["Total TDS deducted", s["total_tds"]],
        ["Total net paid", s["total_net"]], ["TDS deposited", s["deposited_tds"]],
        ["TDS pending deposit", s["pending_tds"]],
    ], [7.5, 5.5], bold_last=True)

    p = doc.add_paragraph(); r = p.add_run("Summary by category"); r.bold = True
    _doc_table(doc, ["Category", "Gross", "TDS"],
              [[c["label"], c["gross"], c["tds"]] for c in s["by_category"]], [7.0, 2.5, 2.5])

    doc.add_paragraph()
    p = doc.add_paragraph(); r = p.add_run("Withholding tax certificates"); r.bold = True; r.font.size = Pt(12)
    doc.add_paragraph("One certificate per payee below — the amount(s) withheld from payments made to them "
                      "during the period, for issue to the payee as evidence of tax deducted at source.")

    for payee in res["by_payee"]:
        doc.add_paragraph()
        p = doc.add_paragraph(); r = p.add_run(f"Certificate of Tax Deducted at Source"); r.bold = True; r.font.size = Pt(11)
        info = doc.add_paragraph()
        ir = info.add_run(f"Payer: {meta.get('firm_name', '')}\nPayee: {payee['payee_name']}"
                          + (f"  |  PAN: {payee['payee_pan']}" if payee["payee_pan"] else "")
                          + f"\nPeriod: {meta.get('fy', '')}")
        ir.font.size = Pt(9.5)
        line_rows = [[l["payment_date"], l["category_label"], l["gross_amount"], f"{l['rate']:.2f}%", l["tds_amount"]]
                    for l in payee["lines"]]
        _doc_table(doc, ["Date", "Nature of payment", "Gross amount", "Rate", "TDS deducted"], line_rows,
                  [2.5, 4.5, 2.5, 1.5, 2.5], bold_last=True)
        doc.add_paragraph(f"Total tax deducted at source: NPR {payee['tds']:,.2f}")

    _audit_signatory_block(doc, res.get("signatory") or {})
    doc.save(path); return path


# --------------------------------------------------------- PERSONAL INCOME TAX

def xlsx_personal_tax(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Computation"
    r = _stamp(ws, dict(meta, title="Personal income tax computation"))
    i, d = res["incomes"], res["deductions"]
    rows = [
        ["Employment income (assessable)", i["employment"]], ["Business net profit", i["business"]],
        ["Rental — gross receipts", i["rental_gross"]], ["Rental — deductible expenses", i["rental_expenses"]],
        ["Rental — net", i["rental_net"]], ["Other income", i["other"]],
        ["Gross assessable income", i["gross_income"]],
        ["Less: retirement contribution deduction", d["retirement"]],
        ["Less: life insurance premium deduction", d["life_insurance"]],
        ["Less: health insurance premium deduction", d["health_insurance"]],
        ["Adjusted income (before donation)", res["adjusted_income"]],
        ["Less: donation deduction", d["donation"]],
        ["Taxable income", res["taxable_income"]],
        ["Tax before rebate", res["tax_before_rebate"]], ["Less: rebate", res["rebate"]],
        ["Tax payable", res["tax_payable"]],
    ]
    end = _table(ws, r, ["Particulars", "Amount"], rows, [34, 20], money_cols=(2,))

    ws2 = wb.create_sheet("Slab detail")
    r2 = _stamp(ws2, dict(meta, title="Slab-wise tax"))
    slab_rows = [[l["band"], f"{l['rate']*100:.0f}%", l["income"], l["tax"]] for l in res["slab_lines"]]
    _table(ws2, r2, ["Band", "Rate", "Income in band", "Tax"], slab_rows, [26, 10, 18, 18], money_cols=(3, 4))
    wb.save(path); return path


def docx_personal_tax(res, meta, path):
    doc = _doc_base(dict(meta, title="Personal Income Tax Computation"))
    person = res.get("person") or {}
    i, d = res["incomes"], res["deductions"]
    doc.add_paragraph(f"Personal income tax computation — filing status: "
                      f"{'Couple' if person.get('status') == 'couple' else 'Individual'}.")
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Employment income (assessable)", i["employment"]], ["Business net profit", i["business"]],
        ["Rental income (net of deductible expenses)", i["rental_net"]], ["Other income", i["other"]],
        ["Gross assessable income", i["gross_income"]],
    ], [7.5, 5.5])

    p = doc.add_paragraph(); r = p.add_run("Deductions"); r.bold = True
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Retirement contribution (Sec 63)", d["retirement"]], ["Life insurance premium", d["life_insurance"]],
        ["Health insurance premium", d["health_insurance"]], ["Donation", d["donation"]],
        ["Total deductions", d["total"]],
    ], [7.5, 5.5])
    if d["donation_disallowed"]:
        doc.add_paragraph(f"Donation disallowed (above the deductible cap): NPR {d['donation_disallowed']:,.2f}.")

    p = doc.add_paragraph(); r = p.add_run("Slab-wise tax"); r.bold = True
    _doc_table(doc, ["Band", "Rate", "Income in band", "Tax"],
              [[l["band"], f"{l['rate']*100:.0f}%", l["income"], l["tax"]] for l in res["slab_lines"]],
              [4.0, 1.5, 3.5, 3.5])

    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Taxable income", res["taxable_income"]], ["Tax before rebate", res["tax_before_rebate"]],
        ["Rebate", res["rebate"]], ["Tax payable", res["tax_payable"]],
    ], [7.5, 5.5], bold_last=True)

    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------------------ VAT

def xlsx_vat(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Sales"
    r = _stamp(ws, dict(meta, title="VAT return — sales register"))
    sales_rows = [[s["date"], s["party"], s["desc"], s["taxable_amount"], s["zero_rated_amount"],
                  s["exempt_amount"], s["vat_amount"]] for s in res["sales"]]
    _table(ws, r, ["Date", "Party", "Description", "Taxable", "Zero-rated", "Exempt", "Output VAT"],
           sales_rows, [12, 26, 30, 16, 16, 16, 16], money_cols=(4, 5, 6, 7))

    ws2 = wb.create_sheet("Purchases")
    r2 = _stamp(ws2, dict(meta, title="VAT return — purchase register"))
    purch_rows = [[p["date"], p["party"], p["desc"], p["taxable_amount"],
                   "Yes" if p["blocked"] else "No", "Yes" if p["capital_goods"] else "No", p["vat_amount"]]
                  for p in res["purchases"]]
    _table(ws2, r2, ["Date", "Party", "Description", "Taxable", "Blocked", "Capital goods", "Input VAT"],
           purch_rows, [12, 26, 30, 16, 12, 14, 16], money_cols=(4, 7))

    ws3 = wb.create_sheet("Return summary")
    r3 = _stamp(ws3, dict(meta, title="VAT return — summary"))
    t = res["totals"]
    rows = [
        ["Taxable sales", t["taxable_sales"]], ["Zero-rated sales", t["zero_rated_sales"]],
        ["Exempt sales", t["exempt_sales"]], ["Credit notes issued", t["credit_notes_issued"]],
        ["Debit notes issued", t["debit_notes_issued"]], ["Net taxable sales", t["net_taxable_sales"]],
        [f"Output VAT @ {res['rate']:.0f}%", t["output_vat"]],
        ["Creditable purchases", t["creditable_purchases"]], ["Blocked purchases (no credit)", t["blocked_purchases"]],
        ["— of which capital goods", t["capital_purchases"]],
        ["Credit notes received", t["credit_notes_received"]], ["Debit notes received", t["debit_notes_received"]],
        ["Net creditable purchases", t["net_creditable_purchases"]], ["Input VAT (gross)", t["input_vat_gross"]],
        ["Apportionment ratio applied" if t["mixed_supply"] else "Apportionment (not applicable — no exempt mix)",
         f"{t['apportionment_ratio']:.2f}%"],
        ["Eligible input VAT", t["eligible_input_vat"]], ["Input VAT restricted (exempt-supply mix)", t["restricted_input_vat"]],
        ["Opening credit brought forward", t["opening_credit"]], ["Total credit available", t["total_credit_available"]],
        ["VAT payable", t["payable"]], ["Credit carried forward", t["credit_carried_forward"]],
    ]
    _table(ws3, r3, ["Particulars", "Amount"], rows, [40, 20], money_cols=(2,))
    wb.save(path); return path


def docx_vat(res, meta, path):
    doc = _doc_base(dict(meta, title="VAT Return"))
    e = res.get("entity") or {}
    t = res["totals"]
    doc.add_paragraph(f"VAT return for {e.get('name', meta.get('client', ''))} "
                      f"(PAN {e.get('pan', '')}), period {e.get('period_label', meta.get('fy', ''))}, "
                      f"prepared under the Value Added Tax Act 2052.")

    p = doc.add_paragraph(); r = p.add_run("Output tax"); r.bold = True
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Taxable sales", t["taxable_sales"]], ["Zero-rated sales (exports)", t["zero_rated_sales"]],
        ["Exempt sales", t["exempt_sales"]], ["Credit notes issued", t["credit_notes_issued"]],
        ["Debit notes issued", t["debit_notes_issued"]], ["Net taxable sales", t["net_taxable_sales"]],
        [f"Output VAT @ {res['rate']:.0f}%", t["output_vat"]],
    ], [7.5, 5.5], bold_last=True)

    p = doc.add_paragraph(); r = p.add_run("Input tax"); r.bold = True
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Creditable purchases", t["creditable_purchases"]],
        ["Blocked purchases (Rule 41 — no credit)", t["blocked_purchases"]],
        ["Credit notes received", t["credit_notes_received"]], ["Debit notes received", t["debit_notes_received"]],
        ["Net creditable purchases", t["net_creditable_purchases"]], ["Input VAT (gross)", t["input_vat_gross"]],
    ], [7.5, 5.5], bold_last=True)
    if t["mixed_supply"]:
        doc.add_paragraph(f"Mixed taxable/exempt supply — input VAT restricted to the taxable share of turnover "
                          f"({t['apportionment_ratio']:.2f}%). Restricted (non-creditable) input VAT: "
                          f"NPR {t['restricted_input_vat']:,.2f}.")

    p = doc.add_paragraph(); r = p.add_run("Net position"); r.bold = True
    _doc_table(doc, ["Particulars", "Amount (NPR)"], [
        ["Output VAT", t["output_vat"]], ["Eligible input VAT", t["eligible_input_vat"]],
        ["Opening credit brought forward", t["opening_credit"]], ["Total credit available", t["total_credit_available"]],
        ["VAT payable", t["payable"]], ["Credit carried forward to next period", t["credit_carried_forward"]],
    ], [7.5, 5.5], bold_last=True)

    if res.get("blocked_categories"):
        doc.add_paragraph()
        p = doc.add_paragraph(); r = p.add_run("Reference — blocked input categories (VAT Act 2052, Rule 41(1))"); r.bold = True
        for cat in res["blocked_categories"]:
            doc.add_paragraph(cat, style="List Bullet")

    _sign_block(doc)
    doc.save(path); return path


# --------------------------------------------------------- transfer pricing
# Phase 1 ships three reports: Related Party Transaction, FAR + Method +
# Benchmarking, and Risk Assessment. Every one states jurisdiction/FY/
# framework up front and carries the same UNVERIFIED banner every other
# report in this app carries when a cited rule hasn't been confirmed.

def _tp_framework_line(meta, res):
    country = res.get("country")
    if not country:
        return "Jurisdiction not yet selected for this engagement — set the client's country before relying on this report."
    return (f"Jurisdiction: {country['name']} ({country['code']}) — tax authority: {country.get('tax_authority') or 'n/a'}. "
            f"Financial year: {res.get('fy', meta.get('fy', ''))}.")


def xlsx_tp_related_party(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Entities"
    r = _stamp(ws, dict(meta, title="Transfer pricing — related party transaction report"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    _table(ws, r, ["Entity", "Country", "Type", "PAN / TIN", "This client?", "Notes"],
           [[e["name"], e.get("country") or "", e.get("entity_type") or "", e.get("pan_or_tin") or "",
             "Yes" if e["is_the_client"] else "No", e.get("notes") or ""] for e in res["entities"]],
           [26, 12, 20, 16, 12, 30])

    ws2 = wb.create_sheet("Relationships")
    r2 = _stamp(ws2, dict(meta, title="Related party relationships"))
    _table(ws2, r2, ["Entity A", "Entity B", "Relationship", "Ownership %", "Voting rights %", "Basis tested under"],
           [[rl["entity_a_name"], rl["entity_b_name"], rl.get("relationship_type") or "", rl.get("ownership_pct"),
             rl.get("voting_rights_pct"), rl.get("country_test_basis") or ""] for rl in res["relationships"]],
           [22, 22, 18, 14, 16, 18], pct_cols=(4, 5))

    ws3 = wb.create_sheet("Transactions")
    r3 = _stamp(ws3, dict(meta, title="Related party transaction register"))
    _table(ws3, r3, ["FY", "Tested entity", "Counterparty", "Source", "Destination", "Category", "Currency",
                     "Amount", "Direction", "Method"],
           [[t["financial_year_label"], t.get("tested_entity_name") or "", t.get("counterparty_entity_name") or "",
             t.get("source_country") or "", t.get("destination_country") or "", t["transaction_category"],
             t.get("currency") or "", t.get("amount"), t.get("direction") or "", t.get("method_selected") or ""]
            for t in res["transactions"]],
           [10, 20, 20, 10, 12, 16, 10, 14, 12, 10], money_cols=(8,))
    wb.save(path); return path


def docx_tp_related_party(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Related Party Transaction Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    if res.get("unverified"):
        p = doc.add_paragraph(); r = p.add_run(
            "One or more regulatory rules cited for this jurisdiction are UNVERIFIED — confirm against current "
            "law/administrative guidance before this report is relied upon or issued to the client."); r.bold = True

    p = doc.add_paragraph(); r = p.add_run("Group / related entities"); r.bold = True
    _doc_table(doc, ["Entity", "Country", "Type", "This client?"],
              [[e["name"], e.get("country") or "", e.get("entity_type") or "", "Yes" if e["is_the_client"] else "No"]
               for e in res["entities"]], [4.5, 2.5, 3.5, 2.5])

    p = doc.add_paragraph(); r = p.add_run("Relationships / basis of association"); r.bold = True
    _doc_table(doc, ["Entity A", "Entity B", "Relationship", "Ownership %", "Tested under"],
              [[rl["entity_a_name"], rl["entity_b_name"], rl.get("relationship_type") or "",
                rl.get("ownership_pct"), rl.get("country_test_basis") or ""] for rl in res["relationships"]],
              [3.5, 3.5, 2.5, 2, 2.5])

    p = doc.add_paragraph(); r = p.add_run("Transaction register"); r.bold = True
    _doc_table(doc, ["FY", "Tested entity", "Counterparty", "Category", "Currency", "Amount", "Method"],
              [[t["financial_year_label"], t.get("tested_entity_name") or "", t.get("counterparty_entity_name") or "",
                t["transaction_category"], t.get("currency") or "", t.get("amount"), t.get("method_selected") or ""]
               for t in res["transactions"]], [1.5, 2.5, 2.5, 2, 1.5, 2, 1.5])

    _sign_block(doc)
    doc.save(path); return path


def xlsx_tp_far_method_benchmark(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "FAR & method"
    r = _stamp(ws, dict(meta, title="Transfer pricing — FAR, method & benchmarking report"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    rows = []
    for t in res["transactions"]:
        far = res["far_by_txn"].get(t["id"]) or {}
        method = res["method_by_txn"].get(t["id"]) or {}
        rows.append([t["transaction_category"], t.get("tested_entity_name") or "", far.get("functional_profile") or "",
                    far.get("conclusion") or "", method.get("recommended_method") or "",
                    method.get("selected_method") or "", method.get("override_justification") or ""])
    _table(ws, r, ["Transaction", "Tested entity", "Functional profile", "FAR conclusion",
                   "Recommended method", "Selected method", "Override justification"],
           rows, [16, 20, 22, 30, 18, 16, 30])

    ws2 = wb.create_sheet("Benchmarking")
    r2 = _stamp(ws2, dict(meta, title="Benchmarking results"))
    rows2 = []
    for t in res["transactions"]:
        b = res["bench_by_txn"].get(t["id"])
        s = (b or {}).get("stats") or {}
        rows2.append([t["transaction_category"], t.get("tested_entity_name") or "", s.get("count"), s.get("min"),
                     s.get("q1"), s.get("median"), s.get("q3"), s.get("max"), s.get("weighted_average")])
    _table(ws2, r2, ["Transaction", "Tested entity", "N", "Min", "Q1", "Median", "Q3", "Max", "Weighted avg"],
           rows2, [16, 20, 6, 10, 10, 10, 10, 10, 14])
    wb.save(path); return path


def docx_tp_far_method_benchmark(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — FAR, Method Selection & Benchmarking Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    doc.add_paragraph("This report sets out the Functional, Asset and Risk (FAR) analysis, the recommended and "
                      "selected transfer pricing method (with justification for any override), and the "
                      "statistical benchmarking result for each related party transaction reviewed.")
    for t in res["transactions"]:
        far = res["far_by_txn"].get(t["id"]) or {}
        method = res["method_by_txn"].get(t["id"]) or {}
        bench = res["bench_by_txn"].get(t["id"]) or {}
        stats = bench.get("stats") or {}
        risk = res["risk_by_txn"].get(t["id"]) or {}

        p = doc.add_paragraph(); r = p.add_run(
            f"{t['transaction_category']} — {t.get('tested_entity_name') or 'tested entity n/a'} "
            f"↔ {t.get('counterparty_entity_name') or 'counterparty n/a'} (FY {t['financial_year_label']})"); r.bold = True
        if far.get("functional_profile") or far.get("conclusion"):
            doc.add_paragraph(f"Functional profile: {far.get('functional_profile') or 'not yet assessed'}. "
                              f"Conclusion: {far.get('conclusion') or 'pending'}.")
        else:
            doc.add_paragraph("FAR analysis not yet recorded for this transaction.")
        method_line = f"Recommended method: {method.get('recommended_method') or 'not yet computed'}"
        if method.get("recommendation_basis"):
            method_line += f" — {method['recommendation_basis']}"
        doc.add_paragraph(method_line)
        if method.get("selected_method") and method.get("selected_method") != method.get("recommended_method"):
            doc.add_paragraph(f"Selected method: {method['selected_method']} (override). "
                              f"Justification: {method.get('override_justification') or 'NOT RECORDED — required for an override.'}")
        if stats.get("count"):
            doc.add_paragraph(f"Benchmarking set: n={stats['count']}, range {stats.get('min')}–{stats.get('max')}, "
                              f"median {stats.get('median')}, interquartile range {stats.get('q1')}–{stats.get('q3')}.")
        else:
            doc.add_paragraph("No accepted comparable set has been benchmarked yet for this transaction.")
        if risk.get("risk_level"):
            doc.add_paragraph(f"Risk classification: {risk['risk_level']}.")
        doc.add_paragraph()

    _sign_block(doc)
    doc.save(path); return path


def xlsx_tp_risk_assessment(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Risk assessment"
    r = _stamp(ws, dict(meta, title="Transfer pricing — risk assessment report"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    rows = []
    for t in res["transactions"]:
        risk = res["risk_by_txn"].get(t["id"]) or {}
        factors = ", ".join(f["factor"] for f in (risk.get("factors") or [])) if risk.get("factors") else ""
        rows.append([t["transaction_category"], t.get("tested_entity_name") or "", t.get("amount"),
                    risk.get("risk_level") or "Not assessed", risk.get("score"), factors])
    _table(ws, r, ["Transaction", "Tested entity", "Amount", "Risk level", "Score", "Contributing factors"],
           rows, [16, 20, 14, 14, 8, 50], money_cols=(3,))
    wb.save(path); return path


def docx_tp_risk_assessment(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Risk Assessment Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    doc.add_paragraph("Risk is scored on a Low / Medium / High / Very High scale using a weighted point-factor "
                      "model (transaction size, comparable availability, benchmark range, loss-making history, "
                      "related-party concentration, cross-border and sensitive-category complexity, and "
                      "documentation completeness). This is a professional screening tool, not a substitute for "
                      "the reviewing partner's judgement.")
    rows = []
    for t in res["transactions"]:
        risk = res["risk_by_txn"].get(t["id"]) or {}
        factors = "; ".join(f"{f['factor']} (+{f['points']})" for f in (risk.get("factors") or []))
        rows.append([t["transaction_category"], t.get("tested_entity_name") or "", risk.get("risk_level") or "Not assessed", factors])
    _doc_table(doc, ["Transaction", "Tested entity", "Risk level", "Contributing factors"], rows, [2.5, 2.5, 2, 6.5])
    _sign_block(doc)
    doc.save(path); return path


# ---- Phase 2 report types: the comprehensive study, adjustment exposure,
# documentation gap, advisory memo, client information request and working
# paper pack. All reuse the same result dict tp_generate_report() assembles
# (now extended with adj_by_txn/checklist/working_papers) — no new data
# assembly per report, matching the pattern every builder above follows.

def xlsx_tp_study(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Overview"
    r = _stamp(ws, dict(meta, title="Transfer pricing study — overview"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    ws.cell(row=r, column=1, value=f"Entities: {len(res['entities'])}   Transactions: {len(res['transactions'])}   "
            f"Relationships: {len(res['relationships'])}").font = TXT

    ws2 = wb.create_sheet("Entities & relationships")
    r2 = _stamp(ws2, dict(meta, title="Group structure"))
    _table(ws2, r2, ["Entity", "Country", "Type", "This client?"],
           [[e["name"], e.get("country") or "", e.get("entity_type") or "", "Yes" if e["is_the_client"] else "No"] for e in res["entities"]],
           [26, 12, 20, 12])

    ws3 = wb.create_sheet("Transactions")
    r3 = _stamp(ws3, dict(meta, title="Transaction register"))
    _table(ws3, r3, ["Tested entity", "Counterparty", "Category", "Currency", "Amount", "Method", "Risk"],
           [[t.get("tested_entity_name") or "", t.get("counterparty_entity_name") or "", t["transaction_category"],
             t.get("currency") or "", t.get("amount"), t.get("method_selected") or "", t.get("risk_level") or ""]
            for t in res["transactions"]], [20, 20, 16, 10, 14, 14, 12], money_cols=(5,))

    ws4 = wb.create_sheet("Adjustment exposure")
    r4 = _stamp(ws4, dict(meta, title="Adjustment exposure summary"))
    rows4 = []
    for t in res["transactions"]:
        a = res["adj_by_txn"].get(t["id"])
        if a: rows4.append([t.get("tested_entity_name") or "", t["transaction_category"], a["actual_result"], a["arm_length_result"],
                            a["adjustment_amount"], a["tax_impact"], a["total_exposure"]])
    _table(ws4, r4, ["Tested entity", "Category", "Actual", "Arm's length", "Adjustment", "Tax impact", "Total exposure"],
           rows4, [20, 16, 12, 12, 14, 14, 14], money_cols=(5, 6, 7))

    ws5 = wb.create_sheet("Documentation status")
    r5 = _stamp(ws5, dict(meta, title="Documentation checklist"))
    _table(ws5, r5, ["Requirement", "Status", "Notes"],
           [[c["label"], c["status"], c.get("notes") or ""] for c in res["checklist"]], [40, 14, 40])
    wb.save(path); return path


def docx_tp_study(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing Study Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    if res.get("unverified"):
        p = doc.add_paragraph(); r = p.add_run(
            "One or more regulatory rules cited for this jurisdiction are UNVERIFIED — confirm against current "
            "law/administrative guidance before this study is relied upon or issued to the client."); r.bold = True
    doc.add_paragraph("This report is a comprehensive transfer pricing study covering the group structure, the "
                      "related-party transaction register, functional/asset/risk (FAR) analysis, method selection, "
                      "benchmarking, risk assessment, adjustment exposure and documentation status for the "
                      "financial year stated above. It is a working analysis for professional review, not a final "
                      "legal or economic conclusion.")

    p = doc.add_paragraph(); r = p.add_run("1. Group structure"); r.bold = True
    _doc_table(doc, ["Entity", "Country", "Type", "This client?"],
              [[e["name"], e.get("country") or "", e.get("entity_type") or "", "Yes" if e["is_the_client"] else "No"]
               for e in res["entities"]], [4.5, 2.5, 3.5, 2.5])
    _doc_table(doc, ["Entity A", "Entity B", "Relationship", "Ownership %", "Tested under"],
              [[rl["entity_a_name"], rl["entity_b_name"], rl.get("relationship_type") or "",
                rl.get("ownership_pct"), rl.get("country_test_basis") or ""] for rl in res["relationships"]],
              [3.5, 3.5, 2.5, 2, 2.5])

    p = doc.add_paragraph(); r = p.add_run("2. Transactions, FAR, method and benchmarking"); r.bold = True
    for t in res["transactions"]:
        far = res["far_by_txn"].get(t["id"]) or {}
        method = res["method_by_txn"].get(t["id"]) or {}
        bench = res["bench_by_txn"].get(t["id"]) or {}
        stats = bench.get("stats") or {}
        risk = res["risk_by_txn"].get(t["id"]) or {}
        adj = res["adj_by_txn"].get(t["id"])

        pp = doc.add_paragraph(); rr = pp.add_run(
            f"{t['transaction_category']} — {t.get('tested_entity_name') or 'tested entity n/a'} "
            f"↔ {t.get('counterparty_entity_name') or 'counterparty n/a'}"); rr.bold = True
        doc.add_paragraph(f"FAR: functional profile {far.get('functional_profile') or 'not yet assessed'}. "
                          f"Conclusion: {far.get('conclusion') or 'pending'}.")
        doc.add_paragraph(f"Method: recommended {method.get('recommended_method') or 'not yet computed'}"
                          f"{' — selected ' + method['selected_method'] if method.get('selected_method') else ''}.")
        if stats.get("count"):
            doc.add_paragraph(f"Benchmark: n={stats['count']}, median {stats.get('median')}, "
                              f"range {stats.get('min')}–{stats.get('max')}.")
        doc.add_paragraph(f"Risk: {risk.get('risk_level') or 'not assessed'}.")
        if adj:
            doc.add_paragraph(f"Adjustment exposure: actual {adj['actual_result']}, arm's length "
                              f"{adj['arm_length_result']}, total exposure {adj['total_exposure']}.")
        doc.add_paragraph()

    p = doc.add_paragraph(); r = p.add_run("3. Documentation status"); r.bold = True
    _doc_table(doc, ["Requirement", "Status", "Notes"],
              [[c["label"], c["status"], c.get("notes") or ""] for c in res["checklist"]], [5, 2, 5])

    _sign_block(doc)
    doc.save(path); return path


def xlsx_tp_adjustment_exposure(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Adjustment exposure"
    r = _stamp(ws, dict(meta, title="Transfer pricing — adjustment exposure report"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    rows, total = [], 0.0
    for t in res["transactions"]:
        a = res["adj_by_txn"].get(t["id"])
        if not a: continue
        total += a.get("total_exposure") or 0
        rows.append([t.get("tested_entity_name") or "", t["transaction_category"], a["actual_result"], a["arm_length_result"],
                    a["adjustment_amount"], a["taxable_income_impact"], a["tax_impact"], a["interest_exposure"],
                    a["penalty_exposure"], a["total_exposure"], a.get("basis_notes") or ""])
    r = _table(ws, r, ["Tested entity", "Category", "Actual", "Arm's length", "Adjustment", "Taxable income impact",
                       "Tax impact", "Interest", "Penalty", "Total exposure", "Notes"],
               rows, [20, 16, 10, 12, 14, 16, 12, 12, 12, 14, 30], money_cols=(5, 6, 7, 8, 9, 10))
    _total_row(ws, r + 1, "Total exposure", {10: round(total, 2)})
    wb.save(path); return path


def docx_tp_adjustment_exposure(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Adjustment Exposure Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    doc.add_paragraph("Potential transfer-pricing adjustment and associated tax, interest and penalty exposure by "
                      "transaction, computed from the actual result entered against the benchmarked arm's-length "
                      "point. Any component left blank below means the underlying rate wasn't supplied or "
                      "configured — never assume it is zero.")
    rows, total = [], 0.0
    for t in res["transactions"]:
        a = res["adj_by_txn"].get(t["id"])
        if not a: continue
        total += a.get("total_exposure") or 0
        rows.append([t.get("tested_entity_name") or "", t["transaction_category"], a["adjustment_amount"],
                    a["tax_impact"], a["interest_exposure"], a["penalty_exposure"], a["total_exposure"]])
    _doc_table(doc, ["Tested entity", "Category", "Adjustment", "Tax", "Interest", "Penalty", "Total"], rows,
              [3, 2, 2, 2, 2, 2, 2], bold_last=True)
    doc.add_paragraph(f"Total exposure across the transactions above: {total:,.2f}.")
    _sign_block(doc)
    doc.save(path); return path


def xlsx_tp_documentation_gap(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Documentation gaps"
    r = _stamp(ws, dict(meta, title="Transfer pricing — documentation gap report"))
    ws.cell(row=r, column=1, value=_tp_framework_line(meta, res)).font = SMALL
    r += 2
    gaps = [c for c in res["checklist"] if c["status"] != "complete"]
    _table(ws, r, ["Requirement", "Status", "Notes"], [[c["label"], c["status"], c.get("notes") or ""] for c in gaps], [40, 14, 40])
    wb.save(path); return path


def docx_tp_documentation_gap(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Documentation Gap Report"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    gaps = [c for c in res["checklist"] if c["status"] != "complete"]
    complete = len(res["checklist"]) - len(gaps)
    doc.add_paragraph(f"{complete} of {len(res['checklist'])} documentation requirements are complete. "
                      f"{len(gaps)} remain missing or in progress, listed below.")
    if gaps:
        _doc_table(doc, ["Requirement", "Status", "Notes"], [[c["label"], c["status"], c.get("notes") or ""] for c in gaps], [5, 2, 5])
    else:
        doc.add_paragraph("No outstanding documentation gaps against the checklist currently on file.")
    _sign_block(doc)
    doc.save(path); return path


def docx_tp_advisory_memo(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Management Advisory Memo"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    doc.add_paragraph("This memo summarises the transfer pricing position for management's attention: key "
                      "transactions, risk flags and open items requiring a decision or further documentation. It "
                      "is an internal advisory note, not a filed position.")

    high_risk = [t for t in res["transactions"] if (res["risk_by_txn"].get(t["id"]) or {}).get("risk_level") in ("High", "Very High")]
    p = doc.add_paragraph(); r = p.add_run("Transactions flagged High or Very High risk"); r.bold = True
    if high_risk:
        for t in high_risk:
            risk = res["risk_by_txn"][t["id"]]
            doc.add_paragraph(f"{t['transaction_category']} — {t.get('tested_entity_name') or ''} "
                              f"↔ {t.get('counterparty_entity_name') or ''}: {risk['risk_level']}. {risk.get('explanation') or ''}",
                              style="List Bullet")
    else:
        doc.add_paragraph("None currently flagged.")

    p = doc.add_paragraph(); r = p.add_run("Transactions without a selected method"); r.bold = True
    no_method = [t for t in res["transactions"] if not t.get("method_selected")]
    if no_method:
        for t in no_method:
            doc.add_paragraph(f"{t['transaction_category']} — {t.get('tested_entity_name') or ''}", style="List Bullet")
    else:
        doc.add_paragraph("None — every transaction has a selected method on file.")

    p = doc.add_paragraph(); r = p.add_run("Outstanding documentation"); r.bold = True
    gaps = [c for c in res["checklist"] if c["status"] != "complete"]
    if gaps:
        for c in gaps:
            doc.add_paragraph(c["label"] + f" ({c['status']})", style="List Bullet")
    else:
        doc.add_paragraph("No outstanding items against the checklist on file.")

    _sign_block(doc)
    doc.save(path); return path


def docx_tp_client_info_request(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Client Information Request"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    doc.add_paragraph("To progress the transfer pricing review for the year above, please provide or confirm the "
                      "following:")

    p = doc.add_paragraph(); r = p.add_run("Outstanding documentation"); r.bold = True
    gaps = [c for c in res["checklist"] if c["status"] != "complete"]
    if gaps:
        for c in gaps:
            doc.add_paragraph(c["label"], style="List Bullet")
    else:
        doc.add_paragraph("No outstanding checklist items on file — please confirm nothing further is needed.")

    p = doc.add_paragraph(); r = p.add_run("Transactions needing further facts"); r.bold = True
    incomplete = [t for t in res["transactions"] if not (res["far_by_txn"].get(t["id"]) or {}).get("functional_profile")]
    if incomplete:
        for t in incomplete:
            doc.add_paragraph(f"{t['transaction_category']} — {t.get('tested_entity_name') or ''} "
                              f"↔ {t.get('counterparty_entity_name') or ''}: functional profile / FAR facts not yet recorded.",
                              style="List Bullet")
    else:
        doc.add_paragraph("FAR facts are on file for every transaction currently in the register.")

    p = doc.add_paragraph(); r = p.add_run("General items"); r.bold = True
    for item in ("Trial balance / financial statements for the year", "Intercompany agreements for each transaction listed",
                 "Confirmation of any change in group structure or ownership during the year",
                 "Confirmation of any new related-party transaction not yet in the register"):
        doc.add_paragraph(item, style="List Bullet")

    _sign_block(doc)
    doc.save(path); return path


def docx_tp_working_paper_pack(res, meta, path):
    doc = _doc_base(dict(meta, title="Transfer Pricing — Audit Working Paper Pack"))
    doc.add_paragraph(_tp_framework_line(meta, res))
    wps = res.get("working_papers") or []
    if not wps:
        doc.add_paragraph("No working papers have been created for this client yet.")
    for wp in wps:
        p = doc.add_paragraph(); r = p.add_run(f"{wp['ref']} — {wp.get('title') or ''}"); r.bold = True
        doc.add_paragraph(f"Prepared by: {wp.get('prepared_by_name') or 'n/a'}   Reviewed by: {wp.get('reviewed_by_name') or 'not yet reviewed'}"
                          f"   Status: {wp.get('review_status') or 'open'}")
        if wp.get("content"):
            doc.add_paragraph(wp["content"])
        if wp.get("review_comment"):
            doc.add_paragraph(f"Reviewer comment: {wp['review_comment']}")
        doc.add_paragraph()
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------- practice management
# One shape (pm_proposal), three document flavours picked by result['type'].

def _pm_fee_line(res):
    parts = []
    if res.get("fee_amount"):
        parts.append(f"NPR {res['fee_amount']:,.2f}")
    if res.get("fee_structure"):
        parts.append(res["fee_structure"])
    return " — ".join(parts) if parts else "To be confirmed"


def docx_pm_proposal(res, meta, path):
    doc = _doc_base(dict(meta, title=res.get("title") or "Proposal for Professional Services"))
    doc.add_paragraph(f"Prepared for {res.get('client_name', '')}" + (f", {res['validity_date']}" if res.get("validity_date") else ""))
    p = doc.add_paragraph(); r = p.add_run("Scope of services"); r.bold = True
    doc.add_paragraph(res.get("scope_of_services") or "To be confirmed with the client.")
    p = doc.add_paragraph(); r = p.add_run("Fees"); r.bold = True
    doc.add_paragraph(_pm_fee_line(res))
    if res.get("validity_date"):
        doc.add_paragraph(f"This proposal is valid until {res['validity_date']}.")
    if res.get("notes"):
        p = doc.add_paragraph(); r = p.add_run("Additional notes"); r.bold = True
        doc.add_paragraph(res["notes"])
    _sign_block(doc)
    doc.save(path); return path


def docx_pm_fee_quote(res, meta, path):
    doc = _doc_base(dict(meta, title=res.get("title") or "Fee Quotation"))
    doc.add_paragraph(f"Quotation for {res.get('client_name', '')}" + (f", valid until {res['validity_date']}" if res.get("validity_date") else ""))
    p = doc.add_paragraph(); r = p.add_run("Services quoted"); r.bold = True
    doc.add_paragraph(res.get("scope_of_services") or "As discussed.")
    p = doc.add_paragraph(); r = p.add_run("Fee"); r.bold = True
    doc.add_paragraph(_pm_fee_line(res))
    doc.add_paragraph("This is a fee quotation, not a tax invoice — a formal invoice follows on acceptance and completion of the engagement letter.")
    if res.get("notes"):
        doc.add_paragraph(res["notes"])
    _sign_block(doc)
    doc.save(path); return path


def docx_pm_engagement_letter(res, meta, path):
    doc = _doc_base(dict(meta, title="Engagement Letter"))
    doc.add_paragraph(f"To: {res.get('client_name', '')}")
    doc.add_paragraph(f"Dear Sir/Madam,\n\nWe are pleased to confirm our appointment to provide the following services, and the terms on which we will provide them.")
    p = doc.add_paragraph(); r = p.add_run("Scope of the engagement"); r.bold = True
    doc.add_paragraph(res.get("scope_of_services") or "As agreed with the client.")
    p = doc.add_paragraph(); r = p.add_run("Fees"); r.bold = True
    doc.add_paragraph(_pm_fee_line(res))
    p = doc.add_paragraph(); r = p.add_run("Responsibilities"); r.bold = True
    doc.add_paragraph("Management remains responsible for the maintenance of proper accounting records, for the prevention and "
                      "detection of fraud and error, and for providing us with complete and accurate information relevant to "
                      "this engagement. Our responsibility is limited to the scope described above.")
    p = doc.add_paragraph(); r = p.add_run("Confidentiality and independence"); r.bold = True
    doc.add_paragraph("We will keep all information obtained in the course of this engagement confidential, except where "
                      "disclosure is required by law or professional standards. We confirm we are independent of the client "
                      "for the purposes of this engagement.")
    if res.get("notes"):
        p = doc.add_paragraph(); r = p.add_run("Other terms"); r.bold = True
        doc.add_paragraph(res["notes"])
    doc.add_paragraph("\nPlease sign and return a copy of this letter to confirm your agreement to these terms.")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------- forensic audit & investigation

def xlsx_fa_investigation_report(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Findings"
    r = _stamp(ws, dict(meta, title="Forensic audit — investigation report"))
    findings = res.get("findings") or []
    counts = {"High": 0, "Medium": 0, "Low": 0}
    for f in findings:
        counts[f.get("severity", "Low")] = counts.get(f.get("severity", "Low"), 0) + 1
    ws.cell(row=r, column=1, value=f"Findings: {len(findings)} total — {counts['High']} High, {counts['Medium']} Medium, {counts['Low']} Low").font = SMALL
    r += 2
    _table(ws, r, ["Detector", "Severity", "Title", "Description", "Dataset", "Status"],
           [[f["detector"], f["severity"], f["title"], f.get("description") or "", f.get("dataset_name") or "", f["status"]] for f in findings],
           [18, 10, 30, 40, 20, 12])
    wb.save(path); return path


def docx_fa_investigation_report(res, meta, path):
    doc = _doc_base(dict(meta, title="Forensic Audit — Investigation Report"))
    findings = res.get("findings") or []
    counts = {"High": 0, "Medium": 0, "Low": 0}
    for f in findings:
        counts[f.get("severity", "Low")] = counts.get(f.get("severity", "Low"), 0) + 1
    doc.add_paragraph(f"This report summarises {len(findings)} screening findings across {len(res.get('datasets') or [])} "
                      f"dataset(s) reviewed for {res.get('client', {}).get('name', '')}. Every finding below is an indicative "
                      "screening result requiring professional judgement and follow-up — none of these, individually or "
                      "together, constitute a conclusion that fraud has occurred.")
    doc.add_paragraph(f"Severity mix: {counts['High']} High, {counts['Medium']} Medium, {counts['Low']} Low.")
    by_detector = {}
    for f in findings:
        by_detector.setdefault(f["detector"], []).append(f)
    for detector, items in by_detector.items():
        p = doc.add_paragraph(); r = p.add_run(detector.replace("_", " ").title() + f" ({len(items)})"); r.bold = True
        _doc_table(doc, ["Severity", "Title", "Description", "Status"],
                  [[it["severity"], it["title"], it.get("description") or "", it["status"]] for it in items[:25]], [1.5, 3, 5, 1.5])
        if len(items) > 25:
            doc.add_paragraph(f"…and {len(items) - 25} more of this type — see the full findings register in the application.")
    _sign_block(doc)
    doc.save(path); return path


def docx_fa_working_paper_pack(res, meta, path):
    doc = _doc_base(dict(meta, title="Forensic Audit — Working Paper Pack"))
    wps = res.get("working_papers") or []
    if not wps:
        doc.add_paragraph("No working papers have been created for this client yet.")
    for wp in wps:
        p = doc.add_paragraph(); r = p.add_run(f"{wp['ref']} — {wp.get('title') or ''}"); r.bold = True
        doc.add_paragraph(f"Prepared by: {wp.get('prepared_by_name') or 'n/a'}   Reviewed by: {wp.get('reviewed_by_name') or 'not yet reviewed'}"
                          f"   Status: {wp.get('review_status') or 'open'}")
        if wp.get("content"):
            doc.add_paragraph(wp["content"])
        if wp.get("review_comment"):
            doc.add_paragraph(f"Reviewer comment: {wp['review_comment']}")
        doc.add_paragraph()
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------- corporate advisory

_CA_RESOLUTION_TYPE_LABEL = {"board": "Board Resolution", "ordinary": "Shareholder Resolution (Ordinary)",
                            "special": "Shareholder Resolution (Special)"}


def docx_ca_resolution(res, meta, path):
    r = res.get("resolution") or {}
    client = res.get("client") or {}
    title = _CA_RESOLUTION_TYPE_LABEL.get(r.get("resolution_type"), "Resolution")
    doc = _doc_base(dict(meta, title=title))
    doc.add_paragraph(f"{client.get('name', '')}")
    doc.add_paragraph(f"A {title.lower()} of the company" + (f", passed on {r['resolution_date']}" if r.get("resolution_date") else "") + ".")
    if r.get("category"):
        p = doc.add_paragraph(); pr = p.add_run("Subject: "); pr.bold = True
        p.add_run(r["category"])
    p = doc.add_paragraph(); pr = p.add_run("RESOLVED THAT"); pr.bold = True
    doc.add_paragraph(r.get("resolution_text") or (r.get("title") or ""))
    doc.add_paragraph()
    status_line = f"Status: {(r.get('status') or 'draft').title()}"
    if r.get("filed_with"):
        status_line += f"   Filed with: {r['filed_with']}"
    if r.get("reference_no"):
        status_line += f"   Reference: {r['reference_no']}"
    doc.add_paragraph(status_line)
    _sign_block(doc)
    doc.save(path); return path


def xlsx_corporate_advisory(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Summary"
    r = _stamp(ws, meta)
    profile = res.get("profile") or {}
    prof_rows = [["Registration no. (OCR)", profile.get("registration_no", "")],
                ["Incorporation date", profile.get("incorporation_date", "")],
                ["Entity class", profile.get("entity_class", "")],
                ["Authorized capital", profile.get("authorized_capital", 0)],
                ["Issued capital", profile.get("issued_capital", 0)],
                ["Paid-up capital", profile.get("paid_up_capital", 0)],
                ["Auditor", profile.get("auditor_name", "")]]
    r = _table(ws, r, ["Particulars", "Detail"], prof_rows, [30, 30]) + 2

    sh = res.get("shareholding") or {}
    ws.cell(row=r, column=1, value="Shareholding structure").font = LBL; r += 1
    r = _table(ws, r, ["Metric", "Value"], [
        ["Total shares", sh.get("total_shares", 0)], ["Holders", sh.get("holder_count", 0)],
        ["Top 5 concentration %", sh.get("top5_concentration_pct", 0)],
        ["Related-party holding %", sh.get("related_party_pct", 0)],
        ["Director holding %", sh.get("director_holding_pct", 0)],
    ], [30, 20]) + 2
    holder_rows = [[h["name"], h["share_class"], h["shares_held"], h["ownership_pct"],
                    "Yes" if h["is_related_party"] else "No", "Yes" if h["is_director"] else "No"]
                   for h in sh.get("holders") or []]
    r = _table(ws, r, ["Shareholder", "Class", "Shares held", "Ownership %", "Related party", "Director"],
              holder_rows, [26, 12, 16, 14, 14, 12], money_cols=(3,)) + 2

    bc = res.get("board_compliance") or {}
    ws.cell(row=r, column=1, value="Board compliance").font = LBL; r += 1
    r = _table(ws, r, ["Metric", "Value"], [
        ["Meetings recorded", bc.get("meetings_count", 0)], ["Board meetings", bc.get("board_meetings_count", 0)],
        ["Minutes filed %", bc.get("minutes_filed_pct", 0)], ["Gaps exceeding maximum", bc.get("gap_breaches_count", 0)],
    ], [30, 20]) + 2

    stc = res.get("share_transaction_compliance") or {}
    ws.cell(row=r, column=1, value=f"Capital events — {stc.get('complete_count', 0)} of {stc.get('total', 0)} complete "
                                    f"({stc.get('complete_pct', 0)}%)").font = LBL
    r += 2

    ws2 = wb.create_sheet("Meetings")
    r2 = _stamp(ws2, dict(meta, title="Board / general meetings"))
    m_rows = [[mt.get("meeting_type", ""), mt.get("meeting_date", ""), mt.get("quorum_present"),
              "Yes" if mt.get("minutes_recorded") else "No", mt.get("status", "")] for mt in res.get("meetings") or []]
    _table(ws2, r2, ["Type", "Date", "Quorum present", "Minutes recorded", "Status"], m_rows, [14, 14, 16, 18, 14])

    ws3 = wb.create_sheet("Resolutions")
    r3 = _stamp(ws3, dict(meta, title="Resolutions"))
    res_rows = [[rr.get("resolution_type", ""), rr.get("category", ""), rr.get("title", ""),
                rr.get("resolution_date", ""), rr.get("status", "")] for rr in res.get("resolutions") or []]
    _table(ws3, r3, ["Type", "Category", "Title", "Date", "Status"], res_rows, [12, 20, 30, 14, 12])

    ws4 = wb.create_sheet("Checklist")
    r4 = _stamp(ws4, dict(meta, title="Company law compliance checklist"))
    c_rows = [[c.get("area", ""), c.get("criterion", ""), c.get("status", ""), c.get("due_date", ""), c.get("remark", "")]
             for c in res.get("checklist") or []]
    _table(ws4, r4, ["Area", "Criterion", "Status", "Due date", "Remark"], c_rows, [18, 40, 12, 14, 26])
    wb.save(path); return path


def docx_corporate_advisory(res, meta, path):
    doc = _doc_base(dict(meta, title="Corporate Advisory Compliance Report"))
    client = res.get("client") or {}
    profile = res.get("profile") or {}
    doc.add_paragraph(f"Corporate compliance summary for {client.get('name') or 'the entity'}"
                      + (f" (OCR registration {profile['registration_no']})" if profile.get("registration_no") else "") + ".")

    sh = res.get("shareholding") or {}
    p = doc.add_paragraph(); r = p.add_run("Shareholding structure"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Total shares", sh.get("total_shares", 0)], ["Holders", sh.get("holder_count", 0)],
        ["Top 5 concentration %", sh.get("top5_concentration_pct", 0)],
        ["Related-party holding %", sh.get("related_party_pct", 0)],
    ], [7.5, 5.5])

    bc = res.get("board_compliance") or {}
    p = doc.add_paragraph(); r = p.add_run("Board compliance"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Board meetings recorded", bc.get("board_meetings_count", 0)],
        ["Minutes filed %", bc.get("minutes_filed_pct", 0)],
        ["Gaps exceeding the maximum assumption", bc.get("gap_breaches_count", 0)],
    ], [7.5, 5.5])
    if bc.get("gap_breaches_count"):
        doc.add_paragraph("One or more gaps between board meetings exceed the maximum assumption on file — see the working file for dates.")

    stc = res.get("share_transaction_compliance") or {}
    p = doc.add_paragraph(); r = p.add_run("Capital events — approval / filing completeness"); r.bold = True
    doc.add_paragraph(f"{stc.get('complete_count', 0)} of {stc.get('total', 0)} capital events ({stc.get('complete_pct', 0)}%) "
                      "have every approval and filing date the event type normally needs.")
    incomplete = [row for row in stc.get("rows") or [] if not row["complete"]]
    if incomplete:
        _doc_table(doc, ["Event type", "Status", "Missing"],
                  [[row["event_type"], row["status"], ", ".join(row["missing"])] for row in incomplete], [4.0, 3.0, 6.0])

    checklist = res.get("checklist") or []
    if checklist:
        p = doc.add_paragraph(); r = p.add_run("Company law compliance checklist"); r.bold = True
        by_area = {}
        for c in checklist:
            by_area.setdefault(c["area"], {"total": 0, "done": 0})
            by_area[c["area"]]["total"] += 1
            if c["status"] == "done":
                by_area[c["area"]]["done"] += 1
        _doc_table(doc, ["Area", "Done", "Total"], [[a, v["done"], v["total"]] for a, v in by_area.items()], [6.0, 3.0, 3.0])

    resolutions = res.get("resolutions") or []
    if resolutions:
        p = doc.add_paragraph(); r = p.add_run("Resolutions on file"); r.bold = True
        _doc_table(doc, ["Type", "Category", "Date", "Status"],
                  [[_CA_RESOLUTION_TYPE_LABEL.get(rr.get("resolution_type"), rr.get("resolution_type")),
                   rr.get("category", ""), rr.get("resolution_date", ""), rr.get("status", "")] for rr in resolutions],
                  [3.5, 4.0, 3.0, 3.0])

    doc.add_paragraph("Full shareholder register, meeting log and checklist detail are in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


# -------------------------------------------------------------- business advisory

_BA_PLAN_SECTION_TITLES = {
    "executive_summary": "Executive Summary", "company_description": "Company Description",
    "market_analysis": "Market Analysis", "business_model": "Business Model",
    "marketing_sales": "Marketing & Sales", "operations": "Operations",
    "management_team": "Management Team", "financial_plan": "Financial Plan",
    "risk_analysis": "Risk Analysis", "appendix": "Appendix",
}
_BA_PLAN_SECTION_ORDER = ["executive_summary", "company_description", "market_analysis", "business_model",
                         "marketing_sales", "operations", "management_team", "financial_plan",
                         "risk_analysis", "appendix"]


def xlsx_ba_management_dashboard(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Profitability"
    r = _stamp(ws, dict(meta, title="Management Dashboard — Profitability"))
    prof = res.get("profitability") or {}
    cs = prof.get("cost_structure") or {}
    cs_rows = [["Total revenue", cs.get("total_revenue", 0)], ["Total direct cost", cs.get("total_direct_cost", 0)],
              ["Direct cost % of revenue", cs.get("direct_cost_pct_of_revenue", 0)],
              ["Total overhead", cs.get("total_overhead", 0)], ["Overhead % of revenue", cs.get("overhead_pct_of_revenue", 0)],
              ["Total gross margin", cs.get("total_gross_margin", 0)], ["Gross margin %", cs.get("total_gross_margin_pct", 0)],
              ["Total net margin", cs.get("total_net_margin", 0)], ["Net margin %", cs.get("total_net_margin_pct", 0)]]
    r = _table(ws, r, ["Cost structure", "Value"], cs_rows, [30, 20]) + 2

    rows = [[x["dimension_type"], x["dimension_name"], x["revenue"], x["direct_cost"], x["allocated_overhead"],
            x["gross_margin"], x["gross_margin_pct"], x["net_margin"], x["net_margin_pct"], x["revenue_share_pct"]]
           for x in prof.get("rows") or []]
    _table(ws, r, ["Dimension", "Name", "Revenue", "Direct cost", "Overhead", "Gross margin", "Gross margin %",
                   "Net margin", "Net margin %", "Revenue share %"], rows,
          [12, 20, 14, 14, 14, 14, 12, 14, 12, 12], money_cols=(3, 4, 5, 6, 8))

    ws2 = wb.create_sheet("Budget vs actual")
    r2 = _stamp(ws2, dict(meta, title="Budget vs actual"))
    bv = res.get("budget_variance") or {}
    s = bv.get("summary") or {}
    r2 = _table(ws2, r2, ["Particulars", "Value"], [
        ["Budgeted revenue", s.get("budgeted_revenue", 0)], ["Actual revenue", s.get("actual_revenue", 0)],
        ["Budgeted expense", s.get("budgeted_expense", 0)], ["Actual expense", s.get("actual_expense", 0)],
        ["Budgeted profit", s.get("budgeted_profit", 0)], ["Actual profit", s.get("actual_profit", 0)],
    ], [30, 20], money_cols=(2,)) + 2
    bv_rows = [[x["line_type"], x["category"], x["period_label"], x["budgeted_amount"], x["actual_amount"],
               x["variance"], x["variance_pct"], "Yes" if x["favorable"] else ("No" if x["favorable"] is False else "")]
              for x in bv.get("rows") or []]
    _table(ws2, r2, ["Type", "Category", "Period", "Budgeted", "Actual", "Variance", "Variance %", "Favorable"],
          bv_rows, [10, 20, 14, 14, 14, 14, 12, 12], money_cols=(4, 5, 6))

    ws3 = wb.create_sheet("KPIs")
    r3 = _stamp(ws3, dict(meta, title="KPI dashboard"))
    kd = res.get("kpi_dashboard") or {}
    k_rows = [[k["name"], k["category"], k["unit"], k["latest_period"], k["latest_value"], k["previous_value"],
              k["trend"], k["target_value"], k["variance_vs_target"], "Yes" if k["on_target"] else ("No" if k["on_target"] is False else "")]
             for k in kd.get("kpis") or []]
    _table(ws3, r3, ["KPI", "Category", "Unit", "Latest period", "Latest value", "Previous value", "Trend",
                    "Target", "Variance vs target", "On target"], k_rows, [22, 14, 8, 14, 12, 12, 8, 10, 14, 10])
    wb.save(path); return path


def docx_ba_management_dashboard(res, meta, path):
    doc = _doc_base(dict(meta, title="Management Dashboard"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Management dashboard for {client.get('name') or 'the entity'}.")

    prof = res.get("profitability") or {}
    cs = prof.get("cost_structure") or {}
    p = doc.add_paragraph(); r = p.add_run("Profitability & cost structure"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Total revenue", cs.get("total_revenue", 0)], ["Direct cost % of revenue", cs.get("direct_cost_pct_of_revenue", 0)],
        ["Overhead % of revenue", cs.get("overhead_pct_of_revenue", 0)],
        ["Gross margin %", cs.get("total_gross_margin_pct", 0)], ["Net margin %", cs.get("total_net_margin_pct", 0)],
    ], [7.5, 5.5])
    if prof.get("best_performer"):
        doc.add_paragraph(f"Best performer: {prof['best_performer']['dimension_name']} "
                          f"({prof['best_performer']['dimension_type']}) — net margin {prof['best_performer']['net_margin_pct']}%.")
    if prof.get("loss_makers"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Loss-making lines"); r2.bold = True
        _doc_table(doc, ["Dimension", "Name", "Net margin"],
                  [[x["dimension_type"], x["dimension_name"], x["net_margin"]] for x in prof["loss_makers"]], [4.0, 5.0, 4.0])

    bv = res.get("budget_variance") or {}
    s = bv.get("summary") or {}
    p3 = doc.add_paragraph(); r3 = p3.add_run("Budget vs actual"); r3.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Budgeted profit", s.get("budgeted_profit", 0)], ["Actual profit", s.get("actual_profit", 0)],
        ["Unfavorable lines", bv.get("unfavorable_count", 0)],
    ], [7.5, 5.5])

    kd = res.get("kpi_dashboard") or {}
    if kd.get("kpis"):
        p4 = doc.add_paragraph(); r4 = p4.add_run("KPI dashboard"); r4.bold = True
        _doc_table(doc, ["KPI", "Latest", "Trend", "Target", "On target"],
                  [[k["name"], k["latest_value"], k["trend"] or "", k["target_value"],
                   "Yes" if k["on_target"] else ("No" if k["on_target"] is False else "")] for k in kd["kpis"]],
                  [4.5, 2.5, 2.0, 2.5, 2.5])
        if kd.get("off_target_count"):
            doc.add_paragraph(f"{kd['off_target_count']} KPI(s) are currently off target.")

    doc.add_paragraph("Full profitability, budget and KPI detail is in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


def docx_ba_business_plan(res, meta, path):
    doc = _doc_base(dict(meta, title="Business Plan"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Business plan — {client.get('name') or 'the entity'}.")

    sections = {s["section_key"]: s.get("content", "") for s in (res.get("plan_sections") or [])}
    bm = res.get("business_model") or {}
    for key in _BA_PLAN_SECTION_ORDER:
        title = _BA_PLAN_SECTION_TITLES[key]
        p = doc.add_paragraph(); r = p.add_run(title); r.bold = True; r.font.size = Pt(12)
        if key == "business_model" and any(bm.values()):
            for label, bkey in (("Value proposition", "value_proposition"), ("Customer segments", "customer_segments"),
                                ("Channels", "channels"), ("Customer relationships", "customer_relationships"),
                                ("Revenue streams", "revenue_streams"), ("Key resources", "key_resources"),
                                ("Key activities", "key_activities"), ("Key partners", "key_partners"),
                                ("Cost structure", "cost_structure_notes")):
                if bm.get(bkey):
                    pp = doc.add_paragraph(); rr = pp.add_run(label + ": "); rr.bold = True
                    pp.add_run(str(bm[bkey]))
        if key == "financial_plan":
            prof = res.get("profitability") or {}
            cs = prof.get("cost_structure") or {}
            if cs.get("total_revenue"):
                _doc_table(doc, ["Particulars", "Value"], [
                    ["Total revenue", cs.get("total_revenue", 0)], ["Gross margin %", cs.get("total_gross_margin_pct", 0)],
                    ["Net margin %", cs.get("total_net_margin_pct", 0)],
                ], [7.5, 5.5])
        doc.add_paragraph(sections.get(key) or "(Not yet drafted.)")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------ ipo / capital market

def xlsx_ipo_advisory(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Readiness"
    r = _stamp(ws, dict(meta, title="IPO Consultation Report — Readiness & Eligibility"))
    profile = res.get("profile") or {}
    prof_rows = [["Target listing date", profile.get("target_listing_date", "")],
                ["Face value", profile.get("face_value", 0)], ["Proposed issue size", profile.get("proposed_issue_size", 0)],
                ["Proposed price per share", profile.get("proposed_price_per_share", 0)],
                ["Pricing method", profile.get("pricing_method", "")], ["Lead manager", profile.get("lead_manager", "")]]
    r = _table(ws, r, ["Particulars", "Detail"], prof_rows, [30, 30]) + 2

    readiness = res.get("readiness") or {}
    area_rows = [[area, v["done"], v["total"], v["completion_pct"]] for area, v in (readiness.get("areas") or {}).items()]
    ws.cell(row=r, column=1, value="Checklist completion by area").font = LBL; r += 1
    r = _table(ws, r, ["Area", "Done", "Total", "Completion %"], area_rows, [20, 12, 12, 14]) + 2

    elig_rows = [[e["criterion"], e.get("threshold_value") or "", e.get("actual_value") or "",
                 "Met" if e["met"] else ("Not met" if e["met"] is False else "Unconfirmed")]
                for e in readiness.get("eligibility") or []]
    if elig_rows:
        ws.cell(row=r, column=1, value="Eligibility criteria").font = LBL; r += 1
        r = _table(ws, r, ["Criterion", "Threshold", "Actual", "Status"], elig_rows, [30, 16, 16, 14]) + 2

    ws2 = wb.create_sheet("Restatement")
    r2 = _stamp(ws2, dict(meta, title="Restatement analysis"))
    ra = res.get("restatement_analysis") or {}
    ra_rows = [[x["fy_label"], x["account"], x["previously_reported"], x["restated"], x["variance"],
               x.get("variance_pct"), x.get("reason", "")] for x in ra.get("rows") or []]
    _table(ws2, r2, ["FY", "Account", "Previously reported", "Restated", "Variance", "Variance %", "Reason"],
          ra_rows, [12, 20, 18, 16, 14, 12, 30], money_cols=(3, 4, 5))

    ws3 = wb.create_sheet("Working capital")
    r3 = _stamp(ws3, dict(meta, title="Working capital adequacy"))
    wc = res.get("working_capital_adequacy") or {}
    wc_rows = [[x["period_label"], x["projected_requirement"], x["available_from_operations"], x["ipo_proceeds_allocated"],
               x["total_available"], x["gap"], "Yes" if x["adequate"] else "No"] for x in wc.get("rows") or []]
    _table(ws3, r3, ["Period", "Requirement", "From operations", "IPO proceeds", "Total available", "Gap", "Adequate"],
          wc_rows, [14, 16, 16, 14, 16, 14, 12], money_cols=(2, 3, 4, 5, 6))

    ws4 = wb.create_sheet("Peer comparison")
    r4 = _stamp(ws4, dict(meta, title="Peer comparison"))
    pc = res.get("peer_comparison") or {}
    peer_rows = [[p["peer_name"], "Listed" if p["listed"] else "Unlisted", p["market_price"], p["eps"],
                 p["book_value_per_share"], p.get("pe_ratio"), p.get("pbv_ratio")] for p in pc.get("peers") or []]
    r4 = _table(ws4, r4, ["Peer", "Status", "Market price", "EPS", "Book value/share", "P/E", "P/BV"],
               peer_rows, [22, 10, 14, 12, 16, 10, 10], money_cols=(3, 4, 5)) + 2
    ws4.cell(row=r4, column=1, value=f"Average P/E: {pc.get('avg_pe')}  ·  Average P/BV: {pc.get('avg_pbv')}").font = TXT

    ws5 = wb.create_sheet("Risk factors")
    r5 = _stamp(ws5, dict(meta, title="Risk factors"))
    risk_rows = [[x.get("severity", ""), x.get("category", ""), x.get("description", ""), x.get("mitigation", "")]
                for x in res.get("risks") or []]
    _table(ws5, r5, ["Severity", "Category", "Description", "Mitigation"], risk_rows, [10, 16, 40, 40])
    wb.save(path); return path


def docx_ipo_advisory(res, meta, path):
    doc = _doc_base(dict(meta, title="IPO Consultation Report"))
    client = res.get("client") or {}
    profile = res.get("profile") or {}
    doc.add_paragraph(f"IPO consultation report for {client.get('name') or 'the entity'}"
                      + (f" — targeting listing on {profile['target_listing_date']}" if profile.get("target_listing_date") else "") + ".")

    readiness = res.get("readiness") or {}
    p = doc.add_paragraph(); r = p.add_run("Readiness & compliance"); r.bold = True
    _doc_table(doc, ["Area", "Done", "Total", "Completion %"],
              [[area, v["done"], v["total"], v["completion_pct"]] for area, v in (readiness.get("areas") or {}).items()],
              [4.5, 2.5, 2.5, 3.5])
    eligibility = readiness.get("eligibility") or []
    not_met = [e for e in eligibility if e["met"] is False]
    unconfirmed = [e for e in eligibility if e["met"] is None]
    if not_met:
        doc.add_paragraph(f"{len(not_met)} eligibility criterion/criteria not currently met: " +
                          ", ".join(e["criterion"] for e in not_met) + ".")
    if unconfirmed:
        doc.add_paragraph(f"{len(unconfirmed)} eligibility criterion/criteria have no confirmed threshold on file yet — "
                          "confirm the current SEBON requirement before relying on this assessment.")

    ra = res.get("restatement_analysis") or {}
    if ra.get("rows"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Restatement analysis"); r2.bold = True
        doc.add_paragraph(f"{ra['lines_restated']} line(s) restated; total absolute impact {ra['total_absolute_impact']:,.0f}.")

    wc = res.get("working_capital_adequacy") or {}
    if wc.get("rows"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Working capital adequacy"); r3.bold = True
        doc.add_paragraph(("The projected working capital requirement is adequately covered by internal accruals and "
                          "allocated IPO proceeds across all periods reviewed." if wc.get("overall_adequate") else
                          f"A shortfall is projected in {wc.get('shortfall_periods')} period(s); maximum cumulative gap "
                          f"{wc.get('max_cumulative_gap', 0):,.0f}."))

    pc = res.get("peer_comparison") or {}
    if pc.get("peers"):
        p4 = doc.add_paragraph(); r4 = p4.add_run("Peer comparison"); r4.bold = True
        _doc_table(doc, ["Peer", "Market price", "P/E", "P/BV"],
                  [[x["peer_name"], x["market_price"], x.get("pe_ratio"), x.get("pbv_ratio")] for x in pc["peers"]],
                  [5.0, 3.0, 2.5, 2.5])
        doc.add_paragraph(f"Average peer P/E: {pc.get('avg_pe')}  ·  Average peer P/BV: {pc.get('avg_pbv')}.")

    pricing = res.get("pricing_scenarios") or []
    if pricing:
        p5 = doc.add_paragraph(); r5 = p5.add_run("Pricing analysis"); r5.bold = True
        _doc_table(doc, ["Scenario", "Proposed price", "Premium to face value %", "Implied P/E"],
                  [[x["label"], x["proposed_price"], x["computed"].get("premium_to_face_value_pct"),
                   x["computed"].get("implied_pe")] for x in pricing], [4.5, 3.0, 4.0, 2.5])

    risks = res.get("risks") or []
    if risks:
        p6 = doc.add_paragraph(); r6 = p6.add_run("Risk factors"); r6.bold = True
        _doc_table(doc, ["Severity", "Category", "Description"],
                  [[x.get("severity", ""), x.get("category", ""), x.get("description", "")] for x in risks], [1.5, 2.5, 9.0])

    doc.add_paragraph("Financial due diligence, financial projections and the share valuation supporting this IPO are "
                      "in the Due Diligence, Projection and Valuation modules respectively — not reproduced here.")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------------- tax advisory

_TAX_CASE_STAGE_LABEL = {
    "notice_received": "Notice received", "response_filed": "Response filed", "assessment_order": "Assessment order",
    "administrative_review": "Administrative review", "revenue_tribunal": "Revenue Tribunal",
    "supreme_court": "Supreme Court", "closed": "Closed",
}


def docx_tax_case_response(res, meta, path):
    case = res.get("case") or {}
    client = res.get("client") or {}
    doc = _doc_base(dict(meta, title="Tax Notice Response"))
    doc.add_paragraph(f"To: {case.get('authority', '')}")
    doc.add_paragraph(f"Re: {case.get('subject', '')}"
                      + (f" — Reference {case['reference_no']}" if case.get("reference_no") else "")
                      + (f" — FY {case['fy_label']}" if case.get("fy_label") else "") + ".")
    doc.add_paragraph(f"On behalf of {client.get('name', '')}, we submit the following in response to the above.")

    issues = res.get("issues") or []
    if issues:
        p = doc.add_paragraph(); r = p.add_run("Issues addressed"); r.bold = True
        for i in issues:
            pp = doc.add_paragraph(); pr = pp.add_run(i.get("issue_description", "")); pr.bold = True
            if i.get("our_position"):
                doc.add_paragraph(i["our_position"])
    if case.get("response_text"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Response"); r2.bold = True
        doc.add_paragraph(case["response_text"])
    if case.get("amount_demanded"):
        doc.add_paragraph(f"Amount demanded: {case['amount_demanded']:,.0f}.")
    _sign_block(doc)
    doc.save(path); return path


def xlsx_tax_audit_pack(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Computation summary"
    r = _stamp(ws, dict(meta, title="Tax Audit Support Pack — Computation Summary"))
    s = res.get("summary") or {}
    s_rows = [["Entity type", s.get("entity_type", "")], ["Book profit", s.get("book_profit", 0)],
             ["Total adjustments", s.get("total_adjustments", 0)], ["Taxable income", s.get("taxable_income", 0)],
             ["Tax rate %", s.get("tax_rate_pct", 0)], ["Tax liability", s.get("tax_liability", 0)],
             ["Turnover", s.get("turnover", 0)]]
    r = _table(ws, r, ["Particulars", "Value"], s_rows, [30, 20], money_cols=(2,)) + 2

    mt = res.get("minimum_tax_check") or {}
    if mt.get("minimum_tax_pct") is not None:
        ws.cell(row=r, column=1, value="Minimum tax check").font = LBL; r += 1
        r = _table(ws, r, ["Particulars", "Value"], [
            ["Minimum tax %", mt.get("minimum_tax_pct")], ["Minimum tax amount", mt.get("minimum_tax_amount")],
            ["Shortfall", mt.get("shortfall")], ["Binding", "Yes" if mt.get("minimum_tax_binding") else "No"],
        ], [30, 20]) + 2

    ws2 = wb.create_sheet("Deferred tax")
    r2 = _stamp(ws2, dict(meta, title="Deferred tax provision"))
    dt = res.get("deferred_tax") or {}
    dt_rows = [[x["particular"], x["book_value"], x["tax_value"], x["temporary_difference"], x["deferred_tax"]]
              for x in dt.get("rows") or []]
    r2 = _table(ws2, r2, ["Particular", "Book value", "Tax value", "Temporary difference", "Deferred tax"],
               dt_rows, [26, 16, 16, 18, 16], money_cols=(2, 3, 4, 5)) + 2
    ws2.cell(row=r2, column=1, value=f"{dt.get('classification', '')}: {dt.get('net_deferred_tax', 0):,.0f}").font = LBL

    ws3 = wb.create_sheet("Deductibility")
    r3 = _stamp(ws3, dict(meta, title="Deductibility checklist"))
    d_rows = [[x.get("description", ""), x.get("category", ""), x.get("amount", 0), x.get("status", ""), x.get("reason", "")]
             for x in res.get("deductibility") or []]
    _table(ws3, r3, ["Description", "Category", "Amount", "Status", "Reason"], d_rows, [30, 20, 14, 14, 26], money_cols=(3,))

    ws4 = wb.create_sheet("TDS reconciliation")
    r4 = _stamp(ws4, dict(meta, title="TDS reconciliation"))
    tds = res.get("tds_reconciliation") or {}
    tds_rows = [[x["section"], x["payee"], x["base_amount"], x["tds_deducted"], x["tds_deposited"], x["variance"],
                "Yes" if x["return_filed"] else "No"] for x in tds.get("rows") or []]
    _table(ws4, r4, ["Section", "Payee", "Base amount", "Deducted", "Deposited", "Variance", "Return filed"],
          tds_rows, [12, 20, 16, 14, 14, 14, 12], money_cols=(3, 4, 5, 6))

    ws5 = wb.create_sheet("VAT reconciliation")
    r5 = _stamp(ws5, dict(meta, title="VAT reconciliation"))
    vs = res.get("vat_sales_reconciliation") or {}
    vp = res.get("vat_purchases_reconciliation") or {}
    r5 = _table(ws5, r5, ["Particulars", "Value"], [
        ["Total output VAT (books)", vs.get("total_output_vat", 0)], ["Undeclared output VAT", vs.get("undeclared_output_vat", 0)],
        ["Total eligible input VAT (books)", vp.get("total_eligible_input_vat", 0)],
        ["Ineligible input VAT", vp.get("ineligible_input_vat", 0)], ["Unmatched input VAT", vp.get("unmatched_input_vat", 0)],
    ], [30, 20], money_cols=(2,)) + 2
    wb.save(path); return path


def docx_tax_audit_pack(res, meta, path):
    doc = _doc_base(dict(meta, title="Tax Audit Support Pack"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Tax audit support pack for {client.get('name') or 'the entity'} — FY {res.get('fy_label', '')}.")

    s = res.get("summary") or {}
    p = doc.add_paragraph(); r = p.add_run("Book profit to taxable income"); r.bold = True
    _doc_table(doc, ["Particulars", "Value"], [
        ["Book profit", s.get("book_profit", 0)], ["Total adjustments", s.get("total_adjustments", 0)],
        ["Taxable income", s.get("taxable_income", 0)], ["Tax rate %", s.get("tax_rate_pct", 0)],
        ["Tax liability", s.get("tax_liability", 0)],
    ], [7.5, 5.5])

    mt = res.get("minimum_tax_check") or {}
    if mt.get("minimum_tax_pct") is not None:
        p2 = doc.add_paragraph(); r2 = p2.add_run("Minimum tax check"); r2.bold = True
        doc.add_paragraph(("Computed tax liability is at or above the minimum-tax floor." if not mt.get("minimum_tax_binding")
                          else f"Minimum tax applies — a shortfall of {mt.get('shortfall', 0):,.0f} against the computed liability."))

    dt = res.get("deferred_tax") or {}
    if dt.get("rows"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Deferred tax provision"); r3.bold = True
        doc.add_paragraph(f"{dt.get('classification', '')} of {dt.get('net_deferred_tax', 0):,.0f} "
                          f"on a net temporary difference of {dt.get('net_temporary_difference', 0):,.0f}.")

    tds = res.get("tds_reconciliation") or {}
    if tds.get("rows"):
        p4 = doc.add_paragraph(); r4 = p4.add_run("TDS reconciliation"); r4.bold = True
        doc.add_paragraph(f"{tds.get('short_deposited_count', 0)} line(s) short-deposited; "
                          f"{tds.get('not_filed_count', 0)} line(s) with no return filed yet.")

    vs = res.get("vat_sales_reconciliation") or {}
    vp = res.get("vat_purchases_reconciliation") or {}
    if vs.get("rows") or vp.get("rows"):
        p5 = doc.add_paragraph(); r5 = p5.add_run("VAT reconciliation"); r5.bold = True
        doc.add_paragraph(f"Undeclared output VAT: {vs.get('undeclared_output_vat', 0):,.0f}. "
                          f"Ineligible input VAT: {vp.get('ineligible_input_vat', 0):,.0f}. "
                          f"Unmatched (unconfirmed-supplier) input VAT: {vp.get('unmatched_input_vat', 0):,.0f}.")

    deductibility = res.get("deductibility") or []
    not_deductible = [d for d in deductibility if d.get("status") == "not_deductible"]
    if not_deductible:
        p6 = doc.add_paragraph(); r6 = p6.add_run("Items assessed as not deductible"); r6.bold = True
        _doc_table(doc, ["Description", "Category", "Amount"],
                  [[d.get("description", ""), d.get("category", ""), d.get("amount", 0)] for d in not_deductible], [6.0, 4.0, 3.0])

    doc.add_paragraph("Full detail for every schedule above is in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------- audit execution

def docx_audx_management_letter(res, meta, path):
    doc = _doc_base(dict(meta, title="Management Letter"))
    client = res.get("client") or {}
    doc.add_paragraph(f"To the Management of {client.get('name') or 'the entity'}")
    doc.add_paragraph(f"In connection with our audit for FY {res.get('fy_label', '')}, we set out below the matters "
                      "that came to our attention during the course of our work.")

    exceptions = res.get("exceptions") or []
    if exceptions:
        p = doc.add_paragraph(); r = p.add_run("Matters arising from substantive testing"); r.bold = True
        for e in exceptions:
            pp = doc.add_paragraph(); pr = pp.add_run(f"{e.get('area', '')} — {e.get('item_ref') or e.get('description') or ''}"); pr.bold = True
            book, tested = e.get("book_amount"), e.get("tested_amount")
            if tested is not None:
                doc.add_paragraph(f"Book amount {book:,.0f} vs tested amount {tested:,.0f}."
                                  + (f" {e['remark']}" if e.get("remark") else ""))
            elif e.get("remark"):
                doc.add_paragraph(e["remark"])
    else:
        doc.add_paragraph("No exceptions were noted from the substantive testing performed.")

    ar = res.get("analytical_review") or {}
    unexplained = [r for r in ar.get("rows") or [] if r.get("flagged") and not r.get("explanation")]
    if unexplained:
        p2 = doc.add_paragraph(); r2 = p2.add_run("Unexplained analytical review variances"); r2.bold = True
        _doc_table(doc, ["Metric", "Current", "Expected", "Variance %"],
                  [[x["metric"], x["current_period_value"], x["expected_value"], x["variance_pct"]] for x in unexplained], [4.5, 2.5, 2.5, 2.5])

    gc = res.get("going_concern_assessment") or {}
    gcs = res.get("going_concern_summary") or {}
    if gcs.get("present_count") or gc.get("conclusion"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Going concern"); r3.bold = True
        doc.add_paragraph(f"{gcs.get('present_count', 0)} of {gcs.get('total_count', 0)} indicator(s) reviewed were noted as present."
                          + (f" {gc['conclusion']}" if gc.get("conclusion") else ""))
        if gc.get("material_uncertainty_exists"):
            doc.add_paragraph("A material uncertainty related to going concern has been identified.")

    cs = res.get("checklist_summary") or {}
    incomplete = [a for a, v in cs.items() if v["completion_pct"] < 100]
    if incomplete:
        doc.add_paragraph(f"The following checklist area(s) are not yet fully complete: {', '.join(incomplete)}.")

    doc.add_paragraph("This letter is provided for management's information and does not modify our audit opinion.")
    _sign_block(doc)
    doc.save(path); return path


def xlsx_audx_execution_summary(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Test items"
    r = _stamp(ws, dict(meta, title="Audit Execution Summary — Substantive Testing"))
    tis = res.get("test_item_summary") or {}
    area_rows = [[area, v["count"], v["exceptions"], v["exception_rate_pct"], v["book_total"], v["tested_total"], v["variance_total"]]
                for area, v in (tis.get("areas") or {}).items()]
    r = _table(ws, r, ["Area", "Items", "Exceptions", "Exception rate %", "Book total", "Tested total", "Variance"],
              area_rows, [16, 10, 12, 14, 16, 16, 14], money_cols=(5, 6, 7)) + 2
    ws.cell(row=r, column=1, value=f"Total items: {tis.get('total_items', 0)}  ·  Total exceptions: {tis.get('total_exceptions', 0)}"
                                   f"  ·  Overall exception rate: {tis.get('overall_exception_rate_pct', 0)}%").font = LBL

    ws2 = wb.create_sheet("Exceptions")
    r2 = _stamp(ws2, dict(meta, title="Exceptions detail"))
    exc_rows = [[e.get("area", ""), e.get("test_type", ""), e.get("item_ref", ""), e.get("description", ""),
                e.get("book_amount", 0), e.get("tested_amount"), e.get("remark", "")] for e in res.get("exceptions") or []]
    _table(ws2, r2, ["Area", "Test type", "Item ref", "Description", "Book amount", "Tested amount", "Remark"],
          exc_rows, [14, 14, 14, 26, 14, 14, 26], money_cols=(5, 6))

    ws3 = wb.create_sheet("Analytical review")
    r3 = _stamp(ws3, dict(meta, title="Analytical review"))
    ar = res.get("analytical_review") or {}
    ar_rows = [[x["metric"], x["prior_period_value"], x["current_period_value"], x["expected_value"], x["variance_pct"],
               "Yes" if x["flagged"] else "No", x["explanation"]] for x in ar.get("rows") or []]
    _table(ws3, r3, ["Metric", "Prior period", "Current period", "Expected", "Variance %", "Flagged", "Explanation"],
          ar_rows, [22, 14, 14, 14, 12, 10, 30], money_cols=(2, 3, 4))

    ws4 = wb.create_sheet("Going concern")
    r4 = _stamp(ws4, dict(meta, title="Going concern"))
    gcs = res.get("going_concern_summary") or {}
    gc = res.get("going_concern_assessment") or {}
    gc_rows = [[cat, v["present"], v["total"]] for cat, v in (gcs.get("by_category") or {}).items()]
    r4 = _table(ws4, r4, ["Category", "Present", "Total"], gc_rows, [22, 12, 12]) + 2
    r4 = _table(ws4, r4, ["Particulars", "Value"], [
        ["Conclusion", gc.get("conclusion", "")], ["Material uncertainty exists", "Yes" if gc.get("material_uncertainty_exists") else "No"],
    ], [30, 30]) + 2

    ws5 = wb.create_sheet("Checklists")
    r5 = _stamp(ws5, dict(meta, title="Checklist completion"))
    cs = res.get("checklist_summary") or {}
    cs_rows = [[area, v["done"], v["total"], v["completion_pct"]] for area, v in cs.items()]
    _table(ws5, r5, ["Area", "Done", "Total", "Completion %"], cs_rows, [22, 12, 12, 14])
    wb.save(path); return path


def docx_audx_execution_summary(res, meta, path):
    doc = _doc_base(dict(meta, title="Audit Execution Summary"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Audit execution summary for {client.get('name') or 'the entity'} — FY {res.get('fy_label', '')}.")

    tis = res.get("test_item_summary") or {}
    p = doc.add_paragraph(); r = p.add_run("Substantive testing"); r.bold = True
    _doc_table(doc, ["Area", "Items", "Exceptions", "Exception rate %"],
              [[area, v["count"], v["exceptions"], v["exception_rate_pct"]] for area, v in (tis.get("areas") or {}).items()],
              [4.5, 2.5, 2.5, 3.5])

    ar = res.get("analytical_review") or {}
    if ar.get("rows"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Analytical review"); r2.bold = True
        doc.add_paragraph(f"{ar.get('flagged_count', 0)} of {len(ar['rows'])} procedure(s) flagged; "
                          f"{ar.get('unexplained_flagged_count', 0)} without an explanation on file.")

    gcs = res.get("going_concern_summary") or {}
    if gcs.get("total_count"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Going concern"); r3.bold = True
        doc.add_paragraph(f"{gcs.get('present_count', 0)} of {gcs.get('total_count', 0)} indicator(s) present.")

    cs = res.get("checklist_summary") or {}
    if cs:
        p4 = doc.add_paragraph(); r4 = p4.add_run("Checklist completion"); r4.bold = True
        _doc_table(doc, ["Area", "Done", "Total", "Completion %"], [[a, v["done"], v["total"], v["completion_pct"]] for a, v in cs.items()], [4.5, 2.5, 2.5, 3.5])

    doc.add_paragraph("Full item-by-item detail is in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


def docx_acct_adjustments_schedule(res, meta, path):
    doc = _doc_base(dict(meta, title="Financial Statement Adjustments & Schedules"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Supporting schedules for {client.get('name') or 'the entity'} — FY {res.get('fy_label', '')}.")

    prov = res.get("provisions") or {}
    if prov.get("rows"):
        p = doc.add_paragraph(); r = p.add_run("Accruals and provisions"); r.bold = True
        _doc_table(doc, ["Item", "Category", "Opening", "Charge for year", "Utilized", "Reversed", "Closing"],
                  [[x["item"], x["category"], x["opening_balance"], x["charge_for_year"], x["utilized"], x["reversed"], x["closing_balance"]]
                   for x in prov["rows"]], [3, 1.8, 1.8, 1.8, 1.5, 1.5, 1.8])

    prep = res.get("prepayments") or {}
    if prep.get("rows"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Prepayment amortisation"); r2.bold = True
        _doc_table(doc, ["Item", "Total amount", "Months", "Elapsed", "Amortised to date", "Balance c/f"],
                  [[x["item"], x["total_amount"], x["total_months"], x["elapsed_months"], x["amortized_to_date"], x["balance_carried_forward"]]
                   for x in prep["rows"]], [3, 2, 1.3, 1.3, 2.2, 2.2])

    fx = res.get("fx") or {}
    if fx.get("rows"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Foreign currency revaluation (NAS 21)"); r3.bold = True
        _doc_table(doc, ["Item", "Currency", "Foreign amount", "Booked rate", "Closing rate", "Gain / (loss)"],
                  [[x["item"], x["currency"], x["foreign_amount"], x["booked_rate"], x["closing_rate"], x["gain_loss"]]
                   for x in fx["rows"]], [3, 1.5, 2, 1.8, 1.8, 2])
        doc.add_paragraph(f"Net exchange gain / (loss) for the year: {fx.get('total_gain_loss', 0):,.2f}")

    ia = res.get("interest") or {}
    if ia.get("rows"):
        p4 = doc.add_paragraph(); r4 = p4.add_run("Interest accrual"); r4.bold = True
        _doc_table(doc, ["Item", "Principal", "Rate %", "Days", "Accrued interest"],
                  [[x["item"], x["principal"], x["rate_pct"], x["days"], x["accrued_interest"]] for x in ia["rows"]],
                  [3.5, 2.2, 1.5, 1.5, 2.3])

    eq = res.get("equity") or {}
    if eq.get("components"):
        p5 = doc.add_paragraph(); r5 = p5.add_run("Statement of changes in equity"); r5.bold = True
        _doc_table(doc, ["Component", "Opening", "Profit for year", "OCI", "Dividend", "Other movement", "Closing"],
                  [[c["name"], c["opening_balance"], c["profit_for_year"], c["oci"], c["dividend"],
                    c["share_issued"] - c["share_buyback"] + c["transfer_in"] - c["transfer_out"] + c["other_movement"], c["closing_balance"]]
                   for c in eq["components"]], [2.8, 1.8, 1.8, 1.3, 1.5, 1.8, 1.8])

    cf = res.get("cash_flow") or {}
    if cf:
        p6 = doc.add_paragraph(); r6 = p6.add_run("Cash flow statement (indirect method)"); r6.bold = True
        doc.add_paragraph(f"Net cash from operating activities: {cf.get('net_operating', 0):,.2f}")
        doc.add_paragraph(f"Net cash from investing activities: {cf.get('net_investing', 0):,.2f}")
        doc.add_paragraph(f"Net cash from financing activities: {cf.get('net_financing', 0):,.2f}")
        doc.add_paragraph(f"Net change in cash: {cf.get('net_change', 0):,.2f}  ·  Opening cash: {cf.get('opening_cash', 0):,.2f}"
                          f"  ·  Computed closing cash: {cf.get('computed_closing_cash', 0):,.2f}")
        if "ties_out" in cf:
            doc.add_paragraph("Ties out to the actual closing cash balance." if cf["ties_out"]
                              else f"Does not tie out to the actual closing cash balance — difference {cf.get('reconciliation_diff', 0):,.2f}.")

    _sign_block(doc)
    doc.save(path); return path


def xlsx_acct_automation_workbook(res, meta, path):
    wb = Workbook(); ws = wb.active; ws.title = "Provisions"
    r = _stamp(ws, dict(meta, title="Accruals and provisions"))
    prov = res.get("provisions") or {}
    prov_rows = [[x["item"], x["category"], x["opening_balance"], x["charge_for_year"], x["utilized"], x["reversed"], x["closing_balance"]]
                for x in prov.get("rows") or []]
    _table(ws, r, ["Item", "Category", "Opening", "Charge for year", "Utilized", "Reversed", "Closing"],
          prov_rows, [22, 12, 14, 16, 12, 12, 14], money_cols=(3, 4, 5, 6, 7))

    ws2 = wb.create_sheet("Prepayments")
    r2 = _stamp(ws2, dict(meta, title="Prepayment amortisation"))
    prep = res.get("prepayments") or {}
    prep_rows = [[x["item"], x["total_amount"], x["total_months"], x["elapsed_months"], x["amortized_to_date"], x["balance_carried_forward"]]
                for x in prep.get("rows") or []]
    _table(ws2, r2, ["Item", "Total amount", "Months", "Elapsed", "Amortised to date", "Balance c/f"],
          prep_rows, [22, 16, 10, 10, 18, 16], money_cols=(2, 5, 6))

    ws3 = wb.create_sheet("FX revaluation")
    r3 = _stamp(ws3, dict(meta, title="Foreign currency revaluation"))
    fx = res.get("fx") or {}
    fx_rows = [[x["item"], x["currency"], x["foreign_amount"], x["booked_rate"], x["closing_rate"], x["gain_loss"]]
              for x in fx.get("rows") or []]
    r3 = _table(ws3, r3, ["Item", "Currency", "Foreign amount", "Booked rate", "Closing rate", "Gain / (loss)"],
               fx_rows, [22, 10, 16, 14, 14, 16], money_cols=(3, 4, 5, 6)) + 2
    ws3.cell(row=r3, column=1, value=f"Net gain / (loss): {fx.get('total_gain_loss', 0)}").font = LBL

    ws4 = wb.create_sheet("Interest accrual")
    r4 = _stamp(ws4, dict(meta, title="Interest accrual"))
    ia = res.get("interest") or {}
    ia_rows = [[x["item"], x["principal"], x["rate_pct"], x["days"], x["accrued_interest"]] for x in ia.get("rows") or []]
    _table(ws4, r4, ["Item", "Principal", "Rate %", "Days", "Accrued interest"], ia_rows, [22, 16, 10, 10, 16], money_cols=(2, 5))

    ws5 = wb.create_sheet("Equity")
    r5 = _stamp(ws5, dict(meta, title="Statement of changes in equity"))
    eq = res.get("equity") or {}
    eq_rows = [[c["name"], c["opening_balance"], c["profit_for_year"], c["oci"], c["dividend"], c["share_issued"],
               c["share_buyback"], c["transfer_in"], c["transfer_out"], c["other_movement"], c["closing_balance"]]
              for c in eq.get("components") or []]
    _table(ws5, r5, ["Component", "Opening", "Profit", "OCI", "Dividend", "Shares issued", "Buyback", "Transfer in",
                    "Transfer out", "Other", "Closing"], eq_rows, [20, 12, 12, 10, 10, 12, 10, 10, 10, 10, 12],
          money_cols=(2, 3, 4, 5, 6, 7, 8, 9, 10, 11))

    ws6 = wb.create_sheet("Cash flow")
    r6 = _stamp(ws6, dict(meta, title="Cash flow statement — indirect method"))
    cf = res.get("cash_flow") or {}
    cf_rows = [["Net cash from operating activities", cf.get("net_operating", 0)],
              ["Net cash from investing activities", cf.get("net_investing", 0)],
              ["Net cash from financing activities", cf.get("net_financing", 0)],
              ["Net change in cash", cf.get("net_change", 0)],
              ["Opening cash", cf.get("opening_cash", 0)],
              ["Computed closing cash", cf.get("computed_closing_cash", 0)]]
    if "closing_cash_actual" in cf:
        cf_rows.append(["Actual closing cash", cf.get("closing_cash_actual", 0)])
        cf_rows.append(["Reconciliation difference", cf.get("reconciliation_diff", 0)])
    _table(ws6, r6, ["Particulars", "Amount"], cf_rows, [32, 18], money_cols=(2,))
    wb.save(path); return path


def docx_acct_automation_workbook(res, meta, path):
    doc = _doc_base(dict(meta, title="Accounting Automation Summary"))
    client = res.get("client") or {}
    doc.add_paragraph(f"Accounting automation summary for {client.get('name') or 'the entity'} — FY {res.get('fy_label', '')}.")

    prov = res.get("provisions") or {}
    if prov.get("rows"):
        p = doc.add_paragraph(); r = p.add_run("Accruals and provisions"); r.bold = True
        doc.add_paragraph(f"{len(prov['rows'])} item(s); closing balance {prov.get('total_closing', 0):,.2f}.")

    prep = res.get("prepayments") or {}
    if prep.get("rows"):
        p2 = doc.add_paragraph(); r2 = p2.add_run("Prepayments"); r2.bold = True
        doc.add_paragraph(f"{len(prep['rows'])} item(s); balance carried forward {prep.get('total_balance_carried_forward', 0):,.2f}.")

    fx = res.get("fx") or {}
    if fx.get("rows"):
        p3 = doc.add_paragraph(); r3 = p3.add_run("Foreign currency revaluation"); r3.bold = True
        doc.add_paragraph(f"Net exchange gain / (loss): {fx.get('total_gain_loss', 0):,.2f}.")

    ia = res.get("interest") or {}
    if ia.get("rows"):
        p4 = doc.add_paragraph(); r4 = p4.add_run("Interest accrual"); r4.bold = True
        doc.add_paragraph(f"Total accrued interest: {ia.get('total_accrued_interest', 0):,.2f}.")

    cf = res.get("cash_flow") or {}
    if cf:
        p5 = doc.add_paragraph(); r5 = p5.add_run("Cash flow"); r5.bold = True
        doc.add_paragraph(f"Net change in cash: {cf.get('net_change', 0):,.2f}.")

    doc.add_paragraph("Full item-by-item detail is in the accompanying Excel workbook.")
    _sign_block(doc)
    doc.save(path); return path


# ------------------------------------------------------------------- registry

BUILDERS = {
    ("depreciation", "xlsx"): xlsx_depreciation, ("depreciation", "docx"): docx_depreciation,
    ("salary_tds", "xlsx"): xlsx_salary_tds,     ("salary_tds", "docx"): docx_salary_tds,
    ("biz_tax", "xlsx"): xlsx_biz_tax,           ("biz_tax", "docx"): docx_biz_tax,
    ("penalty", "xlsx"): xlsx_penalty,           ("penalty", "docx"): docx_penalty,
    ("emi", "xlsx"): xlsx_emi,                   ("emi", "docx"): docx_emi,
    ("lease", "xlsx"): xlsx_lease,               ("lease", "docx"): docx_lease,
    ("fin_statements", "xlsx"): xlsx_fin_statements, ("fin_statements", "docx"): docx_fin_statements,
    ("grant", "xlsx"): xlsx_grant,               ("grant", "docx"): docx_grant,
    ("projection", "xlsx"): xlsx_projection,     ("projection", "docx"): docx_projection,
    ("dd", "xlsx"): xlsx_dd,                     ("dd", "docx"): docx_dd,
    ("capital_advisory", "xlsx"): xlsx_capital_advisory, ("capital_advisory", "docx"): docx_capital_advisory,
    ("fdi", "xlsx"): xlsx_fdi,                   ("fdi", "docx"): docx_fdi,
    ("fdi", "docx_jva"): docx_fdi_jva, ("fdi", "docx_poa"): docx_fdi_poa,
    ("fdi", "docx_tta"): docx_fdi_tta, ("fdi", "docx_profiles"): docx_fdi_profiles,
    ("fdi", "docx_advisory"): docx_fdi_advisory,
    ("valuation", "xlsx"): xlsx_valuation,       ("valuation", "docx"): docx_valuation,
    ("inventory", "xlsx"): xlsx_inventory,       ("inventory", "docx"): docx_inventory,
    ("fixed_assets", "xlsx"): xlsx_fixed_assets, ("fixed_assets", "docx"): docx_fixed_assets,
    ("client_directory", "xlsx"): xlsx_client_directory,
    ("invoice", "docx"): docx_invoice,
    ("internal_audit", "docx"): docx_internal_audit, ("internal_audit", "xlsx"): xlsx_internal_audit,
    ("stat_audit", "docx"): docx_stat_audit, ("stat_audit", "xlsx"): xlsx_stat_audit,
    ("tds", "docx"): docx_tds, ("tds", "xlsx"): xlsx_tds,
    ("personal_tax", "docx"): docx_personal_tax, ("personal_tax", "xlsx"): xlsx_personal_tax,
    ("audit_report", "docx"): docx_audit_report, ("audit_report", "xlsx"): xlsx_audit_report,
    ("vat", "docx"): docx_vat, ("vat", "xlsx"): xlsx_vat,
    ("transfer_pricing", "xlsx_related_party"): xlsx_tp_related_party,
    ("transfer_pricing", "docx_related_party"): docx_tp_related_party,
    ("transfer_pricing", "xlsx_far_method_benchmark"): xlsx_tp_far_method_benchmark,
    ("transfer_pricing", "docx_far_method_benchmark"): docx_tp_far_method_benchmark,
    ("transfer_pricing", "xlsx_risk_assessment"): xlsx_tp_risk_assessment,
    ("transfer_pricing", "docx_risk_assessment"): docx_tp_risk_assessment,
    ("transfer_pricing", "xlsx_tp_study"): xlsx_tp_study,
    ("transfer_pricing", "docx_tp_study"): docx_tp_study,
    ("transfer_pricing", "xlsx_adjustment_exposure"): xlsx_tp_adjustment_exposure,
    ("transfer_pricing", "docx_adjustment_exposure"): docx_tp_adjustment_exposure,
    ("transfer_pricing", "xlsx_documentation_gap"): xlsx_tp_documentation_gap,
    ("transfer_pricing", "docx_documentation_gap"): docx_tp_documentation_gap,
    ("transfer_pricing", "docx_advisory_memo"): docx_tp_advisory_memo,
    ("transfer_pricing", "docx_client_info_request"): docx_tp_client_info_request,
    ("transfer_pricing", "docx_working_paper_pack"): docx_tp_working_paper_pack,
    ("practice_mgmt", "docx_proposal"): docx_pm_proposal,
    ("practice_mgmt", "docx_fee_quote"): docx_pm_fee_quote,
    ("practice_mgmt", "docx_engagement_letter"): docx_pm_engagement_letter,
    ("forensic_audit", "xlsx_investigation_report"): xlsx_fa_investigation_report,
    ("forensic_audit", "docx_investigation_report"): docx_fa_investigation_report,
    ("forensic_audit", "docx_working_paper_pack"): docx_fa_working_paper_pack,
    ("corporate_advisory", "xlsx"): xlsx_corporate_advisory, ("corporate_advisory", "docx"): docx_corporate_advisory,
    ("corporate_advisory", "docx_resolution"): docx_ca_resolution,
    ("business_advisory", "xlsx_management_dashboard"): xlsx_ba_management_dashboard,
    ("business_advisory", "docx_management_dashboard"): docx_ba_management_dashboard,
    ("business_advisory", "docx_business_plan"): docx_ba_business_plan,
    ("ipo_advisory", "xlsx"): xlsx_ipo_advisory, ("ipo_advisory", "docx"): docx_ipo_advisory,
    ("tax_advisory", "docx_case_response"): docx_tax_case_response,
    ("tax_advisory", "xlsx_audit_pack"): xlsx_tax_audit_pack, ("tax_advisory", "docx_audit_pack"): docx_tax_audit_pack,
    ("audit_execution", "docx_management_letter"): docx_audx_management_letter,
    ("audit_execution", "xlsx_execution_summary"): xlsx_audx_execution_summary,
    ("audit_execution", "docx_execution_summary"): docx_audx_execution_summary,
    ("acct_automation", "docx_adjustments_schedule"): docx_acct_adjustments_schedule,
    ("acct_automation", "xlsx_automation_workbook"): xlsx_acct_automation_workbook,
    ("acct_automation", "docx_automation_workbook"): docx_acct_automation_workbook,
}

TITLES = {
    "depreciation": "Depreciation computation — Schedule 2 pools",
    "salary_tds": "Salary tax and TDS computation",
    "biz_tax": "Business income tax computation",
    "penalty": "Fees, interest and penalty exposure",
    "emi": "Loan amortisation and cost of borrowing",
    "lease": "Lease accounting — NFRS/IFRS 16",
    "fin_statements": "Financial statements — balance sheet, P&L and notes",
    "grant": "Government grant accounting — NAS 20 / IAS 20",
    "projection": "Financial projection, feasibility and bank appraisal",
    "dd": "Due diligence — financial, tax and accounting review",
    "capital_advisory": "Capital & debt advisory — capacity, restructuring, funding mix and break-even",
    "fdi": "Foreign direct investment — structuring, model and eligibility",
    "valuation": "Valuation report",
    "inventory": "Inventory valuation — NAS 2",
    "fixed_assets": "Fixed assets and depreciation — NAS 16",
    "client_directory": "Client directory export",
    "invoice": "Tax invoice",
    "internal_audit": "Internal audit report",
    "stat_audit": "Statutory audit — planning & program",
    "tds": "TDS / withholding register and certificates",
    "personal_tax": "Personal income tax computation",
    "audit_report": "Independent auditor's report",
    "vat": "VAT return",
    "practice_mgmt": "Practice management — proposals, quotations & engagement letters",
    "forensic_audit": "Forensic audit & investigation",
    "corporate_advisory": "Corporate advisory — company law & secretarial compliance",
    "business_advisory": "Business advisory — profitability, budgeting & KPIs",
    "ipo_advisory": "IPO / capital market advisory",
    "tax_advisory": "Tax advisory — reconciliations, provisions & dispute management",
    "audit_execution": "Audit execution — substantive testing, analytical review & going concern",
    "transfer_pricing": "Transfer pricing — advisory & compliance",
    "acct_automation": "Accounting automation — TB diagnostics, cash flow, equity, provisions & closing schedules",
}


_FORMAT_LABELS = {"docx_jva": "JVA", "docx_poa": "POA", "docx_tta": "Technology transfer agreement",
                  "docx_profiles": "Investor profiles", "docx_advisory": "Advisory report",
                  "xlsx_related_party": "Related party transactions", "docx_related_party": "Related party transactions",
                  "xlsx_far_method_benchmark": "FAR, method & benchmarking", "docx_far_method_benchmark": "FAR, method & benchmarking",
                  "xlsx_risk_assessment": "Risk assessment", "docx_risk_assessment": "Risk assessment",
                  "xlsx_tp_study": "Transfer pricing study", "docx_tp_study": "Transfer pricing study",
                  "xlsx_adjustment_exposure": "Adjustment exposure", "docx_adjustment_exposure": "Adjustment exposure",
                  "xlsx_documentation_gap": "Documentation gap", "docx_documentation_gap": "Documentation gap",
                  "docx_advisory_memo": "Advisory memo", "docx_client_info_request": "Client information request",
                  "docx_working_paper_pack": "Working paper pack",
                  "docx_proposal": "Proposal", "docx_fee_quote": "Fee quotation", "docx_engagement_letter": "Engagement letter",
                  "xlsx_investigation_report": "Investigation report", "docx_investigation_report": "Investigation report",
                  "docx_resolution": "Resolution",
                  "xlsx_management_dashboard": "Management dashboard", "docx_management_dashboard": "Management dashboard",
                  "docx_business_plan": "Business plan",
                  "docx_case_response": "Tax notice response",
                  "xlsx_audit_pack": "Tax audit support pack", "docx_audit_pack": "Tax audit support pack",
                  "docx_management_letter": "Management letter",
                  "xlsx_execution_summary": "Audit execution summary", "docx_execution_summary": "Audit execution summary",
                  "docx_adjustments_schedule": "Financial statement adjustments & schedules",
                  "xlsx_automation_workbook": "Accounting automation workbook", "docx_automation_workbook": "Accounting automation workbook"}


def build(module_key, fmt, result, meta, out_dir):
    fn = BUILDERS.get((module_key, fmt))
    if not fn:
        raise ValueError(f"No {fmt} report defined for module '{module_key}'")
    os.makedirs(out_dir, exist_ok=True)
    meta = dict(meta)
    meta.setdefault("title", TITLES.get(module_key, module_key))
    ext = "xlsx" if fmt == "xlsx" or fmt.startswith("xlsx_") else "docx"
    label = _FORMAT_LABELS.get(fmt, module_key)
    safe_client = "".join(ch for ch in (meta.get("client") or "client") if ch.isalnum() or ch in " -_")[:40].strip()
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    filename = f"{safe_client or 'client'} - {label} - {stamp}.{ext}"
    path = os.path.join(out_dir, filename)
    fn(result, meta, path)
    return path, filename
