#!/usr/bin/env python3
"""Synthetic RFQ line-item matcher; standard library only."""
import argparse
import csv
from collections import defaultdict
from decimal import Decimal, DecimalException, InvalidOperation, ROUND_HALF_UP, localcontext
from pathlib import Path

FIELDS = [
    'request_line', 'requested_code', 'requested_description',
    'requested_quantity', 'requested_unit', 'status', 'matched_code',
    'matched_description', 'approved_unit', 'approved_unit_price_dkk',
    'line_total_dkk', 'price_source', 'review_reason'
]
DANGEROUS_PREFIXES = ('=', '+', '-', '@')

def clean(value):
    return (value or '').strip()

def safe_csv(value):
    value = str(value or '')
    if value.lstrip().startswith(DANGEROUS_PREFIXES):
        return "'" + value
    return value

def money(value):
    return value.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)

def load_catalogue(path):
    by_code = defaultdict(list)
    with open(path, newline='', encoding='utf-8-sig') as source:
        for raw in csv.DictReader(source):
            row = {key: clean(value) for key, value in raw.items()}
            row['code_key'] = row['code'].casefold()
            row['unit_key'] = row['unit'].casefold()
            by_code[row['code_key']].append(row)
    return by_code

def hold(row, reason, item=None):
    item = item or {}
    return {
        'request_line': row['line'],
        'requested_code': row['code'],
        'requested_description': row['description'],
        'requested_quantity': row['quantity'],
        'requested_unit': row['unit'],
        'status': 'HUMAN_REVIEW',
        'matched_code': item.get('code', ''),
        'matched_description': item.get('description', ''),
        'approved_unit': item.get('unit', ''),
        'approved_unit_price_dkk': '',
        'line_total_dkk': '',
        'price_source': item.get('price_source', ''),
        'review_reason': reason,
    }

def match_row(raw, by_code):
    row = {key: clean(value) for key, value in raw.items()}
    if not row['code'] or not row['unit']:
        return hold(row, 'MISSING_CODE_OR_UNIT')
    candidates = by_code.get(row['code'].casefold(), [])
    if not candidates:
        return hold(row, 'UNKNOWN_CODE')
    same_unit = [item for item in candidates
                 if item['unit_key'] == row['unit'].casefold()]
    if not same_unit:
        return hold(row, 'INCOMPATIBLE_UNIT')
    if len(same_unit) != 1:
        return hold(row, 'AMBIGUOUS_CODE_AND_UNIT')
    item = same_unit[0]
    try:
        quantity = Decimal(row['quantity'])
        if not quantity.is_finite() or quantity <= 0:
            raise InvalidOperation
    except (InvalidOperation, ValueError):
        return hold(row, 'INVALID_QUANTITY', item)
    if not item['approved_unit_price_dkk']:
        return hold(row, 'MISSING_APPROVED_PRICE', item)
    try:
        price = Decimal(item['approved_unit_price_dkk'])
        if not price.is_finite() or price < 0:
            raise InvalidOperation
    except (InvalidOperation, ValueError):
        return hold(row, 'INVALID_APPROVED_PRICE', item)
    if not item['price_source']:
        return hold(row, 'MISSING_PRICE_PROVENANCE', item)
    # Bounded demonstration arithmetic, not a general pricing engine.
    try:
        with localcontext() as ctx:
            ctx.prec = 80
            if quantity > Decimal('1e12') or price > Decimal('1e12'):
                return hold(row, 'ARITHMETIC_RANGE', item)
            if quantity != quantity.quantize(Decimal('0.000001')):
                return hold(row, 'UNSUPPORTED_QUANTITY_PRECISION', item)
            unit_price = money(price)
            if price != unit_price:
                return hold(row, 'UNSUPPORTED_PRICE_PRECISION', item)
            total = money(quantity * unit_price)
    except DecimalException:
        return hold(row, 'ARITHMETIC_RANGE', item)
    return {
        'request_line': row['line'],
        'requested_code': row['code'],
        'requested_description': row['description'],
        'requested_quantity': row['quantity'],
        'requested_unit': row['unit'],
        'status': 'READY_FOR_QUOTE_DRAFT',
        'matched_code': item['code'],
        'matched_description': item['description'],
        'approved_unit': item['unit'],
        'approved_unit_price_dkk': f'{unit_price:.2f}',
        'line_total_dkk': f'{total:.2f}',
        'price_source': item['price_source'],
        'review_reason': '',
    }

def run(request_path, catalogue_path, output_path):
    catalogue = load_catalogue(catalogue_path)
    with open(request_path, newline='', encoding='utf-8-sig') as source:
        results = [match_row(row, catalogue) for row in csv.DictReader(source)]
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, 'w', newline='', encoding='utf-8') as target:
        writer = csv.DictWriter(target, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows({key: safe_csv(value) for key, value in row.items()}
                         for row in results)
    return results

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--request', required=True)
    parser.add_argument('--catalogue', required=True)
    parser.add_argument('--output', required=True)
    args = parser.parse_args()
    results = run(args.request, args.catalogue, args.output)
    ready = sum(row['status'] == 'READY_FOR_QUOTE_DRAFT' for row in results)
    print(f'rows={len(results)} ready={ready} human_review={len(results)-ready} output={args.output}')

if __name__ == '__main__':
    main()
