from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re
from typing import Iterable

from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

from mi100.paths import get_app_root


def _register_fonts() -> tuple[str, str]:
    regular_candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
        "C:/Windows/Fonts/arial.ttf",
    ]
    bold_candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
        "C:/Windows/Fonts/arialbd.ttf",
    ]

    regular_name = "Helvetica"
    bold_name = "Helvetica-Bold"

    for path in regular_candidates:
        if Path(path).exists():
            pdfmetrics.registerFont(TTFont("Mi100Regular", path))
            regular_name = "Mi100Regular"
            break
    for path in bold_candidates:
        if Path(path).exists():
            pdfmetrics.registerFont(TTFont("Mi100Bold", path))
            bold_name = "Mi100Bold"
            break
    return regular_name, bold_name


REG_FONT, BOLD_FONT = _register_fonts()


def _styles():
    base = getSampleStyleSheet()
    return {
        "title": ParagraphStyle(
            "title", parent=base["Normal"], fontName=BOLD_FONT, fontSize=13,
            leading=16, alignment=TA_CENTER, spaceAfter=6,
        ),
        "sub": ParagraphStyle(
            "sub", parent=base["Normal"], fontName=BOLD_FONT, fontSize=10,
            leading=12, alignment=TA_LEFT, spaceAfter=2,
        ),
        "normal": ParagraphStyle(
            "normal", parent=base["Normal"], fontName=REG_FONT, fontSize=10,
            leading=13, alignment=TA_LEFT,
        ),
        "small": ParagraphStyle(
            "small", parent=base["Normal"], fontName=REG_FONT, fontSize=9,
            leading=11, alignment=TA_LEFT,
        ),
        "bold": ParagraphStyle(
            "bold", parent=base["Normal"], fontName=BOLD_FONT, fontSize=10,
            leading=13, alignment=TA_LEFT,
        ),
        "right": ParagraphStyle(
            "right", parent=base["Normal"], fontName=REG_FONT, fontSize=10,
            leading=13, alignment=TA_RIGHT,
        ),
    }


STYLES = _styles()


def invoice_dir() -> Path:
    path = get_app_root() / "documents" / "invoices"
    path.mkdir(parents=True, exist_ok=True)
    return path


def act_dir() -> Path:
    path = get_app_root() / "documents" / "acts"
    path.mkdir(parents=True, exist_ok=True)
    return path




def _available_output_path(path: Path) -> Path:
    path = Path(path)
    if not path.exists():
        return path
    stem = path.stem
    suffix = path.suffix
    parent = path.parent
    for i in range(1, 1000):
        candidate = parent / f"{stem}_{i}{suffix}"
        if not candidate.exists():
            return candidate
    return parent / f"{stem}_{__import__('time').time_ns()}{suffix}"

def sanitize_filename_part(value: str) -> str:
    value = re.sub(r"[\\/:*?\"<>|]+", "_", value or "")
    value = re.sub(r"\s+", " ", value).strip()
    return value[:80] or "document"


def currency_symbol(currency: str) -> str:
    cur = str(currency or 'UAH').upper().strip()
    return {'UAH': '₴', 'USD': '$', 'EUR': '€', 'CAD':'C$', 'PLN': 'zł', 'CZK': 'Kč', 'RON': 'lei', 'BGN':'лв', 'MDL':'L', 'HUF':'Ft', 'RSD':'дин', 'GBP': '£', 'CHF': 'CHF'}.get(cur, cur)


def normalize_currency_code(currency: str | None) -> str:
    raw = str(currency or 'UAH').strip()
    upper = raw.upper()
    aliases = {
        '₴': 'UAH', 'ГРН': 'UAH', 'ГРИВНЯ': 'UAH', 'ГРИВЕНЬ': 'UAH', 'UAH': 'UAH',
        '$': 'USD', 'USD': 'USD', 'ДОЛАР': 'USD', 'ДОЛАРИ': 'USD', 'ДОЛАРІВ': 'USD',
        '€': 'EUR', 'EUR': 'EUR', 'ЄВРО': 'EUR',
        'ZŁ': 'PLN', 'PLN': 'PLN',
        'KČ': 'CZK', 'CZK': 'CZK',
        'LEI': 'RON', 'RON': 'RON',
        'ЛВ': 'BGN', 'BGN': 'BGN',
        'L': 'MDL', 'MDL': 'MDL',
        'FT': 'HUF', 'HUF': 'HUF',
        'ДИН': 'RSD', 'RSD': 'RSD',
        'C$': 'CAD', 'CAD': 'CAD',
        '£': 'GBP', 'GBP': 'GBP',
        'CHF': 'CHF',
    }
    return aliases.get(upper, aliases.get(raw, upper or 'UAH'))


def format_money(amount: float, currency: str) -> str:
    return f"{float(amount or 0):.2f} {currency_symbol(currency)}"


def _currency_words(amount: float, currency: str) -> str:
    amount = float(amount or 0)
    currency = normalize_currency_code(currency)
    whole = int(amount)
    frac = int(round((amount - whole) * 100))
    if frac == 100:
        whole += 1
        frac = 0

    if currency in {'UAH', 'ГРН', 'ГРИВНЯ', 'ГРИВЕНЬ'}:
        unit = _plural_uk(whole, 'гривня', 'гривні', 'гривень')
        return f"{_num_to_words_uk(whole, feminine=False)} {unit} {frac:02d} копійок"
    if currency == 'EUR':
        return f"{_num_to_words_uk(whole, feminine=False)} євро"
    if currency == 'USD':
        return f"{_num_to_words_uk(whole, feminine=False)} доларів США"
    return f"{_num_to_words_uk(whole, feminine=False)} {currency}"


