#!/usr/bin/env python3
"""Re-checks the counts behind "Shared Album Deleted, or Just Unreachable?".

The counts come from Viallo's production analytics and database (query.sql, query.js),
which this script cannot reach. What it can check, it checks:
  - dataset.csv and dataset.json carry the same values;
  - the parts add up to the totals and every percentage recomputes from the counts;
  - no published row with a count is built from fewer than 10 people;
  - the deleted album is still the largest dead-link state and expired is still zero;
  - with a Viallo checkout (VIALLO_REPO, default ~/Repositories/my/viallo), the headings
    and outcome calls the rows quote are still in the share page's source on origin/main.

Usage: check.py [--recheck]. --recheck runs the same steps: there is no lab step to skip
and no public page to re-read.

Exit status: 0 when everything holds, 1 otherwise.
"""
import csv
import json
import os
import re
import subprocess
import sys
import unicodedata

HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.environ.get("VIALLO_REPO", os.path.expanduser("~/Repositories/my/viallo"))
FLOOR = 10

failures = []
notes = []


def normalise(text):
    text = unicodedata.normalize("NFC", text).replace("’", "'").replace("‘", "'")
    return re.sub(r"\s+", " ", text).strip().lower()


def pct(part, whole):
    return round(part * 100.0 / whole, 2) if whole else 0.0


def source(path):
    proc = subprocess.run(["git", "-C", REPO, "show", "origin/main:" + path],
                          capture_output=True, text=True)
    return proc.stdout if proc.returncode == 0 else None


def main():
    with open(os.path.join(HERE, "dataset.json")) as fh:
        meta = json.load(fh)
    with open(os.path.join(HERE, "dataset.csv"), newline="") as fh:
        rows = {r["measure"]: r for r in csv.DictReader(fh)}

    opens = meta["results"]["opens"]
    links = meta["results"]["links"]

    for key, item in opens.items():
        row = rows.get("opens_" + key)
        if row is None:
            failures.append("opens_%s missing from dataset.csv" % key)
            continue
        if int(row["count"]) != item["value"] or int(row["people"]) != item["people"]:
            failures.append("opens_%s: csv and json disagree" % key)
    for key, item in links.items():
        row = rows.get("links_" + key)
        if row is not None and (int(row["count"]) != item["value"]
                                or int(row["people"]) != item["owners"]):
            failures.append("links_%s: csv and json disagree" % key)

    dead_parts = ["album_deleted", "other_dead_link", "expired"]
    dead = opens["dead_link_total"]["value"]
    total = opens["total"]["value"]
    if sum(opens[k]["value"] for k in dead_parts) != dead:
        failures.append("dead-link rows do not add up to %d" % dead)
    if dead + opens["redirected"]["value"] + opens["owner_view"]["value"] != total:
        failures.append("outcomes do not add up to %d opens" % total)

    for name, row in rows.items():
        count, people = int(row["count"]), int(row["people"])
        if count and people < FLOOR:
            failures.append("%s: %d published from %d people, under the floor of %d"
                            % (name, count, people, FLOOR))
        if row["share_of_opens_pct"] and name.startswith("opens_"):
            if abs(float(row["share_of_opens_pct"]) - pct(count, total)) > 0.005:
                failures.append("%s: share of opens %s, recomputed %.2f"
                                % (name, row["share_of_opens_pct"], pct(count, total)))
        if row["share_of_dead_link_screens_pct"]:
            if abs(float(row["share_of_dead_link_screens_pct"]) - pct(count, dead)) > 0.005:
                failures.append("%s: share of dead-link screens %s, recomputed %.2f"
                                % (name, row["share_of_dead_link_screens_pct"], pct(count, dead)))

    if max(dead_parts, key=lambda k: opens[k]["value"]) != "album_deleted":
        failures.append("album_deleted is no longer the largest dead-link state")
    if opens["expired"]["value"] != 0 or links["with_end_date"]["value"] != 0:
        failures.append("expired is no longer zero; the post says no link had an end date")

    quoted = [v for v in opens.values() if v.get("quote")]
    if os.path.isdir(REPO):
        cache = {}
        for item in quoted:
            path = item["source"]
            if path not in cache:
                cache[path] = source(path)
            if cache[path] is None:
                failures.append("cannot read %s from origin/main in %s" % (path, REPO))
            elif normalise(item["quote"]) not in normalise(cache[path]):
                failures.append("%s no longer contains %r" % (path, item["quote"]))
    else:
        notes.append("source quotes not checked: no Viallo checkout at %s" % REPO)

    print("dataset: %d opens, %d dead-link screens (%.2f%%), %d of them album_deleted (%.2f%%)"
          % (total, dead, pct(dead, total), opens["album_deleted"]["value"],
             pct(opens["album_deleted"]["value"], dead)))
    print("links: %d stored, %d turned off, %d with an end date"
          % (links["all"]["value"], links["turned_off"]["value"], links["with_end_date"]["value"]))
    for line in notes:
        print("note: " + line)
    for line in failures:
        print("FAIL: " + line)
    print("ok" if not failures else "%d failure(s)" % len(failures))
    return 1 if failures else 0


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