"""Shared ReportLab PDF builders for the contract-issuance email:
one simple Contract (legal agreement) PDF and one detailed Invoice PDF,
sent as two separate attachments — mirrors how the wizard's "Review &
sign" preview only ever showed the simple agreement, with billing detail
kept in its own document."""
from __future__ import annotations

import asyncio
import base64
from decimal import Decimal
from io import BytesIO
from typing import TYPE_CHECKING, Optional
from xml.sax.saxutils import escape as _xml_escape

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession
    from src.apps.billing.models.invoice import Invoice
    from src.apps.sales.models.contract import Contract
    from src.apps.tenants.models.account import Account


async def get_presigned_contract_pdf_url(
    contract: "Contract", expires_in: int = 600
) -> Optional[str]:
    """Return a presigned S3 GET URL for the contract's generated PDF, or
    ``None`` when S3 isn't configured, the contract has no ``pdf_s3_key`` yet,
    or the presign call itself fails.

    Shared by both the authenticated ``GET /sales/contracts/{id}/pdf``
    endpoint and the public, unauthenticated QR-code redirect endpoint
    (``GET /public/contracts/{subdomain}/{contract_number}/pdf``) so the
    presign-vs-on-demand-stream branch lives in exactly one place.
    """
    from src.core.config import settings

    if not (contract.pdf_s3_key and settings.AWS_ACCESS_KEY_ID and settings.AWS_SECRET_ACCESS_KEY):
        return None

    import boto3
    from botocore.exceptions import NoCredentialsError, ClientError

    s3_key = contract.pdf_s3_key

    def _make_presigned() -> str:
        s3 = boto3.client(
            "s3",
            region_name=settings.AWS_REGION,
            aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
            aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
        )
        return s3.generate_presigned_url(
            "get_object",
            Params={"Bucket": settings.S3_BUCKET, "Key": s3_key},
            ExpiresIn=expires_in,
        )

    try:
        return await asyncio.to_thread(_make_presigned)
    except (NoCredentialsError, ClientError):
        return None


def _crest_initials(cemetery_name: str) -> str:
    """First letter of each of the first two words, uppercased — mirrors the
    wizard preview's crestInitials (ContractWizardModal.tsx)."""
    words = [w for w in cemetery_name.split() if w]
    initials = "".join(w[0].upper() for w in words[:2])
    return initials or "C"


async def resolve_plot_summary_label(
    db: "AsyncSession", tenant_id, plot_id
) -> Optional[str]:
    """Build the "A-1 · A · Standard single" plot summary used on the
    contract PDF (plot_ref · section code · plot type name) — mirrors
    ContractWizardModal's selectedPlotLabel. None when no plot is set."""
    if not plot_id:
        return None

    from sqlalchemy import select
    from src.apps.plots.models.plot import Plot
    from src.apps.plots.models.plot_type import PlotType
    from src.apps.sections.models.section import Section

    plot = (
        await db.execute(
            select(Plot).where(Plot.id == plot_id, Plot.tenant_id == tenant_id)
        )
    ).scalar_one_or_none()
    if not plot:
        return None

    parts = [plot.plot_ref]

    if plot.section_id:
        section = (
            await db.execute(
                select(Section).where(
                    Section.id == plot.section_id, Section.tenant_id == tenant_id
                )
            )
        ).scalar_one_or_none()
        if section:
            parts.append(section.code)

    if plot.plot_type_id:
        plot_type = (
            await db.execute(
                select(PlotType).where(
                    PlotType.id == plot.plot_type_id, PlotType.tenant_id == tenant_id
                )
            )
        ).scalar_one_or_none()
        if plot_type:
            parts.append(plot_type.name)

    return " · ".join(parts)


