#!/usr/bin/env python3
"""Offline rounding-policy model. No tax calculation or payment-provider calls."""
from decimal import Decimal
from fractions import Fraction
from itertools import product, permutations
from pathlib import Path
import json


def allocate(total, lines):
    if type(total) is not int or total < 0:
        raise ValueError('total must be a nonnegative integer minor-unit amount')
    if not lines or len({key for key, _ in lines}) != len(lines):
        raise ValueError('nonempty lines need unique stable IDs')
    weights = [(key, Fraction(Decimal(weight))) for key, weight in lines]
    if any(w < 0 for _, w in weights):
        raise ValueError('negative weights are outside this policy')
    weight_sum = sum(w for _, w in weights)
    if not weight_sum:
        if total:
            raise ValueError('cannot allocate a positive total to zero weights')
        return {key: 0 for key, _ in weights}
    raw = {key: total * w / weight_sum for key, w in weights}
    units = {key: value.numerator // value.denominator for key, value in raw.items()}
    residual = total - sum(units.values())
    order = sorted(raw, key=lambda key: (-(raw[key] - units[key]), key))
    for key in order[:residual]:
        units[key] += 1
    assert sum(units.values()) == total
    assert all(abs(Fraction(units[k]) - raw[k]) < 1 for k in units)
    return units


def half_up(value):
    return (2 * value.numerator + value.denominator) // (2 * value.denominator)


def run_case(name, total, scale, weights):
    lines = list(zip('abc', weights))
    weight_sum = sum(Fraction(Decimal(w)) for w in weights)
    raw = [Fraction(total) * Fraction(Decimal(w)) / weight_sum for w in weights] if weight_sum else [Fraction(0)] * len(weights)
    independent = [half_up(v) for v in raw]
    result = allocate(total, lines)
    return dict(name=name, scale=scale, total_minor=total, weights=weights,
                independent_minor=independent, independent_error_minor=sum(independent)-total,
                allocated_minor=result, allocation_error_minor=sum(result.values())-total)


def main():
    fixtures = [
        ('USD equal thirds', 100, 2, ['1','1','1']),
        ('USD two cents', 2, 2, ['1','1','1']),
        ('USD uneven weights', 101, 2, ['1','2','3']),
        ('JPY one unit', 1, 0, ['1','1']),
        ('Three-decimal scale', 1001, 3, ['1','1','1']),
        ('Zero allocation', 0, 2, ['0','0','0']),
        ('Single line', 100, 2, ['1']),
    ]
    cases = [run_case(*f) for f in fixtures]
    original = cases[0]['allocated_minor']
    reversal = {k: -v for k,v in original.items()}
    assert sum(reversal.values()) == -100
    cases.append(dict(name='Stored full reversal', scale=2, total_minor=-100,
                      allocated_minor=reversal, allocation_error_minor=0,
                      method='negate the stored original amounts; do not reallocate'))
    rejected = []
    for name,total,lines in [
        ('duplicate IDs',100,[('a','1'),('a','2')]),
        ('negative weight',100,[('a','-1'),('b','2')]),
        ('zero weights with positive total',100,[('a','0')]),
        ('fractional minor-unit total',Decimal('1.5'),[('a','1')]),
        ('negative total',-1,[('a','1')]),
        ('empty lines',0,[]),
    ]:
        try:
            allocate(total,lines)
        except ValueError:
            rejected.append(name)
        else:
            raise AssertionError(name)
    mismatches = 0
    checked = 0
    for weights in product(range(1,6), repeat=3):
        for total in range(30):
            row = run_case('sensitivity', total, 2, list(map(str,weights)))
            mismatches += row['independent_error_minor'] != 0
            checked += 1
    permutation_checks = 0
    for _,total,_,weights in fixtures[:5]:
        lines = list(zip('abc',weights))
        expected = allocate(total,lines)
        for reordered in permutations(lines):
            assert allocate(total,list(reordered)) == expected
            permutation_checks += 1
    return dict(scope='Constructed exact-rational arithmetic; no tax, FX, provider or database',
                policy='nonnegative total; largest fractional remainder; stable ID tie-break',
                cases=cases, rejected_inputs=rejected,
                sensitivity=dict(cases=checked, independent_mismatches=mismatches,
                                 allocation_mismatches=0, allocation_bound_violations=0),
                permutation_checks=permutation_checks)


if __name__ == '__main__':
    output=main()
    Path(__file__).with_name('results.json').write_text(json.dumps(output,indent=2)+'\n')
    print(json.dumps(output))
