"""Offline synthetic one-to-one CSV join example, not a payment system."""

import argparse
import csv
import json
import re
from decimal import Decimal, localcontext
from pathlib import Path


ORDER_FIELDS = ("order_id", "currency", "status", "amount")
CHANNEL_FIELDS = ("order_id", "source")
MONEY = re.compile(r"(?:0|[1-9][0-9]{0,15})(?:\.[0-9]{1,2})?")
KEY = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}")


def read_rows(source, fields):
    reader = csv.DictReader(source, strict=True)
    if tuple(reader.fieldnames or ()) != fields:
        raise ValueError("unexpected_csv_header")
    rows = []
    for row in reader:
        if None in row or any(row[field] is None for field in fields):
            raise ValueError("wrong_csv_field_count")
        rows.append(row)
        if len(rows) > 10000:
            raise ValueError("example_row_limit_exceeded")
    if not rows:
        raise ValueError("empty_table")
    return rows


def unique_index(rows, table):
    index = {}
    for row in rows:
        key = row["order_id"]
        if KEY.fullmatch(key) is None:
            raise ValueError("invalid_order_id:" + table)
        if key in index:
            raise ValueError("duplicate_key:" + table + ":" + key)
        index[key] = row
    return index


def reconcile(orders_source, channels_source):
    orders = read_rows(orders_source, ORDER_FIELDS)
    channels = read_rows(channels_source, CHANNEL_FIELDS)
    order_index = unique_index(orders, "orders")
    channel_index = unique_index(channels, "channels")
    missing = sorted(order_index.keys() - channel_index.keys())
    extra = sorted(channel_index.keys() - order_index.keys())
    if missing or extra:
        raise ValueError("key_set_mismatch:missing=" + str(missing)
                         + ";extra=" + str(extra))
    included, excluded, amounts = [], [], []
    for order_id, row in order_index.items():
        if row["currency"] != "CNY":
            raise ValueError("unsupported_currency:" + order_id)
        if row["status"] not in {"paid", "unpaid", "refunded"}:
            raise ValueError("unknown_status:" + order_id)
        if MONEY.fullmatch(row["amount"]) is None:
            raise ValueError("invalid_two_decimal_amount:" + order_id)
        if not channel_index[order_id]["source"].strip():
            raise ValueError("missing_source:" + order_id)
        amount = Decimal(row["amount"])
        if row["status"] == "paid":
            included.append(order_id)
            amounts.append(amount)
        else:
            excluded.append(order_id)
    with localcontext() as context:
        context.prec = 28
        total = sum(amounts, Decimal("0.00"))
    return {
        "input_order_rows": len(orders),
        "input_channel_rows": len(channels),
        "merged_rows": len(order_index),
        "currency": "CNY",
        "included_ids": included,
        "excluded_ids": excluded,
        "paid_total": format(total, ".2f"),
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("orders", type=Path)
    parser.add_argument("channels", type=Path)
    args = parser.parse_args()
    try:
        with args.orders.open(encoding="utf-8-sig", newline="") as orders:
            with args.channels.open(encoding="utf-8-sig", newline="") as channels:
                result = reconcile(orders, channels)
    except (ValueError, csv.Error, UnicodeError, OSError) as error:
        print(json.dumps({"valid": False, "error": str(error)}))
        return 2
    print(json.dumps({"valid": True, **result}, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
