#!/usr/bin/env python3
"""Check behind /blog/client-statement-workpapers.

1. Writes one synthetic checking statement with make_pdfs.py, through
   `uv run --with pikepdf==10.17.0`: once as printed (it ties out) and once with the 09/14 row left off
   while every printed balance stays (it does not tie out).
2. Reads each the way Evidilo's upload and conversion do (pagesOf, readStatementPeriod,
   readStatementAccount, readStatement with no model adapter, the rules lane a page with a text layer
   takes) and writes all seven formats with the production exportByFormat, through
   export-files.harness.ts and vitest, with harness.vitest.config.mjs.
3. Reads each file for what it keeps of the statement: rows, the sum of their amounts, the account
   number, the period, the opening and closing balance, a running balance, and any word that says
   whether the statement tied out.
4. Re-reads the two IRS recordkeeping pages and confirms the quoted sentences are still there.

Exit 0: every result matches dataset.csv and every quote is found. Exit 1: a result differs or a
quote is gone. Exit 2: a cited page could not be reached at all (nothing else failed).
`--recheck` runs the same steps (none takes over a minute). `--write` rewrites dataset.csv.
"""

import csv
import html
import json
import os
import re
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
from decimal import Decimal
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parents[4]  # apps/web/public/blog/<slug> -> repository root
PIKEPDF = "pikepdf==10.17.0"
UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/130.0 Safari/537.36"
)
QUOTES = {
    "https://www.irs.gov/businesses/small-businesses-self-employed/how-long-should-i-keep-records": [
        "Keep records for 3 years if situations (4), (5), and (6) below do not apply to you.",
        "Keep records for 7 years if you file a claim for a loss from worthless securities or bad debt deduction.",
        "For example, your insurance company or creditors may require you to keep them longer than the IRS does.",
    ],
    "https://www.irs.gov/businesses/small-businesses-self-employed/what-kind-of-records-should-i-keep": [
        "It is important to keep these documents because they support the entries in your books and on your tax return.",
        "Cash register tape receipts Account statements",
    ],
}
FORMATS = ["csv", "xlsx", "qbo", "qfx", "ofx", "qif", "iif"]
FIELDS = [
    "case", "statement_check", "format", "file_name", "rows_in_file", "sum_of_rows",
    "account_in_file", "period_in_file", "opening_in_file", "closing_in_file",
    "running_balance_in_file", "tie_out_word_in_file",
]
STATUS_WORDS = re.compile(r"tie|tied|balanced|review|mismatch|difference|does not", re.I)


def amounts(fmt, text):
    if fmt == "csv":
        return [line.rsplit(",", 1)[1] for line in text.strip().splitlines()[1:]]
    if fmt == "xlsx":
        return re.findall(r'<c r="C\d+" s="1"><v>([^<]+)</v>', text)
    if fmt in ("qbo", "qfx", "ofx"):
        return re.findall(r"<TRNAMT>([^\s<]+)", text)
    if fmt == "qif":
        return [line[1:] for line in text.splitlines() if line.startswith("T")]
    if fmt == "iif":
        return [line.split("\t")[5] for line in text.splitlines() if line.startswith("TRNS\t")]
    raise ValueError(fmt)


def figure(text, value):
    """Whether the figure is in the file in any of the ways the formats write a number."""
    plain = value.replace(",", "")
    short = f"{Decimal(plain).normalize():f}"
    return any(re.search(rf"(?<![\d.]){re.escape(v)}(?![\d])", text) for v in {plain, short})


def account(text, number):
    if number in text:
        return "full number"
    if number[-4:] in text:
        return "last four"
    return "none"


def period(text):
    has = lambda *forms: any(f in text for f in forms)
    start = has("20260901", "09/01/2026", "2026-09-01")
    end = has("20260930", "09/30/2026", "2026-09-30")
    return "start and end" if start and end else ("end only" if end else ("start only" if start else "none"))


