"""Starter: the capstone's shape, reduced to one file, on real files.

Practical Workflow Automation, modules 3 to 6. Reads a CSV of orders,
validates each row, writes a digest, writes a run record, and resumes from
that record if a previous run stopped part-way.

    py starter_daily_digest.py --folder .                      dry run (default)
    py starter_daily_digest.py --folder . --apply              write the digest
    py starter_daily_digest.py --folder . --apply --fail-after 3   stop part-way
    py starter_daily_digest.py --folder . --health             check for silence

Fill in the four functions marked "your code here". The completed version is
in complete_daily_digest.py; read it after you have attempted this.

It writes only inside the folder you name, it never deletes anything, and it
makes no network request.
"""

import argparse
import csv
import datetime
import json
import pathlib
import sys

REGIONS = {"NW": "north", "NE": "north", "SW": "south", "SE": "south"}
STALE_AFTER_MINUTES = 26 * 60


def read_orders(path):
    """Every row of the CSV, as dictionaries, in file order."""
    with path.open(newline="", encoding="utf-8") as handle:
        return list(csv.DictReader(handle))


def validate(row):
    """Return (record, None) or (None, reason) for one row.

    Quarantine for the first reason that applies, in this order:
      units that will not convert   units: <value> is not a whole number
      unknown region code           region_code: <code> is not a known region
      status other than confirmed   status: <status> orders are not counted

    An accepted record carries units as an int and a "group" from REGIONS.
    """
    # your code here
    raise NotImplementedError


def build_digest(records, counts, quarantined, source, run_at):
    """The digest lines, in a fixed order so two days can be compared.

    Order: a title line, a generated-from line, the counts, the reconciliation
    line, one line per reporting group sorted by name, then the quarantined
    entries, then the non-advice statement.

    Reconciled means every record read is accounted for:
      read == accepted + quarantined + pending
    skipped is not in that sum: a skipped order was processed by an earlier
    run and is already counted in accepted or quarantined, so including it
    would double-count it. A partial run still reconciles - "we stopped early"
    and "a record went missing" are different facts and must not share a line.
    """
    # your code here
    raise NotImplementedError


def resume_from(previous, rows):
    """Return (to_process, skipped) given the previous run's record, or None.

    Skip any order the previous record lists as processed, whatever that run's
    status was. Resumption is by record identity, never by position: tomorrow's
    file has new rows in it, so an index means a different order. A rerun over
    an unchanged file therefore has nothing to process and reproduces the same
    digest, which is what makes "just run it again" safe.
    """
    # your code here
    raise NotImplementedError


def health(records, now):
    """Return (status, message) from the run records: healthy, stale, or never succeeded.

    Only a record whose status is "completed" counts as a heartbeat.
    """
    # your code here
    raise NotImplementedError


def load_records(state_path):
    if not state_path.exists():
        return []
    return json.loads(state_path.read_text(encoding="utf-8"))["records"]


def write_atomically(path, text):
    """Write to a temporary name and rename, so an interrupted write leaves the old file."""
    temporary = path.with_suffix(path.suffix + ".tmp")
    temporary.write_text(text, encoding="utf-8")
    temporary.replace(path)


def main(argv=None):
    parser = argparse.ArgumentParser(description="Build a daily order digest.")
    parser.add_argument("--folder", required=True, help="the practice folder to work in")
    parser.add_argument("--orders", default="sample-orders.csv", help="the CSV to read")
    parser.add_argument("--apply", action="store_true", help="write the digest and the record")
    parser.add_argument("--fail-after", type=int, default=None, help="stop after N new orders")
    parser.add_argument("--health", action="store_true", help="report on past runs and exit")
    args = parser.parse_args(argv)

    folder = pathlib.Path(args.folder).resolve()
    state_path = folder / "state" / "run-records.json"
    now = datetime.datetime.now().replace(microsecond=0)

    if args.health:
        status, message = health(load_records(state_path), now)
        print(f"{status}: {message}")
        return 0 if status == "healthy" else 1

    orders_path = folder / args.orders
    if not orders_path.is_file():
        print(f"refused: required input {args.orders} is missing from {folder}")
        return 2

    rows = read_orders(orders_path)
    previous = (load_records(state_path) or [None])[-1]
    to_process, skipped = resume_from(previous, rows)

    handled_ids = list(previous.get("processed", [])) if previous else []
    new = 0
    for row in to_process:
        if args.fail_after is not None and new >= args.fail_after:
            break
        validate(row)  # a row is only "handled" once it has been decided
        # Recorded after the row was handled, and by id rather than by
        # position: tomorrow's file has new rows in it, so an index means a
        # different order.
        handled_ids.append(row["order_id"])
        new += 1

    # The digest is a statement about the day, not about this attempt, so it
    # is derived from every processed order rather than from the ones this run
    # handled. Validation is pure, so re-deciding a row costs nothing and
    # guarantees a resumed run reports the same totals as an uninterrupted one.
    processed_ids = set(handled_ids)
    accepted, quarantined = [], []
    for row in rows:
        if row["order_id"] not in processed_ids:
            continue
        record, reason = validate(row)
        if reason is None:
            accepted.append(record)
        else:
            quarantined.append((row["order_id"], reason))

    handled = len(processed_ids)
    status = "completed" if handled == len(rows) else "partial"
    counts = {
        "read": len(rows),
        "accepted": len(accepted),
        "quarantined": len(quarantined),
        "skipped": skipped,
        # Read but not attempted, because this run stopped early. Counting it
        # is what keeps a partial run's numbers reconciling: "we stopped" and
        # "a record went missing" are different facts and must not share a line.
        "pending": len(rows) - handled,
    }
    lines = build_digest(accepted, counts, quarantined, args.orders, now.isoformat())

    if not args.apply:
        print("dry run: would write these lines and change nothing.\n")
        print("\n".join(lines))
        return 0

    (folder / "output").mkdir(exist_ok=True)
    (folder / "state").mkdir(exist_ok=True)
    write_atomically(folder / "output" / "digest.txt", "\n".join(lines) + "\n")

    records = load_records(state_path)
    records.append(
        {
            "run_id": now.isoformat(),
            "status": status,
            "finished_at": now.isoformat(),
            "counts": counts,
            "processed": handled_ids,
            "reason": "" if status == "completed" else f"stopped after {new} new orders",
        }
    )
    write_atomically(state_path, json.dumps({"records": records}, indent=2))
    print(
        f"order-digest {status}: read {counts['read']}, accepted {counts['accepted']}, "
        f"quarantined {counts['quarantined']}, skipped {skipped}, pending {counts['pending']}"
    )
    return 0 if status == "completed" else 1


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