def _base_styles(accent_color_hex: Optional[str]):
    """Style set shared by both PDFs so they read as one document family."""
    from reportlab.lib import colors
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle

    from src.core.constants import DEFAULT_ACCENT_COLOR

    PRIMARY = colors.HexColor(accent_color_hex or DEFAULT_ACCENT_COLOR)
    GRAY = colors.HexColor("#6b7280")
    TEXT = colors.HexColor("#374151")

    base = getSampleStyleSheet()["Normal"]
    body = ParagraphStyle("Body", parent=base, fontName="Times-Roman", fontSize=10, leading=14)
    styles = {
        "PRIMARY": PRIMARY,
        "GRAY": GRAY,
        "TEXT": TEXT,
        "body": body,
        "heading": ParagraphStyle(
            "SectionHeading", parent=body, fontName="Times-Bold", fontSize=11,
            spaceBefore=12, spaceAfter=4, textColor=colors.HexColor("#2563eb"),
        ),
        "small": ParagraphStyle("Small", parent=body, fontSize=8, textColor=GRAY),
        "crest": ParagraphStyle(
            "Crest", parent=body, fontSize=12, fontName="Times-Bold",
            textColor=colors.white, alignment=1,
        ),
        "letterhead_title": ParagraphStyle(
            "LetterheadTitle", parent=body, fontSize=16, fontName="Times-Bold",
            textColor=PRIMARY, alignment=1, spaceAfter=2, leading=19,
        ),
        "tagline": ParagraphStyle(
            "Tagline", parent=body, fontName="Times-Italic", fontSize=10,
            textColor=GRAY, alignment=1,
        ),
    }
    return styles


def _build_letterhead(story, cemetery_name: str, tagline: str, styles) -> None:
    from reportlab.lib.units import inch
    from reportlab.platypus import Paragraph, Spacer, Table, TableStyle, HRFlowable

    crest_width, crest_height = 0.5 * inch, 0.38 * inch
    crest_table = Table(
        [[Paragraph(_xml_escape(_crest_initials(cemetery_name)), styles["crest"])]],
        colWidths=[crest_width], rowHeights=[crest_height],
    )
    crest_table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (0, 0), styles["PRIMARY"]),
        ("VALIGN", (0, 0), (0, 0), "MIDDLE"),
        ("ALIGN", (0, 0), (0, 0), "CENTER"),
        ("ROUNDEDCORNERS", [4, 4, 4, 4]),
    ]))
    crest_table.hAlign = "CENTER"
    story.append(crest_table)
    story.append(Spacer(1, 0.08 * inch))
    # cemetery_name/tagline are tenant-editable free text (account.organization_name,
    # public_site_name/location_tagline) — escape before Paragraph interpolates
    # ReportLab's mini-HTML markup subset, otherwise a stray '<'/'>' can crash
    # PDF generation or, worse, get interpreted as an <img src=...>/<a href=...>
    # tag (SSRF / crash — see build_contract_pdf_bytes docstring).
    story.append(Paragraph(_xml_escape(cemetery_name.upper()), styles["letterhead_title"]))
    story.append(Paragraph(_xml_escape(tagline), styles["tagline"]))
    story.append(Spacer(1, 0.1 * inch))
    story.append(HRFlowable(width="100%", thickness=2, color=styles["PRIMARY"]))
    story.append(Spacer(1, 0.15 * inch))


