#!/usr/bin/env python3
"""Simulation fictive, hors ligne. Aucun appel Sharetribe, aucun export client.

Sous-périmètre : livraisons CDN d'images réussies, usage ordinaire sans
engagement, au plus 3 millions de crédits au total par mois. Pas un moteur
de facturation exhaustif. Barème documentaire vérifié le 24 octobre 2026.
Les autres usages doivent être fournis, sans chevauchement avec les images.
"""
from collections import defaultdict
from dataclasses import dataclass, replace
from decimal import Decimal, ROUND_HALF_UP
from itertools import permutations
import json
import re

D = Decimal
GIB = 2 ** 30
PROFILE = "sharetribe-images-2026-10-24"


@dataclass(frozen=True)
class Delivery:
    request_id: str
    month: str
    image_id: str
    variant: str
    transferred_bytes: int
    status: int = 200


def valid_month(month):
    return isinstance(month, str) and re.fullmatch(r"\d{4}-(0[1-9]|1[0-2])", month)


def totals(rows, other, profile=PROFILE):
    if profile != PROFILE:
        raise ValueError("Barème non pris en charge")
    if not other:
        raise ValueError("Usage hors images absent")
    for month, credits in other.items():
        if not valid_month(month) or type(credits) is not int or credits < 0:
            raise ValueError("Période ou autres crédits invalides")
    requests, unique, byte_counts, seen = defaultdict(int), defaultdict(set), defaultdict(int), {}
    for row in rows:
        if not isinstance(row, Delivery):
            raise ValueError("Livraison invalide")
        if not all(isinstance(x, str) and x.strip() for x in
                   (row.request_id, row.image_id, row.variant)):
            raise ValueError("Identité absente")
        if row.month not in other or not valid_month(row.month):
            raise ValueError("Mois sans périmètre complet")
        if type(row.transferred_bytes) is not int or row.transferred_bytes < 0:
            raise ValueError("Octets invalides")
        # Le modèle n'estime pas les réponses d'erreur ni les revalidations.
        if type(row.status) is not int or not 200 <= row.status <= 299:
            raise ValueError("Seules les livraisons réussies sont modélisées")
        key = (row.month, row.request_id)
        if key in seen:
            if seen[key] != row:
                raise ValueError("Requête dupliquée contradictoire")
            continue  # Même observation réimportée ; pas une nouvelle requête.
        seen[key] = row
        requests[row.month] += 1
        unique[row.month].add(row.image_id)
        byte_counts[row.month] += row.transferred_bytes
    output = {}
    for month in sorted(other):
        n = requests[month]
        u = len(unique[month])
        size = byte_counts[month]
        images = D(u) * 340 + D(n) * D("1.5") + D(size) / GIB * 10000
        total = D(other[month]) + images
        if total > 3000000:
            raise ValueError("Au-delà de 3M : autres tranches non modélisées")
        usage_usd = (max(D(0), total - 1000000) / 1000 * D("0.07")).quantize(
            D("0.01"), rounding=ROUND_HALF_UP)
        output[month] = dict(images_unique=u, requests=n, bytes=size,
                             image_credits=str(images), other_credits=other[month],
                             total_credits=str(total), usage_usd=str(usage_usd),
                             live_monthly_plus_usage_usd=str(D(259) + usage_usd))
    return output


def fixture(image_count, total_bytes, month="2026-12"):
    """Six livraisons par image, deux par variante, avec somme d'octets exacte."""
    n = image_count * 6
    small, remainder = divmod(total_bytes, n)
    return [Delivery(f"r-{i}", month, f"image-{i // 6}",
                     ("small", "medium", "large")[i % 3],
                     small + int(i < remainder)) for i in range(n)]


def self_test():
    checks = 0

    def check(ok):
        nonlocal checks
        assert ok
        checks += 1

    def refused(fn):
        nonlocal checks
        try:
            fn()
        except ValueError:
            checks += 1
        else:
            raise AssertionError("Un cas invalide a été accepté")

    eager = fixture(5000, 15 * GIB)
    limited = fixture(1000, 3 * GIB)
    base = {"2026-12": 650000}
    a, b = totals(eager, base)["2026-12"], totals(limited, base)["2026-12"]
    for field, expected in dict(images_unique=5000, requests=30000, bytes=15*GIB,
                                image_credits="1895000.0", total_credits="2545000.0",
                                usage_usd="108.15", live_monthly_plus_usage_usd="367.15").items():
        check(a[field] == expected)
    for field, expected in dict(images_unique=1000, requests=6000, bytes=3*GIB,
                                image_credits="379000.0", total_credits="1029000.0",
                                usage_usd="2.03", live_monthly_plus_usage_usd="261.03").items():
        check(b[field] == expected)
    check(D(a['usage_usd']) - D(b['usage_usd']) == D('106.12'))
    check(totals(reversed(eager), base)["2026-12"] == a)
    check(totals(eager + eager, base)["2026-12"] == a)
    sample = [Delivery("one", "2026-12", "photo", "small", 10),
              Delivery("two", "2026-12", "photo", "large", 20),
              Delivery("three", "2026-12", "other-photo", "small", 30)]
    expected = totals(sample, base)
    for perm in permutations(sample):
        check(totals(perm, base) == expected)
    check(expected['2026-12']['images_unique'] == 2)
    repeat = [Delivery("a", "2026-12", "same", "s", 0),
              Delivery("b", "2027-01", "same", "s", 0)]
    monthly = totals(repeat, {"2026-12": 0, "2027-01": 0})
    check([x['images_unique'] for x in monthly.values()] == [1, 1])
    for credits, cost in [(0, '0.00'), (1000000, '0.00'), (1001000, '0.07'),
                          (3000000, '140.00')]:
        check(totals([], {'2026-12': credits})['2026-12']['usage_usd'] == cost)
    for row in [replace(sample[0], request_id=''), replace(sample[0], image_id=''),
                replace(sample[0], variant=''), replace(sample[0], month='2026-13'),
                replace(sample[0], transferred_bytes=-1), replace(sample[0], transferred_bytes=True),
                replace(sample[0], transferred_bytes=1.5), replace(sample[0], status=404),
                replace(sample[0], status=304), replace(sample[0], status=500),
                replace(sample[0], month='2027-01')]:
        refused(lambda row=row: totals([row], base))
    refused(lambda: totals([sample[0], replace(sample[0], transferred_bytes=11)], base))
    for wrong in [{}, {'2026-12': -1}, {'2026-12': True}, {'2026-12': 3000001}]:
        refused(lambda wrong=wrong: totals([], wrong))
    refused(lambda: totals([], base, 'future-profile'))
    # Trois calculs volontairement faux, distincts du calcul validé.
    variants_as_images = 15000 * 340 + 30000 * 1.5 + 150000
    decimal_gb = D(15 * GIB) / 1000000000 * 10000
    forget_unique = 30000 * 1.5 + 150000
    check(variants_as_images == 5295000 and D(str(variants_as_images)) != D(a['image_credits']))
    check(decimal_gb == D('161061.27360000') and decimal_gb != 150000)
    check(forget_unique == 195000 and D(str(forget_unique)) != D(a['image_credits']))
    return dict(checks=checks, status='PASS', fixture='fictive-no-network',
                eager=a, limited=b, difference_usd='106.12', permutations=6,
                wrong_witnesses={'variantsAsImagesCredits': str(variants_as_images),
                                 'decimalGbTransferCredits': str(decimal_gb),
                                 'missingUniqueCredits': str(forget_unique)})


if __name__ == '__main__':
    print(json.dumps(self_test(), ensure_ascii=False, indent=2))
