"""Reproduce NEXORA's matched-invoice analysis using ECB reference-rate XML.

Examples:
  python scripts/research/fx-invoice-study.py --source ecb-history.xml
  python scripts/research/fx-invoice-study.py --output invoice-study.json

Rates are foreign-currency units per euro, not executable transaction quotes.
The study creates a separate invoice worth EUR 10,000 on the baseline date for
each currency, then holds that invoice's foreign-currency amount constant.
No exchange-rate forecast, dealer spread, fee, tax or hedge is assumed.
"""
import argparse
from datetime import date, datetime, timezone
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext
import hashlib
import json
from pathlib import Path
from urllib.request import Request, urlopen
import xml.etree.ElementTree as ET

SOURCE_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'
CURRENCIES = ('USD', 'JPY', 'GBP', 'CNY', 'CHF')


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


def parse_rates(data):
    if len(data) > 3_000_000 or b'<!DOCTYPE' in data.upper() or b'<!ENTITY' in data.upper():
        raise ValueError('Unexpected XML size or declarations')
    rows = {}
    for node in ET.fromstring(data).iter():
        if 'time' not in node.attrib:
            continue
        day = date.fromisoformat(node.attrib['time']).isoformat()
        rates = {}
        for child in node:
            code = child.attrib.get('currency')
            if code not in CURRENCIES:
                continue
            value = Decimal(child.attrib['rate'])
            if not value.is_finite() or value <= 0 or code in rates:
                raise ValueError('Invalid or duplicate currency rate')
            rates[code] = value
        if day in rows:
            raise ValueError('Duplicate observation date')
        rows[day] = rates
    return rows


def calculate(data, start, end, base_eur):
    series = parse_rates(data)
    if start >= end or end > datetime.now(timezone.utc).date().isoformat():
        raise ValueError('Dates must be chronological and not in the future')
    if not base_eur.is_finite() or base_eur <= 0:
        raise ValueError('The euro benchmark must be positive')
    if any(day not in series for day in (start, end)):
        raise ValueError('The ECB input does not contain both requested dates')
    rows = []
    total_comparison = Decimal('0')
    with localcontext() as context:
        context.prec = 40
        for currency in CURRENCIES:
            before = series[start].get(currency)
            after = series[end].get(currency)
            if before is None or after is None:
                raise ValueError(f'Missing {currency} on a requested date')
            invoice = base_eur * before
            cost_after = invoice / after
            total_comparison += cost_after
            change = cost_after - base_eur
            rows.append({
                'currency': currency,
                'rateBaselinePerEUR': str(before),
                'rateComparisonPerEUR': str(after),
                'fixedInvoiceForeignCurrency': money(invoice),
                'costBaselineEUR': money(base_eur),
                'costComparisonEUR': money(cost_after),
                'changeEUR': money(change),
                'changePercent': str((change / base_eur * 100).quantize(Decimal('0.0001'), rounding=ROUND_HALF_UP)),
                'fixed10000NativeCostBaselineEUR': money(Decimal('10000') / before),
                'fixed10000NativeCostComparisonEUR': money(Decimal('10000') / after),
            })
    return {
        'method': 'Equal-euro baseline; fixed foreign-currency invoices; unrounded Decimal arithmetic',
        'sourceURL': SOURCE_URL,
        'sourceSHA256': hashlib.sha256(data).hexdigest(),
        'calculatedAt': datetime.now(timezone.utc).isoformat(timespec='seconds').replace('+00:00', 'Z'),
        'baselineDate': start,
        'comparisonDate': end,
        'baseInvoiceEUR': money(base_eur),
        'kind': 'ECB daily reference rates; informational, not transaction quotes',
        'formula': 'foreign invoice = base EUR × baseline quote; comparison EUR = foreign invoice ÷ comparison quote',
        'equalWeightBasket': {
            'costBaselineEUR': money(base_eur * len(CURRENCIES)),
            'costComparisonEUR': money(total_comparison),
            'changeEUR': money(total_comparison - base_eur * len(CURRENCIES)),
        },
        'rows': rows,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--source', type=Path, help='Previously downloaded ECB history XML; no network used')
    parser.add_argument('--output', type=Path)
    parser.add_argument('--start', default='2026-10-27')
    parser.add_argument('--end', default='2026-11-04')
    parser.add_argument('--base-eur', default='10000')
    args = parser.parse_args()
    try:
        if args.source:
            data = args.source.read_bytes()
        else:
            request = Request(SOURCE_URL, headers={'User-Agent': 'NEXORA reproducible research/1.0'})
            with urlopen(request, timeout=25) as response:
                data = response.read(3_000_001)
        result = calculate(data, date.fromisoformat(args.start).isoformat(),
                           date.fromisoformat(args.end).isoformat(), Decimal(args.base_eur))
        encoded = json.dumps(result, indent=2) + '\n'
        if args.output:
            args.output.parent.mkdir(parents=True, exist_ok=True)
            args.output.write_text(encoded, encoding='utf-8')
        else:
            print(encoded)
    except (OSError, ValueError, InvalidOperation, ET.ParseError) as error:
        parser.exit(1, f'No study output produced: {error}\n')


if __name__ == '__main__':
    main()