def _build_payment_details_section(
    story, contract: "Contract", account: Optional["Account"],
    bank_details: Optional[dict], styles,
) -> None:
    """Payment Details + Bank Details section. `bank_details`, when given,
    must be the decrypted dict returned by
    BankDetailsService.get_decrypted_for_tenant — the full account number is
    shown so the purchaser can actually complete an EFT/wire transfer from
    this PDF."""
    from reportlab.lib.units import inch
    from reportlab.platypus import Paragraph, Spacer

    body = styles["body"]
    cemetery_name = getattr(account, "organization_name", None) or "INDELIS Cemetery"
    cemetery_email = getattr(account, "contact_email", None) or "—"
    cemetery_phone = getattr(account, "contact_phone", None) or "—"

    if bank_details is not None:
        story.append(Paragraph(
            "To complete your payment, please use the bank details below to "
            "transfer the outstanding amount.",
            body,
        ))
        story.append(Spacer(1, 0.05 * inch))

    story.append(Paragraph("Payment Details", styles["heading"]))
    story.append(Paragraph(f"Cemetery: {_xml_escape(cemetery_name)}", body))
    story.append(Paragraph(f"Amount: CAD ${contract.total_amount:,.2f}", body))
    story.append(Paragraph(
        f"Reference: Order #{_xml_escape(contract.contract_number or '—')}", body,
    ))
    story.append(Spacer(1, 0.1 * inch))

    if bank_details is None:
        story.append(Paragraph(
            "Bank details have not yet been configured for this cemetery. "
            f"Please contact {_xml_escape(cemetery_email)} or "
            f"{_xml_escape(cemetery_phone)} for payment instructions.",
            body,
        ))
        return

    # bank_details[...] (account_holder_name, bank_name in particular) is
    # free text a tenant admin enters in Settings — MUST be escaped before
    # Paragraph interpolation, same reasoning as the letterhead above. This
    # was previously unescaped and was the confirmed SSRF/crash vector
    # (an <img src="http://..."/> or unclosed tag in bank_name).
    story.append(Paragraph("Bank Details", styles["heading"]))
    story.append(Paragraph(
        f"Account Holder: {_xml_escape(bank_details['account_holder_name'])}", body,
    ))
    story.append(Paragraph(f"Bank: {_xml_escape(bank_details['bank_name'])}", body))
    story.append(Paragraph(
        f"Institution Number: {_xml_escape(bank_details['institution_number'])}", body,
    ))
    story.append(Paragraph(
        f"Transit Number: {_xml_escape(bank_details['transit_number'])}", body,
    ))
    story.append(Paragraph(
        f"Account Number: {_xml_escape(bank_details['account_number'])}", body,
    ))
    story.append(Paragraph(
        f"Account Type: {_xml_escape(bank_details['account_type'].title())}", body,
    ))


def build_bank_payment_email_text(
    amount: Optional[Decimal], reference: Optional[str],
    account: Optional["Account"], bank_details: Optional[dict],
) -> str:
    """Full Payment Details + Bank Details text block for a transactional
    email's {{bank_details_block}} merge field (contract_signed,
    invoice_overdue_reminder).

    `bank_details`, when given, must be the decrypted dict returned by
    BankDetailsService.get_decrypted_for_tenant — the purchaser needs the
    real account number to actually complete an EFT/wire transfer.
    """
    cemetery_name = getattr(account, "organization_name", None) or "INDELIS Cemetery"
    cemetery_email = getattr(account, "contact_email", None) or "—"
    cemetery_phone = getattr(account, "contact_phone", None) or "—"

    instruction = (
        "To complete your payment, please use the bank details below to "
        "transfer the outstanding amount.\n\n"
        if bank_details else ""
    )

    amount_text = f"CAD ${amount:,.2f}" if amount is not None else "—"
    header = (
        f"{instruction}"
        "Payment Details\n"
        f"Cemetery: {cemetery_name}\n"
        f"Amount: {amount_text}\n"
        f"Reference: Order #{reference or '—'}\n"
    )

    if not bank_details:
        return (
            f"{header}\n"
            "Bank details have not yet been configured for this cemetery. "
            f"Please contact {cemetery_email} or {cemetery_phone} for payment "
            "instructions."
        )

    return (
        f"{header}\n"
        "Bank Details\n"
        f"Account Holder: {bank_details['account_holder_name']}\n"
        f"Bank: {bank_details['bank_name']}\n"
        f"Institution Number: {bank_details['institution_number']}\n"
        f"Transit Number: {bank_details['transit_number']}\n"
        f"Account Number: {bank_details['account_number']}\n"
        f"Account Type: {bank_details['account_type'].title()}\n"
        f"Branch Address: {bank_details['branch_address']}"
    )


