"""CSV Rescue Desk: a reproducible fictional product-import example.

Python 3.10+, standard library only. No network calls.
Run: python csv_rescue_demo.py --demo --out sample-output
Or:  python csv_rescue_demo.py --input products.csv --out new-output

Exact columns: sku,name,price. Prices follow US decimal/grouping rules,
are non-negative and have at most two decimal places. This is a sample
policy, not a universal importer. Review output before any real import.
Output directories must be new; original files are never overwritten.
"""
import argparse
import csv
import io
import json
import re
from decimal import Decimal
from pathlib import Path

DEMO = '''sku,name,price
001, Ridge Mug ,12.00
002,Desk Tray, 14.50 
001,Ridge Mug,12.00
003,Linen Tote,N/A
004,Task Lamp,25
005,,18.00
006,Studio Desk,"1,200.00"
007,Sample Card,0
'''
COLUMNS = ['sku', 'name', 'price']
PRICE = re.compile(r'(?:[0-9]+|[0-9]{1,3}(?:,[0-9]{3})+)(?:\.[0-9]{1,2})?\Z')


def clean(source):
    reader = csv.reader(source, strict=True)
    if next(reader, None) != COLUMNS:
        raise ValueError('Expected exactly the columns sku,name,price in that order.')
    kept, review, duplicates, changes, seen = [], [], [], [], {}
    originals = []
    for number, raw in enumerate(reader, 2):
        originals.append({'source_record': number, 'values': raw})
        if len(raw) != 3:
            review.append({'source_record': number, 'original': raw, 'reason': 'Wrong column count'})
            continue
        row = dict(zip(COLUMNS, [v.strip() for v in raw]))
        reason = ''
        if not row['sku'] or not row['name']:
            reason = 'Missing SKU or product name'
        elif any(v.startswith(('=', '+', '-', '@')) for v in row.values()):
            reason = 'Possible spreadsheet formula: review as text'
        elif not PRICE.fullmatch(row['price']):
            reason = 'Invalid US price; no value guessed'
        if reason:
            review.append({'source_record': number, 'original': raw, 'reason': reason})
            continue
        # Formatting exact cents only; no rounding policy is silently applied.
        row['price'] = format(Decimal(row['price'].replace(',', '')), '.2f')
        if row['sku'] in seen:
            prior, prior_number = seen[row['sku']]
            if row == prior:
                duplicates.append({'source_record': number, 'matches_record': prior_number, 'original': raw})
            else:
                review.append({'source_record': number, 'original': raw, 'reason': f'Conflicting SKU; earlier record {prior_number} retained for review before import'})
            continue
        seen[row['sku']] = (row, number)
        kept.append(row)
        for column, before in zip(COLUMNS, raw):
            if row[column] != before:
                changes.append({'source_record': number, 'column': column, 'before': before, 'after': row[column]})
    assert len(originals) == len(kept) + len(review) + len(duplicates)
    return {
        'summary': {'input_rows': len(originals), 'clean_rows': len(kept),
                    'review_rows': len(review), 'duplicate_rows': len(duplicates)},
        'cleaned': kept, 'review': review, 'duplicates': duplicates,
        'changes': changes, 'original_records': originals,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument('--demo', action='store_true')
    source.add_argument('--input', type=Path)
    parser.add_argument('--out', type=Path, required=True)
    args = parser.parse_args()
    try:
        if args.demo:
            result = clean(io.StringIO(DEMO))
        else:
            with args.input.open(encoding='utf-8-sig', newline='') as stream:
                result = clean(stream)
        args.out.mkdir(parents=True, exist_ok=False)
        with (args.out / 'cleaned.csv').open('w', encoding='utf-8', newline='') as stream:
            writer = csv.DictWriter(stream, fieldnames=COLUMNS)
            writer.writeheader()
            writer.writerows(result['cleaned'])
        # JSON keeps rejected raw values inert and preserves every original record.
        (args.out / 'report.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding='utf-8')
        if args.demo:
            (args.out / 'original.csv').write_text(DEMO, encoding='utf-8')
        print(json.dumps(result['summary'], indent=2))
        print(f'Files written to {args.out.resolve()}. Review report.json before importing.')
    except (OSError, ValueError, csv.Error) as error:
        parser.exit(1, f'Error: {error}\n')


if __name__ == '__main__':
    main()