def _money_value_display(amount: float, currency: str) -> str:
    amount = float(amount or 0)
    currency = normalize_currency_code(currency)
    if currency in {'UAH', 'ГРН', 'ГРИВНЯ', 'ГРИВЕНЬ'}:
        whole = int(amount)
        frac = int(round((amount - whole) * 100))
        if frac == 100:
            whole += 1
            frac = 0
        return f"{whole},{frac:02d} грн"
    if amount.is_integer():
        return f"{int(amount)} {currency}"
    return f"{amount:.2f} {currency}"


def format_amount_ua_full(amount: float, currency: str) -> str:
    amount = float(amount or 0)
    currency = normalize_currency_code(currency)
    value = _money_value_display(amount, currency)
    words = _currency_words(amount, currency)
    if currency in {'UAH', 'ГРН', 'ГРИВНЯ', 'ГРИВЕНЬ'}:
        whole = int(amount)
        kop = int(round((amount - whole) * 100))
        if kop == 100:
            whole += 1
            kop = 0
        return (
            f"Загальна сума, що підлягає оплаті: {whole} ({_num_to_words_uk(whole, feminine=True)}) "
            f"гривень {kop:02d} коп. Без ПДВ."
        )
    return f"Загальна сума, що підлягає оплаті: {value} ({words})."


def format_act_total_ua(amount: float, currency: str) -> str:
    amount = float(amount or 0)
    currency = normalize_currency_code(currency)
    value = _money_value_display(amount, currency)
    words = _currency_words(amount, currency)
    if currency in {'UAH', 'ГРН', 'ГРИВНЯ', 'ГРИВЕНЬ'}:
        return f"Загальна вартість виконаних робіт (послуг) становить {value} ({words}). Без ПДВ."
    return f"Загальна вартість виконаних робіт (послуг) становить {value} ({words})."


def _num_to_words_uk(num: int, feminine: bool = False) -> str:
    units_m = ["", "один", "два", "три", "чотири", "п'ять", "шість", "сім", "вісім", "дев'ять"]
    units_f = ["", "одна", "дві", "три", "чотири", "п'ять", "шість", "сім", "вісім", "дев'ять"]
    teens = ["десять", "одинадцять", "дванадцять", "тринадцять", "чотирнадцять", "п'ятнадцять", "шістнадцять", "сімнадцять", "вісімнадцять", "дев'ятнадцять"]
    tens = ["", "", "двадцять", "тридцять", "сорок", "п'ятдесят", "шістдесят", "сімдесят", "вісімдесят", "дев'яносто"]
    hundreds = ["", "сто", "двісті", "триста", "чотириста", "п'ятсот", "шістсот", "сімсот", "вісімсот", "дев'ятсот"]

    def triad_to_words(n: int, fem: bool = False) -> list[str]:
        words = []
        h, rem = divmod(n, 100)
        t, u = divmod(rem, 10)
        if h:
            words.append(hundreds[h])
        if t == 1:
            words.append(teens[u])
        else:
            if t:
                words.append(tens[t])
            if u:
                words.append((units_f if fem else units_m)[u])
        return words

    if num == 0:
        return 'нуль'

    parts = []
    millions, rem = divmod(num, 1_000_000)
    thousands, rest = divmod(rem, 1_000)

    if millions:
        parts.extend(triad_to_words(millions))
        parts.append(_plural_uk(millions, 'мільйон', 'мільйони', 'мільйонів'))
    if thousands:
        parts.extend(triad_to_words(thousands, True))
        parts.append(_plural_uk(thousands, 'тисяча', 'тисячі', 'тисяч'))
    if rest:
        parts.extend(triad_to_words(rest, feminine))
    return ' '.join([p for p in parts if p]).strip()


def _plural_uk(n: int, one: str, few: str, many: str) -> str:
    n = abs(int(n)) % 100
    if 11 <= n <= 14:
        return many
    n = n % 10
    if n == 1:
        return one
    if 2 <= n <= 4:
        return few
    return many


def format_invoice_date(date_text: str) -> str:
    return f"від {date_text}"


def format_act_date(date_text: str) -> str:
    try:
        y, m, d = [int(x) for x in str(date_text).split('-')]
        months = ["січня", "лютого", "березня", "квітня", "травня", "червня", "липня", "серпня", "вересня", "жовтня", "листопада", "грудня"]
        return f"від {d} {months[m-1]} {y} р."
    except Exception:
        return f"від {date_text}"

@dataclass
class GeneratedDocument:
    file_path: str
    total_amount: float
    currency: str
    with_contract: bool


def document_dir(doc_type: str) -> Path:
    root = get_app_root() / "documents"
    if doc_type == "INVOICE":
        path = root / "invoices"
    else:
        path = root / "acts"
    path.mkdir(parents=True, exist_ok=True)
    return path




def _group_rows_by_currency(rows: Iterable[dict], currency_keys: tuple[str, ...] = ("currency", "currency_snapshot")) -> list[tuple[str, list[dict]]]:
    groups: dict[str, list[dict]] = {}
    for row in rows:
        cur = ''
        for key in currency_keys:
            cur = str(row.get(key) or '').strip()
            if cur:
                break
        cur = normalize_currency_code(cur or 'UAH')
        groups.setdefault(cur, []).append(row)
    return sorted(groups.items(), key=lambda x: x[0])