def build_contract_pdf_bytes(
    contract: "Contract",
    account: Optional["Account"] = None,
    accent_color_hex: Optional[str] = None,
    plot_label: Optional[str] = None,
    bank_details: Optional[dict] = None,
) -> bytes:
    """Build the simple Cemetery Plot Purchase Agreement PDF — deliberately
    matches the wizard's "Review & sign" preview exactly (letterhead, legal
    intro, Purchaser/Plot/Total lines, blank signature boxes) rather than a
    full billing breakdown, which now lives in its own Invoice PDF
    (see build_contract_invoice_pdf_bytes)."""
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import letter
    from reportlab.lib.styles import ParagraphStyle
    from reportlab.lib.units import inch
    from reportlab.platypus import (
        SimpleDocTemplate,
        Paragraph,
        Spacer,
        Table,
        TableStyle,
        Image as RLImage,
    )

    buf = BytesIO()
    doc = SimpleDocTemplate(
        buf,
        pagesize=letter,
        rightMargin=0.75 * inch,
        leftMargin=0.75 * inch,
        topMargin=0.75 * inch,
        bottomMargin=0.75 * inch,
    )

    styles = _base_styles(accent_color_hex)
    body = styles["body"]
    intro_style = ParagraphStyle(
        "Intro", parent=body, fontSize=9.5, textColor=styles["TEXT"], leading=13,
    )
    caption_style = ParagraphStyle(
        "SigCaption", parent=body, fontName="Times-Bold", fontSize=8,
        textColor=styles["GRAY"], alignment=1,
    )

    story = []
    cemetery_name = getattr(account, "organization_name", None) or "INDELIS Cemetery"
    _build_letterhead(story, cemetery_name, "Cemetery Plot Purchase Agreement", styles)

    story.append(Paragraph(
        f'This agreement is entered into between {_xml_escape(cemetery_name)} '
        '(the "Cemetery") and the purchaser named below for the right of '
        "interment described herein, subject to applicable provincial and "
        "municipal cemetery regulations.",
        intro_style,
    ))
    story.append(Spacer(1, 0.18 * inch))

    # purchaser_name / plot_label are user-supplied free text — escape the
    # value while keeping the deliberate <b> markup around it (SEC: this pair
    # was flagged as unescaped alongside the bank-details fields above).
    story.append(Paragraph(
        f"Purchaser: <b>{_xml_escape(contract.purchaser_name or '—')}</b>", body,
    ))
    story.append(Paragraph(f"Plot: <b>{_xml_escape(plot_label or '—')}</b>", body))
    story.append(Paragraph(
        f"Total payable: <b>${contract.total_amount:,.2f} CAD</b>", body,
    ))
    story.append(Spacer(1, 0.2 * inch))

    _build_payment_details_section(story, contract, account, bank_details, styles)
    story.append(Spacer(1, 0.2 * inch))

    # Signatures — two boxes side by side, blank (a surface to sign on, not
    # a data display); names print as plain text below instead.
    box_width = 3.15 * inch
    box_height = 0.9 * inch

    purchaser_cell = ""
    if contract.purchaser_signature_b64:
        try:
            sig_b64 = contract.purchaser_signature_b64
            if "," in sig_b64:
                sig_b64 = sig_b64.split(",", 1)[1]
            sig_bytes = base64.b64decode(sig_b64)
            purchaser_cell = RLImage(BytesIO(sig_bytes), width=2.3 * inch, height=0.65 * inch)
        except Exception:
            purchaser_cell = ""

    purchaser_box = Table([[purchaser_cell]], colWidths=[box_width], rowHeights=[box_height])
    purchaser_box.setStyle(TableStyle([
        ("VALIGN", (0, 0), (0, 0), "MIDDLE"),
        ("ALIGN", (0, 0), (0, 0), "CENTER"),
        ("BACKGROUND", (0, 0), (0, 0), colors.HexColor("#fafafa")),
        ("BOX", (0, 0), (0, 0), 0.75, colors.HexColor("#d1d5db")),
        ("ROUNDEDCORNERS", [8, 8, 8, 8]),
    ]))

    witness_box = Table([[""]], colWidths=[box_width], rowHeights=[box_height])
    witness_box.setStyle(TableStyle([
        ("VALIGN", (0, 0), (0, 0), "MIDDLE"),
        ("ALIGN", (0, 0), (0, 0), "CENTER"),
        ("BACKGROUND", (0, 0), (0, 0), colors.HexColor("#f0fdf4")),
        ("BOX", (0, 0), (0, 0), 0.75, colors.HexColor("#bbf7d0")),
        ("ROUNDEDCORNERS", [8, 8, 8, 8]),
    ]))

    sig_layout = Table(
        [[purchaser_box, witness_box],
         [Paragraph("PURCHASER SIGNATURE", caption_style),
          Paragraph("WITNESS — CEMETERY ADMINISTRATOR", caption_style)]],
        colWidths=[box_width + 0.2 * inch, box_width + 0.2 * inch],
    )
    sig_layout.setStyle(TableStyle([
        ("LEFTPADDING", (0, 0), (-1, -1), 0.1 * inch),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0.1 * inch),
        ("TOPPADDING", (0, 1), (-1, 1), 6),
        ("VALIGN", (0, 0), (-1, 0), "TOP"),
    ]))
    story.append(sig_layout)
    story.append(Spacer(1, 0.2 * inch))

    story.append(Paragraph(
        f"<b>Purchaser Name:</b> {_xml_escape(contract.purchaser_name or '—')}", body,
    ))
    story.append(Spacer(1, 0.08 * inch))
    story.append(Paragraph(
        f"<b>Witness Name:</b> {_xml_escape(contract.witness_name or '—')}", body,
    ))
    story.append(Spacer(1, 0.2 * inch))
    story.append(Paragraph(
        "This document constitutes a legally binding agreement between the purchaser "
        f"and {_xml_escape(cemetery_name)} upon execution by both parties.",
        styles["small"],
    ))

    doc.build(story)
    return buf.getvalue()