def measure():
    with tempfile.TemporaryDirectory() as tmp:
        pdfs = Path(tmp) / "pdfs"
        subprocess.run(
            ["uv", "run", "-q", "--with", PIKEPDF, "python3", str(HERE / "make_pdfs.py"), str(pdfs)],
            check=True, stdout=subprocess.DEVNULL,
        )
        out = Path(tmp) / "files.json"
        env = dict(os.environ, PAGES_DIR=str(pdfs), PAGES_OUT=str(out))
        subprocess.run(
            ["npx", "vitest", "run", "--config", str(HERE / "harness.vitest.config.mjs")],
            cwd=REPO, env=env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
        read = json.loads(out.read_text())["cases"]
        printed = json.loads((pdfs / "cases.json").read_text())

    acct = printed["account"]
    number = acct["number"].replace(" ", "")
    balances = [r[3] for r in acct["rows"]]
    rows = []
    for case in read:
        continuity = case["balanceContinuity"] or {}
        check = continuity.get("status", "not run")
        difference = (continuity.get("difference") or {}).get("minorUnits")
        if difference is not None:
            check += f", off by {int(difference) / 100:.2f} from row {continuity.get('firstBrokenRow')}"
        for fmt in FORMATS:
            file = case["files"][fmt]
            base = {f: "" for f in FIELDS}
            base.update(case=case["id"], statement_check=check, format=fmt)
            if "refused" in file:
                rows.append({**base, "file_name": "refused: " + file["refused"]})
                continue
            text = file["text"]
            found = amounts(fmt, text)
            rows.append({
                **base,
                "file_name": file["fileName"],
                "rows_in_file": str(len(found)),
                "sum_of_rows": f"{sum(Decimal(a) for a in found):.2f}",
                "account_in_file": account(text, number),
                "period_in_file": period(text),
                "opening_in_file": "yes" if figure(text, acct["opening"]) else "no",
                "closing_in_file": "yes" if figure(text, acct["closing"]) else "no",
                "running_balance_in_file": "yes" if any(figure(text, b) for b in balances[:-1]) else "no",
                "tie_out_word_in_file": "yes" if STATUS_WORDS.search(re.sub(r"<[^>]+>", " ", text) if fmt == "xlsx" else text) else "no",
            })
    return rows


def page_text(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=30) as resp:
        raw = resp.read().decode("utf-8", "replace")
    raw = re.sub(r"<script.*?</script>|<style.*?</style>", " ", raw, flags=re.S)
    text = html.unescape(re.sub(r"<[^>]+>", " ", raw))
    return re.sub(r"\s+", " ", text).replace("’", "'")


def check_quotes():
    """0 when every quote is found, 1 when one is gone, 2 when a page could not be read."""
    status = 0
    for url, quotes in QUOTES.items():
        try:
            text = page_text(url)
        except (urllib.error.URLError, TimeoutError) as error:
            print(f"UNREACHABLE {url}: {error}")
            status = max(status, 2)
            continue
        for quote in quotes:
            if quote not in text:
                print(f"FAIL quote gone from {url}: {quote!r}")
                status = 1
    return status


def main():
    rows = measure()
    target = HERE / "dataset.csv"
    if "--write" in sys.argv:
        with target.open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=FIELDS)
            w.writeheader()
            w.writerows(rows)

    failed = False
    with target.open(newline="") as f:
        stored = list(csv.DictReader(f))
    if stored != rows:
        failed = True
        key = lambda r: (r["case"], r["format"])
        stored_by = {key(r): r for r in stored}
        for row in rows:
            if stored_by.get(key(row)) != row:
                print(f"FAIL {key(row)}: now {row}, stored {stored_by.get(key(row))}")
        if len(stored) != len(rows):
            print(f"FAIL {len(rows)} rows measured, {len(stored)} stored")

    quotes = check_quotes()
    if failed or quotes == 1:
        return 1
    if quotes == 2:
        return 2
    print(f"ok: {len(rows)} files match dataset.csv, every quote found")
    return 0


if __name__ == "__main__":
    sys.exit(main())