def build_invoice_pdf(*, doc_number: str, created_date: str, client: dict, projects: list[dict],
                      supplier_requisites_text: str, signer_name: str, signer_position: str) -> GeneratedDocument:
    grouped = _group_rows_by_currency(projects, ("currency",))
    currency = grouped[0][0] if len(grouped) == 1 else ''
    total_amount = sum(float(p.get("total_amount") or 0) for p in projects)
    with_contract = bool((client.get("contract_number") or "").strip())
    client_name = client.get("company_name") or client.get("display_name") or "Замовник"
    filename = f"invoice_{sanitize_filename_part(doc_number)}_{sanitize_filename_part(client_name)}.pdf"
    path = document_dir("INVOICE") / filename

    service_text = "Послуги звукозапису підрядницького голосу"
    contract_number = (client.get("contract_number") or "").strip()
    contract_date = (client.get("contract_date_text") or "").strip()
    if with_contract and contract_number:
        service_text += f" згідно з Договором № {contract_number}"
        if contract_date:
            service_text += f" від {contract_date}"

    parties_widths = [87 * mm, 87 * mm]
    items_widths = [12 * mm, 82 * mm, 24 * mm, 14 * mm, 21 * mm, 21 * mm]

    story = []
    story.append(Paragraph(f"Рахунок-фактура №{doc_number}", STYLES["title"]))
    story.append(Paragraph(format_invoice_date(created_date), STYLES["title"]))
    story.append(Spacer(1, 4 * mm))

    parties = Table([
        [Paragraph("<b>Постачальник</b>", STYLES["bold"]), Paragraph("<b>Платник</b>", STYLES["bold"])],
        [Paragraph(_multiline_to_html(supplier_requisites_text), STYLES["small"]), Paragraph(_multiline_to_html(_client_payment_block(client)), STYLES["small"])],
    ], colWidths=parties_widths)
    parties.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("BOX", (0, 0), (-1, -1), 0.8, colors.black),
        ("INNERGRID", (0, 0), (-1, -1), 0.4, colors.black),
        ("LEFTPADDING", (0, 0), (-1, -1), 4),
        ("RIGHTPADDING", (0, 0), (-1, -1), 4),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
    ]))
    story.extend([parties, Spacer(1, 5 * mm)])

    multi = len(grouped) > 1
    for idx_group, (cur, cur_projects) in enumerate(grouped):
        if multi:
            story.append(Paragraph(f"<b>Валюта: {cur}</b>", STYLES["normal"]))
            story.append(Spacer(1, 2 * mm))
        rows = [[
            Paragraph("<b>№</b>", STYLES["small"]),
            Paragraph("<b>Найменування</b>", STYLES["small"]),
            Paragraph("<b>Одиниця виміру</b>", STYLES["small"]),
            Paragraph("<b>К-сть</b>", STYLES["small"]),
            Paragraph("<b>Ціна</b>", STYLES["small"]),
            Paragraph("<b>Сума</b>", STYLES["small"]),
        ]]
        subtotal = 0.0
        for idx, proj in enumerate(cur_projects, start=1):
            amount = float(proj.get("total_amount") or 0)
            subtotal += amount
            title = (proj.get("title") or "").strip()
            row_text = service_text if len(cur_projects) == 1 else f"{service_text}. Проєкт: {title}"
            rows.append([
                Paragraph(str(idx), STYLES["small"]),
                Paragraph(_escape(row_text), STYLES["small"]),
                Paragraph("послуга", STYLES["small"]),
                Paragraph("1", STYLES["small"]),
                Paragraph(f"{_money_value_display(amount, cur)}.", STYLES["small"]),
                Paragraph(f"{_money_value_display(amount, cur)}.", STYLES["small"]),
            ])
        rows.append([
            "", Paragraph("<b>Сума (без ПДВ)</b>", STYLES["small"]), "", "", "", Paragraph(f"<b>{_money_value_display(subtotal, cur)}.</b>", STYLES["small"])
        ])
        tbl = Table(rows, colWidths=items_widths, repeatRows=1)
        tbl.setStyle(TableStyle([
            ("BOX", (0, 0), (-1, -1), 0.8, colors.black),
            ("INNERGRID", (0, 0), (-1, -1), 0.4, colors.black),
            ("VALIGN", (0, 0), (-1, -1), "TOP"),
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#EBEBEB")),
            ("ALIGN", (0, 0), (0, -1), "CENTER"),
            ("ALIGN", (2, 1), (3, -1), "CENTER"),
            ("ALIGN", (4, 1), (-1, -1), "RIGHT"),
            ("LEFTPADDING", (0, 0), (-1, -1), 4),
            ("RIGHTPADDING", (0, 0), (-1, -1), 4),
            ("TOPPADDING", (0, 0), (-1, -1), 4),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ]))
        story.extend([tbl, Spacer(1, 4 * mm)])
        story.append(Paragraph(format_amount_ua_full(subtotal, cur), STYLES["bold"]))
        story.append(Spacer(1, 4 * mm))

    story.append(Spacer(1, 6 * mm))
    if signer_name:
        signature = f"______________________________{_escape(signer_name)}"
        sign_tbl = Table([[Paragraph(signature, STYLES["normal"]) ]], colWidths=[174*mm])
        sign_tbl.setStyle(TableStyle([("VALIGN", (0,0), (-1,-1), "TOP")]))
        story.append(sign_tbl)

    _build_pdf(path, story)
    return GeneratedDocument(str(path), total_amount, currency or 'MULTI', with_contract)