def build_contract_invoice_pdf_bytes(
    contract: "Contract",
    invoice: "Invoice",
    account: Optional["Account"] = None,
    accent_color_hex: Optional[str] = None,
) -> bytes:
    """Build the detailed billing document for one of a contract's
    invoices — header, full purchaser info, itemized line items — kept
    separate from the simple agreement PDF (build_contract_pdf_bytes)."""
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import letter
    from reportlab.lib.units import inch
    from reportlab.platypus import (
        SimpleDocTemplate,
        Paragraph,
        Spacer,
        Table,
        TableStyle,
    )

    buf = BytesIO()
    doc = SimpleDocTemplate(
        buf,
        pagesize=letter,
        rightMargin=0.75 * inch,
        leftMargin=0.75 * inch,
        topMargin=0.75 * inch,
        bottomMargin=0.75 * inch,
    )

    styles = _base_styles(accent_color_hex)
    heading = styles["heading"]

    story = []
    cemetery_name = getattr(account, "organization_name", None) or "INDELIS Cemetery"
    _build_letterhead(story, cemetery_name, "Invoice", styles)

    meta_data = [
        ["Invoice Number:", invoice.invoice_number or "—"],
        ["Contract Number:", contract.contract_number or "—"],
        ["Due Date:", invoice.due_date.strftime("%B %d, %Y") if invoice.due_date else "—"],
        ["Status:", (invoice.status or "—").upper()],
    ]
    meta_table = Table(meta_data, colWidths=[2 * inch, 4 * inch])
    meta_table.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "Times-Roman"),
        ("FONTNAME", (0, 0), (0, -1), "Times-Bold"),
        ("FONTSIZE", (0, 0), (-1, -1), 10),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
        ("TEXTCOLOR", (0, 0), (0, -1), styles["TEXT"]),
    ]))
    story.append(meta_table)
    story.append(Spacer(1, 0.2 * inch))

    story.append(Paragraph("Purchaser Information", heading))
    purchaser_data = [
        ["Name:", contract.purchaser_name or invoice.purchaser_name or "—"],
        ["Email:", contract.purchaser_email or invoice.purchaser_email or "—"],
        ["Phone:", contract.purchaser_phone or "—"],
        ["Address:", contract.purchaser_address or "—"],
    ]
    purchaser_table = Table(purchaser_data, colWidths=[1.5 * inch, 5 * inch])
    purchaser_table.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "Times-Roman"),
        ("FONTNAME", (0, 0), (0, -1), "Times-Bold"),
        ("FONTSIZE", (0, 0), (-1, -1), 10),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
    ]))
    story.append(purchaser_table)
    story.append(Spacer(1, 0.2 * inch))

    story.append(Paragraph("Items", heading))
    line_items = contract.line_items or []
    li_headers = [["Description", "Qty", "Unit Price", "Subtotal"]]
    li_rows = []
    subtotal = Decimal("0")
    for item in line_items:
        unit = Decimal(str(item.unit_price))
        qty = item.quantity
        total = Decimal(str(item.line_total))
        subtotal += total
        li_rows.append([item.description, str(qty), f"${unit:,.2f}", f"${total:,.2f}"])

    if not li_rows:
        li_rows = [["No line items", "", "", ""]]

    hst_rate = Decimal("0.13")
    hst_amount = (subtotal * hst_rate).quantize(Decimal("0.01"))
    contract_total = subtotal + hst_amount
    # invoice.total_amount is what's actually owed on THIS invoice — for a
    # deposit_50 payment plan it's half of contract_total, not the full
    # contract value, so it must drive the amount shown on the document
    # rather than the recomputed contract_total (which previously overstated
    # split invoices by always showing the full contract's grand total).
    invoice_total = Decimal(str(invoice.total_amount))
    invoice_balance = Decimal(str(invoice.balance_due))
    summary_rows = [
        ["", "", "Subtotal:", f"${subtotal:,.2f}"],
        ["", "", "HST (13%):", f"${hst_amount:,.2f}"],
        ["", "", "Contract total:", f"${contract_total:,.2f}"],
        ["", "", "This invoice:", f"${invoice_total:,.2f}"],
        ["", "", "Balance due:", f"${invoice_balance:,.2f}"],
    ]

    li_col_widths = [3.5 * inch, 0.5 * inch, 1.25 * inch, 1.25 * inch]
    li_table = Table(li_headers + li_rows + summary_rows, colWidths=li_col_widths)
    li_table.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "Times-Roman"),
        ("BACKGROUND", (0, 0), (-1, 0), styles["PRIMARY"]),
        ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
        ("FONTNAME", (0, 0), (-1, 0), "Times-Bold"),
        ("FONTSIZE", (0, 0), (-1, 0), 10),
        ("ALIGN", (1, 0), (-1, 0), "RIGHT"),
        ("FONTSIZE", (0, 1), (-1, -1), 9),
        ("ROWBACKGROUNDS", (0, 1), (-1, len(li_rows)), [colors.white, colors.HexColor("#f9fafb")]),
        ("ALIGN", (1, 1), (-1, -1), "RIGHT"),
        ("GRID", (0, 0), (-1, len(li_rows)), 0.5, colors.HexColor("#e5e7eb")),
        ("FONTNAME", (2, len(li_rows) + 1), (2, -1), "Times-Bold"),
        ("LINEABOVE", (2, len(li_rows) + 1), (-1, len(li_rows) + 1), 1, colors.HexColor("#d1d5db")),
        ("FONTNAME", (2, -1), (-1, -1), "Times-Bold"),
        ("FONTSIZE", (2, -1), (-1, -1), 10),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
        ("TOPPADDING", (0, 0), (-1, -1), 6),
        ("LEFTPADDING", (0, 0), (-1, -1), 6),
        ("RIGHTPADDING", (0, 0), (-1, -1), 6),
    ]))
    story.append(li_table)
    story.append(Spacer(1, 0.2 * inch))

    story.append(Paragraph(
        "All amounts in Canadian Dollars (CAD). HST included where applicable.",
        styles["small"],
    ))

    doc.build(story)
    return buf.getvalue()
