"""Creates an electronic invoice and asks the document what stands in its way.

The Python twin of the `write_checked_invoice` example in Rust: the same
document, through the binding rather than through the library directly.

Every other example reaches a standard by declaring it. This one asks first.
`Document.check` reads the document as it stands, writes nothing, changes
nothing, and hands back everything known here to stand between it and the
standard named — all of it at once, each finding citing the clause it comes from.

The invoice is composed the way a caller first writes it, with three things wrong
that only a report can name: it claims no archival standard, it names an invoice
it never attached, and it claims a profile the XML does not carry. The first two
hide the third, because a file that is not there cannot be read; so each round
prints what came back, puts it right, and asks again. Nothing is written while
anything stands.

An empty report is not a certificate. It says that nothing this library knows to
look for is in the way, which is not the same as conforming: `veraPDF` says
whether the file conforms, and `scripts/check_pdfa.sh` asks it.

The page is written in the language `HQF_PDF_LANG` names. The XML is not: it is
read by an accounting system rather than by a person, and the same file goes to a
buyer in any country. What the terminal says is not either — it is the library's
own report, in the words the library states its clauses in.

Usage: python examples/write_checked_invoice.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_checked_invoice.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# When the invoice was issued. The library never reads the clock — a document that
# stamped itself with the time would be a different file on every build — so the caller
# says when, and here the caller is an example.
ISSUED = "2026-07-14T09:30:00+02:00"

# The part of the archival standard an electronic invoice belongs in.
CONTAINER = hqf_pdf.PdfA.A3B

# The profile the caller claims before reading the XML.
CLAIMED_AT_FIRST = hqf_pdf.InvoiceProfile.En16931

# The profile the XML states under `GuidelineSpecifiedDocumentContextParameter`.
CARRIED = hqf_pdf.InvoiceProfile.Basic

# The version of the Factur-X specification the XML follows.
SPECIFICATION = "1.0"

# The name the standard has reading software look the XML up by, and the name
# `Attachment.invoice` carries it under.
INVOICE_FILE_NAME = "factur-x.xml"

# The invoice, as a machine reads it: a Factur-X BASIC profile document, which carries
# the billed lines as well as the header. A real invoice carries more, and carries it
# under the same rules.
INVOICE_XML = """<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
    xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
    xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
    xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
  <rsm:ExchangedDocumentContext>
    <ram:GuidelineSpecifiedDocumentContextParameter>
      <ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic</ram:ID>
    </ram:GuidelineSpecifiedDocumentContextParameter>
  </rsm:ExchangedDocumentContext>
  <rsm:ExchangedDocument>
    <ram:ID>2026-0207</ram:ID>
    <ram:TypeCode>380</ram:TypeCode>
    <ram:IssueDateTime>
      <udt:DateTimeString format="102">20260714</udt:DateTimeString>
    </ram:IssueDateTime>
  </rsm:ExchangedDocument>
  <rsm:SupplyChainTradeTransaction>
    <ram:IncludedSupplyChainTradeLineItem>
      <ram:AssociatedDocumentLineDocument>
        <ram:LineID>1</ram:LineID>
      </ram:AssociatedDocumentLineDocument>
      <ram:SpecifiedTradeProduct>
        <ram:Name>Rendering engine licence</ram:Name>
      </ram:SpecifiedTradeProduct>
      <ram:SpecifiedLineTradeAgreement>
        <ram:NetPriceProductTradePrice>
          <ram:ChargeAmount>12400.00</ram:ChargeAmount>
        </ram:NetPriceProductTradePrice>
      </ram:SpecifiedLineTradeAgreement>
      <ram:SpecifiedLineTradeDelivery>
        <ram:BilledQuantity unitCode="C62">1</ram:BilledQuantity>
      </ram:SpecifiedLineTradeDelivery>
      <ram:SpecifiedLineTradeSettlement>
        <ram:ApplicableTradeTax>
          <ram:TypeCode>VAT</ram:TypeCode>
          <ram:CategoryCode>S</ram:CategoryCode>
          <ram:RateApplicablePercent>20.00</ram:RateApplicablePercent>
        </ram:ApplicableTradeTax>
        <ram:SpecifiedTradeSettlementLineMonetarySummation>
          <ram:LineTotalAmount>12400.00</ram:LineTotalAmount>
        </ram:SpecifiedTradeSettlementLineMonetarySummation>
      </ram:SpecifiedLineTradeSettlement>
    </ram:IncludedSupplyChainTradeLineItem>
    <ram:ApplicableHeaderTradeAgreement>
      <ram:SellerTradeParty>
        <ram:Name>HQF Development</ram:Name>
        <ram:SpecifiedLegalOrganization>
          <ram:ID schemeID="0002">752492777</ram:ID>
        </ram:SpecifiedLegalOrganization>
        <ram:PostalTradeAddress>
          <ram:CountryID>FR</ram:CountryID>
        </ram:PostalTradeAddress>
      </ram:SellerTradeParty>
      <ram:BuyerTradeParty>
        <ram:Name>Northwind Trading SARL</ram:Name>
      </ram:BuyerTradeParty>
    </ram:ApplicableHeaderTradeAgreement>
    <ram:ApplicableHeaderTradeDelivery/>
    <ram:ApplicableHeaderTradeSettlement>
      <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
      <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
        <ram:TaxBasisTotalAmount>12400.00</ram:TaxBasisTotalAmount>
        <ram:TaxTotalAmount currencyID="EUR">2480.00</ram:TaxTotalAmount>
        <ram:GrandTotalAmount>14880.00</ram:GrandTotalAmount>
        <ram:DuePayableAmount>14880.00</ram:DuePayableAmount>
      </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
    </ram:ApplicableHeaderTradeSettlement>
  </rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