def build_act_pdf(*, doc_number: str, created_date: str, client: dict, invoices: list[dict], projects: list[dict],
                  executor_legal_text: str, signer_name: str, signer_position: str) -> GeneratedDocument:
    invoice0 = invoices[0] if invoices else {}
    with_contract = bool(int(invoice0.get("with_contract") or 0) or (client.get("contract_number") or '').strip())
    grouped = _group_rows_by_currency(projects, ("currency_snapshot", "currency"))
    currency = grouped[0][0] if len(grouped) == 1 else ''
    total_amount = sum(float(inv.get("total_amount") or 0) for inv in invoices) if invoices else sum(float(p.get('amount_snapshot') if p.get('amount_snapshot') is not None else p.get('total_amount') or 0) for p in projects)
    client_name = client.get("company_name") or client.get("display_name") or "Замовник"
    filename = f"act_{sanitize_filename_part(doc_number)}_{sanitize_filename_part(client_name)}.pdf"
    path = document_dir("ACT") / filename

    story = []
    story.append(Paragraph(f"АКТ № {doc_number}", STYLES["title"]))
    story.append(Paragraph("здачі-прийняття робіт (надання послуг)", STYLES["title"]))
    story.append(Paragraph(format_act_date(created_date), STYLES["title"]))
    story.append(Spacer(1, 4 * mm))

    client_legal = (client.get("legal_block_text") or "").strip()
    if client_legal:
        story.append(Paragraph(_multiline_to_html(client_legal), STYLES["normal"]))
        story.append(Spacer(1, 2 * mm))

    executor_legal = (executor_legal_text or "").strip()
    contract_number = (invoice0.get("contract_number_snapshot") or client.get("contract_number") or "").strip()
    contract_date = (invoice0.get("contract_date_snapshot") or client.get("contract_date_text") or "").strip()
    if executor_legal:
        suffix = ""
        if with_contract and contract_number:
            suffix = f" згідно з Договором № {contract_number} від {contract_date}:" if contract_date else f" згідно з Договором № {contract_number}:"
        elif invoices:
            numbers = ', '.join(str(inv.get('doc_number') or '') for inv in invoices if inv.get('doc_number'))
            if numbers:
                suffix = " згідно рахунків:\n" + numbers
        story.append(Paragraph(_multiline_to_html(executor_legal + suffix), STYLES["normal"]))
        story.append(Spacer(1, 4 * mm))

    multi = len(grouped) > 1
    for cur, cur_projects in grouped:
        if multi:
            story.append(Paragraph(f"<b>Валюта: {cur}</b>", STYLES["normal"]))
            story.append(Spacer(1, 2 * mm))
        rows = [[
            Paragraph("<b>Дата</b>", STYLES["small"]),
            Paragraph("<b>Назва проекту</b>", STYLES["small"]),
            Paragraph("<b>Вартість</b>", STYLES["small"]),
        ]]
        subtotal = 0.0
        for p in cur_projects:
            amount = float(p.get('amount_snapshot') if p.get('amount_snapshot') is not None else p.get('total_amount') or 0)
            subtotal += amount
            rows.append([
                Paragraph(_escape(str(p.get("project_date_snapshot") or p.get("created_date") or "")), STYLES["small"]),
                Paragraph(_escape(str(p.get("project_title_snapshot") or p.get("title") or "")), STYLES["small"]),
                Paragraph(_money_value_display(amount, cur), STYLES["small"]),
            ])
        rows.append([
            "", Paragraph("<b>Всього</b>", STYLES["small"]), Paragraph(f"<b>{_money_value_display(subtotal, cur)}</b>", STYLES["small"])
        ])
        tbl = Table(rows, colWidths=[28 * mm, 107 * mm, 40 * mm], repeatRows=1)
        tbl.setStyle(TableStyle([
            ("BOX", (0, 0), (-1, -1), 0.8, colors.black),
            ("INNERGRID", (0, 0), (-1, -1), 0.4, colors.black),
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#EBEBEB")),
            ("ALIGN", (2, 1), (2, -1), "RIGHT"),
            ("VALIGN", (0, 0), (-1, -1), "TOP"),
            ("LEFTPADDING", (0, 0), (-1, -1), 4),
            ("RIGHTPADDING", (0, 0), (-1, -1), 4),
            ("TOPPADDING", (0, 0), (-1, -1), 4),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ]))
        story.extend([tbl, Spacer(1, 4 * mm)])
        story.append(Paragraph(format_act_total_ua(subtotal, cur), STYLES["normal"]))
        story.append(Spacer(1, 3 * mm))

    story.append(Paragraph("Сторони претензій одна до одної не мають.", STYLES["normal"]))
    story.append(Spacer(1, 8 * mm))

    studio_name = (signer_name or "").strip()
    studio_pos = (signer_position or "").strip()
    client_signer = (client.get("authorized_signer_name") or "").strip()
    client_pos = (client.get("authorized_signer_position") or "").strip()

    sign_tbl = Table([
        [Paragraph("<b>Роботу здав:</b>", STYLES["normal"]), Paragraph("<b>Роботу прийняв:</b>", STYLES["normal"])],
        [Paragraph(_escape(studio_name), STYLES["normal"]), Paragraph(_escape(client_signer), STYLES["normal"])],
        [Paragraph(_escape(studio_pos), STYLES["normal"]), Paragraph(_escape(client_pos), STYLES["normal"])],
    ], colWidths=[87 * mm, 87 * mm])
    sign_tbl.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("LEFTPADDING", (0, 0), (-1, -1), 0),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
        ("TOPPADDING", (0, 0), (-1, -1), 2),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
    ]))
    story.append(sign_tbl)

    _build_pdf(path, story)
    return GeneratedDocument(str(path), total_amount, currency or 'MULTI', with_contract)

