"""MIT. Normalize fields, match line items and check printed totals locally."""
import argparse
from collections import Counter
from decimal import Decimal, InvalidOperation
import json
from pathlib import Path
import unicodedata

FIELDS = ['invoice_id', 'document_type', 'vendor', 'email', 'invoice_date', 'due_date', 'currency', 'subtotal', 'tax', 'total', 'bank_code', 'purchase_order']
LINE_FIELDS = ['description', 'quantity', 'unit_price', 'amount']
NUMERIC = {'subtotal', 'tax', 'total', 'quantity', 'unit_price', 'amount'}


def normalize(key, value):
    if value is None:
        return None
    if not isinstance(value, str):
        raise ValueError('Fields must be strings or null')
    value = ' '.join(unicodedata.normalize('NFKC', value).split())
    if key in NUMERIC:
        result = Decimal(value)
        if not result.is_finite():
            raise ValueError('Nonfinite number')
        return result
    return value


def equal(key, actual, expected):
    try:
        if key in NUMERIC:
            # Currency symbols and thousands separators are format, not value.
            stripped = str(actual).replace('$', '').replace('€', '').replace('£', '').replace(',', '')
            return normalize(key, stripped) == normalize(key, expected)
        return normalize(key, actual) == normalize(key, expected)
    except (InvalidOperation, ValueError):
        return False


def schema_valid(arguments):
    if not isinstance(arguments, dict) or set(arguments) != set(FIELDS + ['line_items', 'note']):
        return False
    if any(not isinstance(arguments[k], str) for k in FIELDS if k != 'purchase_order'):
        return False
    if arguments['purchase_order'] is not None and not isinstance(arguments['purchase_order'], str):
        return False
    if arguments['document_type'] not in ('invoice', 'credit_note') or arguments['currency'] not in ('USD', 'EUR', 'GBP', 'CAD'):
        return False
    return isinstance(arguments['note'], str) and isinstance(arguments['line_items'], list) and all(isinstance(r, dict) and set(r) == set(LINE_FIELDS) and all(isinstance(v, str) for v in r.values()) for r in arguments['line_items'])


def total_flag(arguments):
    try:
        values = [normalize(k, arguments.get(k)) for k in ('subtotal', 'tax', 'total')]
        return None if None in values else values[0] + values[1] != values[2]
    except (InvalidOperation, ValueError):
        return None


def line_key(row):
    return tuple(normalize(k, row[k]) for k in LINE_FIELDS)


def score(item, arguments, config):
    arguments = arguments if isinstance(arguments, dict) else {}
    fields = {k: int(k in arguments and equal(k, arguments[k], item['gold'][k])) for k in FIELDS}
    expected = Counter(line_key(row) for row in item['line_items'])
    got = Counter()
    invalid = 0
    lines = arguments.get('line_items', [])
    if not isinstance(lines, list):
        lines = []
    for row in lines:
        try:
            got[line_key(row)] += 1
        except (KeyError, TypeError, ValueError, InvalidOperation):
            invalid += 1
    tp = sum((expected & got).values())
    flag = total_flag(arguments)
    return {'score': sum(fields.values()) / len(FIELDS), 'secondary': {
        'line_item_f1': {'tp': tp, 'fp': sum(got.values()) + invalid - tp, 'fn': sum(expected.values()) - tp},
        'schema_validity': {'score': int(schema_valid(arguments))},
        'planted_errors_caught': {'score': int(item['total_error'] and flag is True)},
        'false_flags': {'score': int(not item['total_error'] and flag is True)},
        **{k + '_exact': {'score': v} for k, v in fields.items()},
    }}


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Offline example check; exits nonzero on any mismatch')
    parser.add_argument('--fixture', type=Path, default=Path(__file__).with_name('fixture.jsonl'))
    parser.add_argument('--item', default='dev-00')
    parser.add_argument('--arguments', type=Path, required=True)
    args = parser.parse_args()
    item = next(json.loads(line) for line in args.fixture.read_text().splitlines() if json.loads(line)['item_id'] == args.item)
    result = score(item, json.loads(args.arguments.read_text()), {})
    print(json.dumps(result, indent=2))
    secondary = result['secondary']
    raise SystemExit(0 if result['score'] == 1 and secondary['schema_validity']['score'] == 1 and secondary['line_item_f1']['fp'] == secondary['line_item_f1']['fn'] == 0 and total_flag(json.loads(args.arguments.read_text())) == item['total_error'] else 1)
