"""Guided capstone starter. Edit the FIVE named TODO stages below.
Read the bridge checkpoints in lesson 10 first. The ingestion helper is supplied.
Public checks run against YOUR clean_orders; no reference solution is imported.
"""
from io import StringIO
import csv
import pandas as pd
REQUIRED = ["order_id", "date", "category", "units", "unit_price_cents"]
CATEGORIES = {"notebooks", "pens", "markers"}
# Start at 1; move through 5 as each stage works. Set 6 for the full project.
CHECKPOINT_STAGE = 1
STAGE_CSV = 'order_id,date,category,units,unit_price_cents\nQ01,2026-06-01,pens,2,80\nQ01,2026-06-01,pens,2,80\n Q01 ,2026-06-01,pens,3,80\nQ02,bad-date,markers,unknown,150\nQ03,2026-06-03, NOTEBOOKS ,4,250\n'


def prepare_work(raw):
    """Stage 1. Return (work, duplicates, conflicts).
    Split exact extra copies BEFORE trimming IDs/categories. Preserve indexes.
    conflicts is a Boolean Series on work.index marking every repeated ID.
    Checkpoint: full-schema STAGE_CSV -> duplicate label [1], conflicts [0, 2].
    """
    raise NotImplementedError("Stage 1: split duplicates and flag conflicts")


def validate_work(work, conflicts):
    """Stage 2. Return (converted, reasons), preserving work.index.
    Copy work; check date/numeric source spelling before conversion, then check
    parsed calendar/range and numeric bounds. Include ID/category/conflict rules.
    reasons is a string Series: empty for valid rows, semicolon-separated names
    for failures. Preserve ALL failure reasons on a row. See bridge checkpoint 3.
    """
    raise NotImplementedError("Stage 2: create aligned validation reasons")


def split_evidence(raw, converted, reasons):
    """Stage 3. Return (accepted, rejected).
    Select accepted converted rows where reasons is empty. Select rejected rows
    from raw using the corresponding source labels, then attach their reasons.
    Convert accepted unit/price columns to int64; add revenue_cents.
    Sort accepted rows by date/order_id; only now may their index be reset.
    """
    raise NotImplementedError("Stage 3: retain original rejected evidence")


def summarize_accepted(accepted):
    """Stage 4. One row per category, columns category,units,revenue_cents.
    Sum numeric columns and sort categories. Empty accepted input stays empty.
    """
    raise NotImplementedError("Stage 4: compute category summary")


def make_audit(raw, duplicates, rejected, accepted):
    """Stage 5. Return a dict with raw_rows, duplicate_rows, rejected_rows,
    accepted_rows and accepted_revenue_cents. Sum cents with Python integers.
    Verify raw = duplicates + rejected + accepted, even for empty input.
    """
    raise NotImplementedError("Stage 5: reconcile counts and subtotal")


def clean_orders(csv_text):
    # The orchestration is supplied; the five data decisions above are yours.
    raw = read_order_records(csv_text)
    work, duplicates, conflicts = prepare_work(raw)
    converted, reasons = validate_work(work, conflicts)
    accepted, rejected = split_evidence(raw, converted, reasons)
    summary = summarize_accepted(accepted)
    audit = make_audit(raw, duplicates, rejected, accepted)
    return accepted, rejected, duplicates, summary, audit


def preview_stage(stage):
    """Supplied driver for the full-schema five-row checkpoint; no answers."""
    raw = read_order_records(STAGE_CSV)
    work, duplicates, conflicts = prepare_work(raw)
    print("Stage 1 working labels:", work.index.tolist())
    print("Stage 1 duplicate labels:", duplicates.index.tolist())
    print("Stage 1 conflicts:", conflicts.to_dict())
    if stage == 1:
        return
    converted, reasons = validate_work(work, conflicts)
    print("Stage 2 reasons:", reasons.to_dict())
    if stage == 2:
        return
    accepted, rejected = split_evidence(raw, converted, reasons)
    print("Stage 3 accepted:\n" + accepted.to_string(index=False))
    print("Stage 3 rejected evidence:\n" + rejected.to_string(index=False))
    if stage == 3:
        return
    summary = summarize_accepted(accepted)
    print("Stage 4 summary:\n" + summary.to_string(index=False))
    if stage == 4:
        return
    print("Stage 5 audit:", make_audit(raw, duplicates, rejected, accepted))


# SUPPLIED INGESTION SUPPORT. Read the structural checkpoint, then reuse this.
def read_order_records(csv_text):
    # Parse strictly before constructing a DataFrame. Some CSV readers can
    # reinterpret an extra field as an index; that would lose source evidence.
    try:
        records = list(csv.reader(StringIO(csv_text), strict=True))
    except csv.Error as error:
        raise ValueError(f"malformed CSV quoting: {error}") from error
    if not records:
        raise ValueError("empty CSV: required header is missing")
    header = records[0]
    missing = set(REQUIRED) - set(header)
    if missing:
        raise ValueError(f"missing columns: {sorted(missing)}")
    if len(header) != len(REQUIRED) or set(header) != set(REQUIRED):
        raise ValueError("unexpected or duplicate columns")
    if len(records) - 1 > 1000:
        raise ValueError("this teaching pipeline supports at most 1000 rows")
    for position, record in enumerate(records[1:], start=2):
        if len(record) != len(header):
            raise ValueError(
                f"CSV record {position} has {len(record)} fields; "
                f"expected {len(header)}. Original fields: {record!r}"
            )
    # No indexing inference: every validated source field becomes a table cell.
    raw = pd.DataFrame(records[1:], columns=header, dtype="string")
    return raw