def _detect_currency(projects: Iterable[dict]) -> str:
    for p in projects:
        cur = str(p.get("currency") or p.get("currency_snapshot") or "").strip()
        if cur:
            return cur
    return "UAH"


def _build_pdf(path: Path, story: list) -> None:
    doc = SimpleDocTemplate(
        str(path), pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm, topMargin=14 * mm, bottomMargin=14 * mm,
    )
    doc.build(story)


def _escape(text: str) -> str:
    return (text or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def _multiline_to_html(text: str) -> str:
    return "<br/>".join(_escape(line) for line in (text or "").splitlines())


def _client_payment_block(client: dict) -> str:
    req = (client.get("requisites_text") or "").strip()
    return req


def _build_act_intro(client: dict, executor_legal_text: str, with_contract: bool, invoice: dict) -> str:
    left = (executor_legal_text or "").strip() or "Виконавець"
    client_name = client.get("company_name") or client.get("display_name") or "Замовник"
    signer_name = (client.get("authorized_signer_name") or "").strip()
    signer_position = (client.get("authorized_signer_position") or "").strip()
    contract_number = (invoice.get("contract_number_snapshot") or client.get("contract_number") or "").strip()
    contract_date = (invoice.get("contract_date_snapshot") or client.get("contract_date_text") or "").strip()

    if signer_name and signer_position:
        right = f"{client_name}, в особі {signer_position} {signer_name}"
    elif signer_name:
        right = f"{client_name}, в особі {signer_name}"
    else:
        right = client_name

    if with_contract and contract_number:
        contract_ref = f" На підставі договору № {contract_number}"
        if contract_date:
            contract_ref += f" від {contract_date}"
        contract_ref += "."
    else:
        contract_ref = " Сторони підтверджують виконання наступних робіт:"

    return f"{left}, з однієї сторони, та {right}, з іншої сторони, склали цей Акт про те, що Виконавець надав, а Замовник прийняв наступні послуги.{contract_ref}"




def _currency_totals_blocks(projects: list[dict]) -> tuple[list[tuple[str, list[dict], float, str, str, str]], dict[str, dict[str, str]], str, str, str, str]:
    grouped = _group_rows_by_currency(projects, ("currency", "currency_snapshot"))
    lines: list[str] = []
    sections_html: list[str] = []
    totals_lines: list[str] = []
    numeric_lines: list[str] = []
    per_currency: dict[str, dict[str, str]] = {}
    running_idx = 1
    for cur, cur_projects in grouped:
        subtotal = 0.0
        table_rows = []
        for project in cur_projects:
            number = project.get('project_number') or project.get('project_number_snapshot') or ''
            title = project.get('title') or project.get('project_title_snapshot') or ''
            date = project.get('created_date') or project.get('project_date_snapshot') or ''
            amount = float(project.get('total_amount') or project.get('amount_snapshot') or 0.0)
            subtotal += amount
            lines.append(f"{running_idx}. №{number} • {date} • {title} • {format_money(amount, cur)}")
            display_title = str(title or '—').strip() or '—'
            amount_text = format_money(amount, cur)
            table_rows.append(
                '<tr>'
                f'<td style="text-align:center;width:6%;">{running_idx}</td>'
                f'<td style="width:42%;">{_escape(display_title)}</td>'
                '<td style="text-align:center;width:13%;">послуга</td>'
                '<td style="text-align:center;width:8%;">1</td>'
                f'<td style="text-align:center;width:15%;">{_escape(amount_text)}</td>'
                f'<td style="text-align:center;width:16%;">{_escape(amount_text)}</td>'
                '</tr>'
            )
            running_idx += 1
        table_html = (
            '<table border="1" style="width:100%;border-collapse:collapse;border:1.4px solid #000000;">'
            '<thead>'
            '<tr>'
            '<th style="text-align:center;width:6%;">№</th>'
            '<th style="text-align:center;width:42%;">Найменування</th>'
            '<th style="text-align:center;width:13%;">Одиниця виміру</th>'
            '<th style="text-align:center;width:8%;">К-сть</th>'
            '<th style="text-align:center;width:15%;">Ціна</th>'
            '<th style="text-align:center;width:16%;">Сума</th>'
            '</tr>'
            '</thead>'
            '<tbody>' + (''.join(table_rows) if table_rows else '<tr><td colspan="6" style="text-align:center;">—</td></tr>') + '</tbody>'
            '<tfoot>'
            '<tr>'
            f'<td colspan="5" style="text-align:right;font-weight:700;">Сума (без ПДВ)</td>'
            f'<td style="text-align:center;font-weight:700;">{_escape(_money_value_display(subtotal, cur))}</td>'
            '</tr>'
            '</tfoot>'
            '</table>'
        )
        total_line = _templated_total_text(subtotal, cur)
        section_title = '' if len(grouped) == 1 else f'<div style="font-weight:700;margin:8px 0 4px 0;">Валюта: {currency_symbol(cur)}</div>'
        section_html = section_title + table_html + f'<div style="margin-top:6px;font-weight:700;">{_escape(total_line)}</div>'
        sections_html.append(section_html)
        totals_lines.append(total_line)
        numeric_lines.append(_money_value_display(subtotal, cur))
        per_currency[cur] = {
            'table': table_html,
            'section': section_html,
            'total_text': total_line,
            'total_number': _money_value_display(subtotal, cur),
            'symbol': currency_symbol(cur),
        }
    projects_block = '<br/>'.join(_escape(x) for x in lines) if lines else '—'
    projects_table = '<br/><br/>'.join(sections_html) if sections_html else '—'
    total_numeric = '<br/>'.join(_escape(x) for x in numeric_lines) if numeric_lines else _escape(_money_value_display(0, 'UAH'))
    total_text = '<br/>'.join(_escape(x) for x in totals_lines) if totals_lines else _escape(_templated_total_text(0, 'UAH'))
    currencies_list = ', '.join(currency_symbol(cur) for cur, _ in grouped) if grouped else currency_symbol('UAH')
    return [(cur, rows, sum(float(p.get('total_amount') or p.get('amount_snapshot') or 0.0) for p in rows), per_currency[cur]['table'], per_currency[cur]['section'], per_currency[cur]['total_text']) for cur, rows in grouped], per_currency, projects_block, projects_table, total_numeric, total_text, currencies_list


def build_templated_client_document_pdf(*, doc_type: str, doc_number: str, created_date: str, client: dict, projects: list[dict], supplier_requisites_text: str, template_text: str) -> GeneratedDocument:
    doc_type = str(doc_type or 'INVOICE').upper().strip()
    is_invoice = doc_type == 'INVOICE'
    out_dir = invoice_dir() if is_invoice else act_dir()
    prefix = 'invoice' if is_invoice else 'act'
    client_name = client.get('company_name') or client.get('display_name') or 'client'
    path = out_dir / f"{prefix}_{sanitize_filename_part(doc_number)}_{sanitize_filename_part(client_name)}.pdf"
    grouped = _group_rows_by_currency(projects, ("currency", "currency_snapshot"))
    currency = grouped[0][0] if len(grouped) == 1 else ''
    total_amount = sum(float(p.get('total_amount') or p.get('amount_snapshot') or 0.0) for p in projects)

    sections, per_currency, projects_block, projects_table, total_numeric, total_text, currencies_list = _currency_totals_blocks(projects)
    project_price_text = _money_value_display(float(projects[0].get('total_amount') or projects[0].get('amount_snapshot') or 0.0), normalize_currency_code(projects[0].get('currency') or projects[0].get('currency_snapshot') or currency or 'UAH')) if projects else '0.00'

    replacements = {
        '[[DOC_NUMBER]]': _escape(doc_number),
        '[[CREATED_DATE]]': _escape(created_date),
        '[[PROJECTS_LIST]]': projects_block,
        '[[PROJECTS_TABLE]]': projects_table,
        '[[PROJECTS_TABLE_ALL]]': projects_table,
        '[[CURRENCY_SECTIONS]]': projects_table,
        '[[PROJECT_PRICE]]': _escape(project_price_text),
        '[[PAYER_REQUISITES]]': _multiline_to_html((client.get('requisites_text') or '').strip()) or '—',
        '[[SUPPLIER_REQUISITES]]': _multiline_to_html((supplier_requisites_text or '').strip()) or '—',
        '[[TOTAL_AMOUNT]]': total_text,
        '[[TOTAL_AMOUNT_NUMBER]]': total_numeric,
        '[[TOTALS_BY_CURRENCY]]': total_numeric,
        '[[TOTALS_WORDS_BY_CURRENCY]]': total_text,
        '[[CURRENCIES_LIST]]': _escape(currencies_list),
    }
    for code, payload in per_currency.items():
        normalized = normalize_currency_code(code)
        replacements[f'[[PROJECTS_TABLE_{normalized}]]'] = payload['table']
        replacements[f'[[CURRENCY_SECTION_{normalized}]]'] = payload['section']
        replacements[f'[[TOTAL_AMOUNT_{normalized}]]'] = _escape(payload['total_number'])
        replacements[f'[[TOTAL_AMOUNT_WORDS_{normalized}]]'] = _escape(payload['total_text'])

    text_body = (template_text or '').strip()
    if not text_body:
        text_body = '[[DOC_NUMBER]]<br/>[[CREATED_DATE]]<br/><br/>[[PROJECTS_TABLE]]<br/><br/>[[TOTAL_AMOUNT]]'
    is_html = '<html' in text_body.lower() or '<body' in text_body.lower()
    if not is_html:
        text_body = _escape(text_body).replace('\n', '<br/>')

    for key, value in replacements.items():
        text_body = text_body.replace(key, value).replace(_escape(key), value)

    _render_html_to_pdf(path, 'Рахунок' if is_invoice else 'Акт', text_body)
    return GeneratedDocument(str(path), total_amount, currency or 'MULTI', False)


def _templated_total_text(amount: float, currency: str) -> str:
    return f"{_money_value_display(amount, currency)} ({_currency_words(amount, currency)})"


def _render_html_to_pdf(path: Path, title: str, html_body: str) -> None:
    try:
        from PySide6.QtGui import QTextDocument
        from PySide6.QtCore import QMarginsF
        from PySide6.QtGui import QPageLayout, QPageSize
        from PySide6.QtPrintSupport import QPrinter

        printer = QPrinter(QPrinter.HighResolution)
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setOutputFileName(str(path))
        printer.setPageSize(QPageSize(QPageSize.A4))
        printer.setPageMargins(QMarginsF(18, 14, 18, 14), QPageLayout.Millimeter)

        doc = QTextDocument()
        doc.setDocumentMargin(0)
        table_css = (
            "table[border]:not([border='0']), table[border]:not([border='0']) td, table[border]:not([border='0']) th {"
            " border: 1.4px solid #000000 !important; border-color: #000000 !important; }"
            "table[border='0'], table[border='0'] td, table[border='0'] th { border: none !important; }"
            "table { border-collapse: collapse; }"
        )
        base_css = "body{font-family:'Cambria Math','Segoe UI Symbol','DejaVu Sans','Arial Unicode MS','Segoe UI';}"
        doc.setHtml(f"<html><head><meta charset='utf-8'><style>{base_css}{table_css}</style></head><body>{html_body}</body></html>")
        doc.print_(printer)
        return
    except Exception:
        pass

    story = [
        Paragraph(title, STYLES['title']),
        Spacer(1, 3 * mm),
        Paragraph(html_body.replace('\n', '<br/>'), STYLES['normal']),
    ]
    _build_pdf(path, story)

def report_dir(kind: str) -> Path:
    root = get_app_root() / "documents" / "reports"
    path = root / kind
    path.mkdir(parents=True, exist_ok=True)
    return path


def _totals_by_currency(rows: list[dict], amount_key: str, paid_key: str, currency_key: str = "currency") -> list[tuple[str, float]]:
    totals: dict[str, float] = {}
    for row in rows:
        if int(row.get(paid_key) or 0) != 0:
            continue
        cur = normalize_currency_code(row.get(currency_key) or 'UAH')
        totals[cur] = totals.get(cur, 0.0) + float(row.get(amount_key) or 0)
    return sorted(totals.items())


def _totals_paragraph_lines(rows: list[dict], amount_key: str, paid_key: str, currency_key: str = "currency", prefix: str = "Разом до оплати:", fallback_currency: str = 'UAH') -> list[str]:
    totals = _totals_by_currency(rows, amount_key, paid_key, currency_key)
    if not totals:
        cur = str(fallback_currency or 'UAH')
        return [f"{prefix} {_money_value_display(0.0, cur)} ({_currency_words(0.0, cur)})"]
    lines = []
    for idx, (cur, amount) in enumerate(totals):
        label = prefix if idx == 0 else ""
        value = _money_value_display(amount, cur)
        words = _currency_words(amount, cur)
        lines.append(f"{label} {value} ({words})".strip())
    return lines


def build_client_report_pdf(*, client: dict, rows: list[dict], start_date: str | None, end_date: str | None, unpaid_only: bool, balance_map: dict[str, float] | None = None, debt_map: dict[str, float] | None = None) -> str:
    client_name = client.get('company_name') or client.get('display_name') or 'Замовник'
    suffix = 'all_unpaid' if unpaid_only else f"{start_date or 'all'}_{end_date or 'all'}"
    filename = f"client_report_{sanitize_filename_part(client_name)}_{sanitize_filename_part(suffix)}.pdf"
    path = _available_output_path(report_dir('clients') / filename)
    period_text = f"Період: {start_date or '...'} — {end_date or '...'}"
    if unpaid_only:
        period_text += ' | Усі неоплачені'
    story = [
        Paragraph("Звіт по замовнику", STYLES['title']),
        Paragraph(f"Замовник: {client_name}", STYLES['bold']),
        Paragraph(period_text, STYLES['normal']),
        Spacer(1, 4*mm),
    ]
    balance_map = balance_map or {}
    debt_map = debt_map or {}
    if balance_map:
        story.append(Paragraph("Залишковий баланс:", STYLES['bold']))
        for code, amount in sorted(balance_map.items()):
            if abs(float(amount or 0.0)) < 0.000001:
                continue
            story.append(Paragraph(f"{currency_symbol(code)} {float(amount or 0.0):.2f}", STYLES['normal']))
        if debt_map:
            story.append(Spacer(1, 1*mm))
            story.append(Paragraph("Загальна сума боргу з урахуванням балансу:", STYLES['bold']))
            for code, amount in sorted(debt_map.items()):
                if abs(float(amount or 0.0)) < 0.000001:
                    continue
                story.append(Paragraph(f"{currency_symbol(code)} {float(amount or 0.0):.2f}", STYLES['normal']))
        story.append(Spacer(1, 3*mm))
    grouped = _group_rows_by_currency(rows, ("currency",))
    multi = len(grouped) > 1
    for cur, cur_rows in grouped:
        if multi:
            story.append(Paragraph(f"<b>Валюта: {cur}</b>", STYLES['normal']))
            story.append(Spacer(1, 2*mm))
        table_data = [[
            Paragraph('Дата', STYLES['bold']),
            Paragraph('№', STYLES['bold']),
            Paragraph('Проєкт', STYLES['bold']),
            Paragraph('Статус', STYLES['bold']),
            Paragraph('Сума', STYLES['bold']),
            Paragraph('Оплата', STYLES['bold']),
        ]]
        for row in cur_rows:
            paid = int(row.get('client_paid') or 0)
            work = str(row.get('work_status') or '').upper()
            status_text = 'Завершено' if work == 'COMPLETED' else 'В роботі'
            payment_text = 'Оплачено' if paid else 'Не оплачено'
            table_data.append([
                Paragraph(str(row.get('created_date') or ''), STYLES['small']),
                Paragraph(str(row.get('project_number') or ''), STYLES['small']),
                Paragraph(str(row.get('title') or ''), STYLES['small']),
                Paragraph(status_text, STYLES['small']),
                Paragraph(format_money(float(row.get('total_amount') or 0), cur), STYLES['small']),
                Paragraph(payment_text, STYLES['small']),
            ])
        table = Table(table_data, colWidths=[24*mm, 14*mm, 70*mm, 26*mm, 28*mm, 26*mm], repeatRows=1)
        table.setStyle(TableStyle([
            ('GRID', (0,0), (-1,-1), 0.5, colors.black),
            ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#EBEBEB')),
            ('VALIGN', (0,0), (-1,-1), 'TOP'),
        ]))
        story.append(table)
        story.append(Spacer(1, 4*mm))
        unpaid_amount = sum(float(r.get('total_amount') or 0) for r in cur_rows if int(r.get('client_paid') or 0) == 0)
        for line in _totals_paragraph_lines(cur_rows, 'total_amount', 'client_paid', 'currency', 'Разом до оплати:', fallback_currency=cur):
            story.append(Paragraph(line, STYLES['bold']))
        balance_for_cur = float((balance_map or {}).get(cur, 0.0) or 0.0)
        if balance_for_cur > 0:
            adjusted = max(0.0, unpaid_amount - balance_for_cur)
            story.append(Paragraph(f"До сплати з урахуванням балансу: {_money_value_display(adjusted, cur)} ({_currency_words(adjusted, cur)})", STYLES['bold']))
        story.append(Spacer(1, 3*mm))
    doc = SimpleDocTemplate(str(path), pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=15*mm, bottomMargin=15*mm)
    try:
        doc.build(story)
    except PermissionError:
        path = _available_output_path(path)
        doc = SimpleDocTemplate(str(path), pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=15*mm, bottomMargin=15*mm)
        doc.build(story)
    return str(path)

def build_actor_report_pdf(*, actor: dict, rows: list[dict], start_date: str | None, end_date: str | None, unpaid_only: bool, free_only: bool=False) -> str:
    actor_name = actor.get('display_name') or "Підрядник"
    suffix = 'all_free' if free_only else ('all_unpaid' if unpaid_only else f"{start_date or 'all'}_{end_date or 'all'}")
    filename = f"actor_report_{sanitize_filename_part(actor_name)}_{sanitize_filename_part(suffix)}.pdf"
    path = _available_output_path(report_dir('actors') / filename)
    period_flags = []
    if unpaid_only:
        period_flags.append('Усі неоплачені')
    if free_only:
        period_flags.append('Усі безкоштовні')
    period_text = f"Період: {start_date or '...'} — {end_date or '...'}"
    if period_flags:
        period_text += ' | ' + ', '.join(period_flags)
    story = [
        Paragraph("Звіт по підряднику", STYLES['title']),
        Paragraph(f"Підрядник: {actor_name}", STYLES['bold']),
        Paragraph(period_text, STYLES['normal']),
        Spacer(1, 4*mm),
    ]
    grouped_items = _group_rows_by_currency(rows, ('fee_currency', 'currency'))
    for cur, cur_rows in grouped_items:
        if len(grouped_items) > 1:
            story.append(Paragraph(f"Валюта: {currency_symbol(cur)}", STYLES['bold']))
            story.append(Spacer(1, 2*mm))
        table_data = [[
            Paragraph('Дата', STYLES['bold']),
            Paragraph('Проєкт', STYLES['bold']),
            Paragraph('Гонорар', STYLES['bold']),
            Paragraph('Оплата', STYLES['bold']),
        ]]
        for row in cur_rows:
            paid = int(row.get('paid_to_actor') or 0)
            fee = float(row.get('fee') or 0)
            payment_text = 'Безкоштовно' if abs(fee) < 0.000001 else ('Оплачено' if paid else 'Не оплачено')
            table_data.append([
                Paragraph(str(row.get('created_date') or ''), STYLES['small']),
                Paragraph(str(row.get('title') or ''), STYLES['small']),
                Paragraph(_money_value_display(fee, cur), STYLES['small']),
                Paragraph(payment_text, STYLES['small']),
            ])
        table = Table(table_data, colWidths=[26*mm, 96*mm, 28*mm, 28*mm], repeatRows=1)
        table.setStyle(TableStyle([
            ('GRID', (0,0), (-1,-1), 0.5, colors.black),
            ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#EBEBEB')),
            ('VALIGN', (0,0), (-1,-1), 'TOP'),
        ]))
        story.append(table)
        story.append(Spacer(1, 4*mm))
        for line in _totals_paragraph_lines(cur_rows, 'fee', 'paid_to_actor', 'fee_currency', 'Разом до виплати:', fallback_currency=cur):
            story.append(Paragraph(line, STYLES['bold']))
        story.append(Spacer(1, 3*mm))
    doc = SimpleDocTemplate(str(path), pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=15*mm, bottomMargin=15*mm)
    try:
        doc.build(story)
    except PermissionError:
        path = _available_output_path(path)
        doc = SimpleDocTemplate(str(path), pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=15*mm, bottomMargin=15*mm)
        doc.build(story)
    return str(path)
