# Profile: Data · NumPy and pandas. No local files required.
from io import StringIO
import csv
import pandas as pd
REQUIRED = ["order_id", "date", "category", "units", "unit_price_cents"]
CATEGORIES = {"notebooks", "pens", "markers"}
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'

def clean_orders(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")
    # Exact duplicates compare source field values, before normalization.
    duplicate_mask = raw.duplicated(keep="first")
    duplicates = raw.loc[duplicate_mask].copy()
    work = raw.loc[~duplicate_mask].copy()
    work["order_id"] = work["order_id"].str.strip()
    work["category"] = work["category"].str.strip().str.lower()
    # A same-ID disagreement is not permission to choose a winner.
    conflict_mask = work["order_id"].duplicated(keep=False)
    iso_date_shape = work["date"].str.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
    work["date"] = pd.to_datetime(work["date"], format="%Y-%m-%d", errors="coerce")
    integer_syntax = {}
    for column in ["units", "unit_price_cents"]:
        numeric_text = work[column].str.strip()
        integer_syntax[column] = numeric_text.str.fullmatch(r"[0-9]+")
        work[column] = pd.to_numeric(numeric_text, errors="coerce")
    valid_units = (integer_syntax["units"] & work["units"].notna() & work["units"].gt(0)
                   & work["units"].le(1_000_000) & work["units"].mod(1).eq(0))
    valid_price = (integer_syntax["unit_price_cents"] & work["unit_price_cents"].notna() & work["unit_price_cents"].ge(0)
                   & work["unit_price_cents"].le(1_000_000_000)
                   & work["unit_price_cents"].mod(1).eq(0))
    # Upper bounds are an explicit teaching contract, keeping int64 products safe.
    checks = {
        "missing_order_id": work["order_id"].eq(""),
        "conflicting_order_id": conflict_mask,
        "invalid_date": (work["date"].isna() | ~iso_date_shape
                         | ~work["date"].between("2000-01-01", "2099-12-31")),
        "invalid_category": ~work["category"].isin(CATEGORIES),
        "invalid_units": ~valid_units,
        "invalid_price": ~valid_price,
    }
    reasons = pd.Series("", index=work.index, dtype="string")
    for reason, mask in checks.items():
        reasons.loc[mask] = reasons.loc[mask] + reason + ";"
    valid = reasons.eq("")
    rejected = raw.loc[work.index[~valid]].copy()
    rejected["reason"] = reasons.loc[~valid].str.rstrip(";")
    accepted = work.loc[valid].copy()
    for column in ["units", "unit_price_cents"]:
        accepted[column] = accepted[column].astype("int64")
    accepted["revenue_cents"] = accepted["units"] * accepted["unit_price_cents"]
    accepted = accepted.sort_values(["date", "order_id"]).reset_index(drop=True)
    summary = (accepted.groupby("category", as_index=False)[["units", "revenue_cents"]]
               .sum().sort_values("category").reset_index(drop=True))
    # Python ints avoid fixed-width overflow in the overall audit subtotal.
    subtotal = sum(int(value) for value in accepted["revenue_cents"])
    audit = {"raw_rows": len(raw), "duplicate_rows": len(duplicates),
             "rejected_rows": len(rejected), "accepted_rows": len(accepted),
             "accepted_revenue_cents": subtotal}
    return accepted, rejected, duplicates, summary, audit

accepted, rejected, duplicates, summary, audit = clean_orders(CSV_TEXT)
assert audit == {"raw_rows":16,"duplicate_rows":1,"rejected_rows":5,"accepted_rows":10,"accepted_revenue_cents":6690}
print("AUDIT", audit)
for name, frame in [("ACCEPTED", accepted), ("REJECTED", rejected), ("DUPLICATES", duplicates), ("SUMMARY", summary)]:
    print("\n" + name + " CSV")
    print(frame.to_csv(index=False))