"""

# The invoice's number, which the page shows and the XML carries under `ram:ID`. One
# number, written once on each side.
NUMBER = "2026-0207"

# Who issued the invoice, on the page and in the XML both.
SELLER = "HQF Development"

# What each of the three billed lines comes to, in the order they are set. These are the
# figures the XML totals, so they read the same wherever the page is read.
AMOUNTS = ["12 400.00 EUR", "2 480.00 EUR", "14 880.00 EUR"]

# How many characters a billed line runs to: its label, a space, the leader dots, a
# space, then its amount. The dots take whatever is left, so the amounts end on the same
# character however long the label before them is.
LINE_CHARS = 58

# The size each line of the page is set at, in points, in the order they are drawn.
SIZES = [16.0, 10.0, 11.0, 11.0, 11.0, 12.0]

# How far in from the left edge of the sheet every line is set, in points.
LEFT = 72.0


@dataclass(frozen=True)
class Words:
    """Every word the page is written in, in one language.

    What is not language stays out of it: the invoice's number, the seller's name
    and the three amounts read the same wherever the page is read, and so does
    every word of the XML.
    """

    # What stands before the invoice's number, at the head of the page and in the
    # document's own title.
    number_label: str
    # What the file says it holds.
    subject: str
    # What stands before the seller's name.
    issued_by: str
    # The line that gives the day it was issued and the term it falls due in.
    dates: str
    # What each billed line is called, in the order `AMOUNTS` prices them.
    items: tuple[str, str, str]

    def title(self) -> str:
        """What the page is headed by, which is also what the file says of itself."""
        return f"{self.number_label} {NUMBER}"

    def lines(self) -> list[str]:
        """The lines the page shows, which say what the XML says."""
        lines = [self.title(), f"{self.issued_by} {SELLER}", self.dates]
        for item, amount in zip(self.items, AMOUNTS):
            dots = max(LINE_CHARS - len(item) - len(amount) - 2, 0)
            lines.append(f"{item} {'.' * dots} {amount}")
        return lines


# The page in English.
ENGLISH = Words(
    number_label="Invoice No.",
    subject="An electronic invoice, measured before it was claimed",
    issued_by="Issued by",
    dates="Issued 14 July 2026 — due within 30 days",
    items=("Rendering engine licence", "VAT at 20 %", "Total due"),
)

# The page in French.
FRENCH = Words(
    number_label="Facture n°",
    subject="Une facture électronique, mesurée avant d'être revendiquée",
    issued_by="Émise par",
    dates="Émise le 14 juillet 2026 — payable sous 30 jours",
    items=("Licence du moteur de rendu", "TVA 20 %", "Total à payer"),
)

# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}


def facts(words: Words, profile: hqf_pdf.InvoiceProfile) -> hqf_pdf.Metadata:
    """The facts the document states about itself, naming the invoice it carries
    and claiming ``profile`` for it."""
    return hqf_pdf.Metadata(
        title=words.title(),
        author=SELLER,
        subject=words.subject,
        producer="hqf-pdf",
        created=ISSUED,
        invoice=hqf_pdf.Invoice(INVOICE_FILE_NAME, profile, SPECIFICATION),
    )


def invoice_page(handle: hqf_pdf.FontHandle, words: Words) -> hqf_pdf.Page:
    """The page a person reads, saying what the attached XML says."""
    content = hqf_pdf.Content()

    top = 760.0
    for line, size in zip(words.lines(), SIZES):
        content.draw_text(handle, size, LEFT, top, line)
        top -= size * 2.0

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    return page


def print_report(asked: str, report: hqf_pdf.Report) -> None:
    """Print what ``report`` holds: the clause each finding cites, what it says,
    and the one thing it is about where it names one."""
    if report.is_empty():
        print(f"{asked}: nothing known here stands in the way")
        return
    print(f"{asked}: {len(report)} in the way")
    for finding in report.findings:
        if finding.subject is None:
            print(f"  {finding.clause}: {finding.what}")
        else:
            print(f"  {finding.clause}: {finding.what} — {finding.subject}")


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(
        Path(_language.file_name("checked_invoice.pdf", language)).stem
    )

    # The profile the document claims, which is what it is measured against: a report
    # answers the claim that was made, not the one that should have been.
    claimed = CLAIMED_AT_FIRST

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_metadata(facts(words, claimed))

    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
    document.add_page(invoice_page(handle, words))

    report = document.check(hqf_pdf.Standard.invoice(claimed))
    print_report("as first composed", report)

    # An invoice belongs in a document claiming the third part of the archival standard,
    # and the file it names is one it carries. What the attachment *is* to the document
    # has no sensible default: `Data` is right for the profiles that carry part of the
    # invoice, and `Alternative` for those that carry all of it. This one is BASIC,
    # which carries part.
    document.set_conformance(CONTAINER)
    document.attach(
        hqf_pdf.Attachment.invoice(
            INVOICE_XML.encode(), hqf_pdf.Relationship.Data, ISSUED
        )
    )

    report = document.check(hqf_pdf.Standard.invoice(claimed))
    print_report("once the file is carried and the standard claimed", report)

    # The XML states the profile it keeps to, and the document says what the XML states.
    claimed = CARRIED
    document.set_metadata(facts(words, claimed))

    report = document.check(hqf_pdf.Standard.invoice(claimed))
    print_report("once the profile is the one the XML carries", report)

    # The archival standard is a second claim, and a claim nobody asked about is a claim
    # nobody measured.
    archival = document.check(hqf_pdf.Standard.archival(CONTAINER))
    print_report("against the archival standard now claimed", archival)

    if not report.is_empty() or not archival.is_empty():
        raise SystemExit("something still stands in the way, and nothing is written")

    written = document.write(out)
    print(f"wrote {out} ({written} bytes)")


if __name__ == "__main__":
    main()