# SUPPLIED PUBLIC CHECKS. These call your function with new toy inputs.
HEADER = "order_id,date,category,units,unit_price_cents\n"

def check_submission(clean_orders):
    """Call the function passed by the learner on fresh, public test fixtures."""
    count = 0
    def inspect(text):
        result = clean_orders(text)
        assert isinstance(result, tuple) and len(result) == 5, "Return the five-item tuple"
        accepted, rejected, duplicates, summary, audit = result
        assert all(isinstance(frame, pd.DataFrame) for frame in result[:4]), "First four results must be tables"
        assert isinstance(audit, dict), "The last result must be an audit dictionary"
        assert audit["raw_rows"] == len(accepted) + len(rejected) + len(duplicates), "Counts do not reconcile"
        assert audit["accepted_rows"] == len(accepted)
        assert audit["rejected_rows"] == len(rejected)
        assert audit["duplicate_rows"] == len(duplicates)
        assert audit["accepted_revenue_cents"] == sum(int(x) for x in accepted["revenue_cents"])
        return result

    accepted, rejected, duplicates, summary, audit = inspect(
        HEADER+"T01,2026-05-01,pens,2,80\nT02,2026-05-02,markers,3,150\n")
    assert audit["accepted_revenue_cents"] == 610 and len(accepted) == 2
    paired = dict(zip(summary["category"], summary["revenue_cents"]))
    assert paired == {"pens": 160, "markers": 450}, "Match category labels to totals"
    count += 1
    *_, audit = inspect(HEADER+"T03,2026-05-03,pens,9,90\n")
    assert audit["accepted_revenue_cents"] == 810, "Changed data must change the total"
    count += 1
    row = "T04,2026-05-04,pens,1,80\n"
    accepted, rejected, duplicates, _, audit = inspect(HEADER+row+row)
    assert (len(accepted),len(rejected),len(duplicates)) == (1,0,1)
    assert duplicates.iloc[0]["order_id"] == "T04"
    count += 1
    accepted, rejected, _, _, _ = inspect(HEADER+row+" T04 ,2026-05-04,pens,2,80\n")
    assert accepted.empty and len(rejected) == 2
    assert rejected["reason"].str.contains("conflicting_order_id").all()
    count += 1
    accepted, rejected, _, _, _ = inspect(HEADER+" T07 ,bad-date, PENS ,unknown,080\n")
    assert accepted.empty and len(rejected) == 1
    evidence = rejected.iloc[0]
    assert evidence["order_id"] == " T07 " and evidence["category"] == " PENS "
    assert evidence["units"] == "unknown" and evidence["unit_price_cents"] == "080"
    assert {"invalid_date", "invalid_units"} <= set(evidence["reason"].split(";"))
    count += 1
    accepted, rejected, _, _, audit = inspect(HEADER)
    assert accepted.empty and rejected.empty and audit["raw_rows"] == 0
    count += 1
    for row in ["extra,T08,2026-05-01,pens,1,80\n", 'T08,2026-05-01,pens,1,"80\n']:
        try:
            clean_orders(HEADER+row)
        except ValueError:
            pass
        else:
            raise AssertionError("Structurally malformed CSV must stop ingestion")
    count += 1
    for row in ["T09,2026-05-01,pens,1.0000000000000001,80\n", "T09,2500-05-01,pens,1,80\n"]:
        accepted, rejected, _, _, _ = inspect(HEADER+row)
        assert accepted.empty and len(rejected) == 1
    count += 1
    accepted, _, _, _, audit = inspect(HEADER+"T10,2000-01-01,pens,001,0\n")
    assert len(accepted) == 1 and audit["accepted_revenue_cents"] == 0
    count += 1
    print(f"{count} public capstone contract checks passed for your supplied function")
    print("Next: explain one new input, inspect exports and chart, and use the rubric.")
    return count

CSV_TEXT = 'order_id,date,category,units,unit_price_cents\nO001,2026-04-01,notebooks,3,250\nO002,2026-04-01,pens,10,80\nO003,2026-04-02,notebooks,2,250\nO004,2026-04-02,markers,4,150\nO004,2026-04-02,markers,4,150\nO005,2026-04-03,pens,,80\nO006,2026-04-03,markers,-2,150\nO007,2026-04-04,notebooks,1,unknown\nO008,not-a-date,pens,5,80\nO009,2026-04-04, PENS ,6,80\nO010,2026-04-05,,2,150\nO011,2026-04-05,notebooks,5,250\nO012,2026-04-06,markers,3,150\nO013,2026-04-06,pens,7,80\nO014,2026-04-07,notebooks,4,250\nO015,2026-04-07,markers,2,150\n'

# Keep definitions plus this driver in one browser run.
if 1 <= CHECKPOINT_STAGE <= 5:
    preview_stage(CHECKPOINT_STAGE)
elif CHECKPOINT_STAGE == 6:
    check_submission(clean_orders)
    accepted, rejected, duplicates, summary, audit = clean_orders(CSV_TEXT)
    print("AUDIT", audit)
    for label, frame in [("ACCEPTED", accepted), ("REJECTED", rejected),
                         ("DUPLICATES", duplicates), ("SUMMARY", summary)]:
        print("\n" + label + " CSV")
        print(frame.to_csv(index=False))
else:
    raise ValueError("CHECKPOINT_STAGE must be 1 through 6")
