"""
Money and currency utilities for TripTalley.
All money amounts are stored as integer **minor units** (cents) with explicit currency.
No floats in storage or settlement - ensures exact math.
"""

from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import Dict, List, Tuple
import math

# Currency metadata: ISO-4217 code -> (exponent, smallest_unit_name)
# Exponent = 2 for USD/EUR (cents), 0 for JPY (yen), 3 for BHD (millimes), etc.
CURRENCY_METRICS: Dict[str, Tuple[int, str]] = {
    "USD": (2, "cent"),
    "EUR": (2, "cent"),
    "GBP": (2, "penny"),
    "JPY": (0, "yen"),
    "AUD": (2, "cent"),
    "CAD": (2, "cent"),
    "CHF": (2, "cent"),
    "CNY": (2, "fen"),
    "SEK": (2, "öre"),
    "NOK": (2, "øre"),
    "DKK": (2, "øre"),
    "NZD": (2, "cent"),
    "MXN": (2, "centavo"),
    "SGD": (2, "cent"),
    "HKD": (2, "cent"),
    "KRW": (0, "won"),
    "INR": (2, "paisa"),
    "BRL": (2, "centavo"),
    "ZAR": (2, "cent"),
    "RUB": (2, "kopeck"),
    "TRY": (2, "kurus"),
    "BHD": (3, "millime"),
    "KWD": (3, "fils"),
    "OMR": (3, "baisa"),
    "JOD": (3, "fils"),
}


def get_exponent(currency: str) -> int:
    """Get the minor-unit exponent for a currency (default 2)."""
    return CURRENCY_METRICS.get(currency, (2, ""))[0]


def get_smallest_unit(currency: str) -> str:
    """Get the name of the smallest unit for display."""
    return CURRENCY_METRICS.get(currency, (2, "unit"))[1]


def to_minor(amount: float, currency: str) -> int:
    """
    Convert a decimal amount to integer minor units (cents, etc.).
    Uses Decimal to avoid float precision issues.
    """
    exponent = get_exponent(currency)
    decimal_amount = Decimal(str(amount))
    factor = Decimal(10) ** exponent
    return int((decimal_amount * factor).to_integral_value(rounding=ROUND_HALF_UP))


def from_minor(minor: int, currency: str) -> Decimal:
    """Convert integer minor units back to a Decimal amount."""
    exponent = get_exponent(currency)
    factor = Decimal(10) ** -exponent
    return Decimal(minor) * factor


@dataclass
class Money:
    """Represents an amount in a specific currency as integer minor units."""
    amount_minor: int
    currency: str
    
    def to_decimal(self) -> Decimal:
        """Convert to Decimal for display."""
        return from_minor(self.amount_minor, self.currency)
    
    def __repr__(self) -> str:
        return f"{self.amount_minor} {self.currency}"
    
    def __eq__(self, other) -> bool:
        if not isinstance(other, Money):
            return False
        return self.amount_minor == other.amount_minor and self.currency == other.currency
    
    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"Cannot add {self.currency} + {other.currency}")
        return Money(self.amount_minor + other.amount_minor, self.currency)
    
    def __sub__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract {self.currency} - {other.currency}")
        return Money(self.amount_minor - other.amount_minor, self.currency)
    
    def __mul__(self, factor: int) -> "Money":
        return Money(self.amount_minor * factor, self.currency)
    
    def __truediv__(self, divisor: int) -> "Money":
        return Money(self.amount_minor // divisor, self.currency)


def convert_currency(amount_minor: int, from_currency: str, to_currency: str, 
                     rate: float) -> int:
    """
    Convert between currencies using a historical FX rate.
    Handles different minor-unit exponents (e.g., USD cents to JPY yen).
    
    Formula: minor_dest = round(minor_src * rate * 10^(exp_dest - exp_src))
    """
    exp_src = get_exponent(from_currency)
    exp_dest = get_exponent(to_currency)
    
    # Use Decimal for precise conversion
    factor = Decimal(str(rate)) * (Decimal(10) ** (exp_dest - exp_src))
    result = Decimal(amount_minor) * factor
    
    # Round half-up to integer
    return int(result.to_integral_value(rounding=ROUND_HALF_UP))


def split_equal(total_minor: int, participants: List[int]) -> Dict[int, int]:
    """
    Split a total equally among participants using largest-remainder rounding.
    
    Returns: {user_id: owed_minor} for each participant
    The sum of all values equals total_minor exactly.
    """
    n = len(participants)
    if n == 0:
        return {}
    
    base = total_minor // n
    remainder = total_minor - base * n
    
    # Sort participants by ID for deterministic distribution
    sorted_participants = sorted(participants)
    
    result = {}
    for i, user_id in enumerate(sorted_participants):
        # First `remainder` participants get one extra minor unit
        result[user_id] = base + (1 if i < remainder else 0)
    
    return result


def split_percentage(total_minor: int, participants: List[Tuple[int, int]]) -> Dict[int, int]:
    """
    Split a total by percentage weights (basis points, summing to 10000).
    
    Args:
        total_minor: Total amount to split
        participants: List of (user_id, weight_in_basis_points)
    
    Returns:
        {user_id: owed_minor} for each participant
        Sum equals total_minor exactly using largest-remainder.
    """
    total_weight = sum(weight for _, weight in participants)
    if total_weight != 10000:
        raise ValueError(f"Weights must sum to 10000 basis points, got {total_weight}")
    
    # Calculate raw shares (may have fractional cents)
    shares = {}
    for user_id, weight in participants:
        raw_share = Decimal(total_minor) * Decimal(weight) / Decimal(10000)
        shares[user_id] = raw_share
    
    # Floor to get base allocation
    result = {}
    total_floored = 0
    for user_id, raw in shares.items():
        floored = int(raw)
        result[user_id] = floored
        total_floored += floored
    
    # Distribute remainder using largest fractional remainders
    remainder = total_minor - total_floored
    if remainder > 0:
        # Sort by fractional part (descending), then by user_id (for ties)
        remainder_allocation = sorted(
            [(user_id, float(raw - floored), user_id) 
             for user_id, floored in result.items()],
            key=lambda x: (-x[1], x[2])
        )
        for i in range(remainder):
            user_id = remainder_allocation[i][0]
            result[user_id] += 1
    
    return result


def split_custom(total_minor: int, participants: List[Tuple[int, int]]) -> Dict[int, int]:
    """
    Custom split where participants specify exact amounts.
    Validates that the sum equals total_minor.
    
    Args:
        total_minor: Total amount to split
        participants: List of (user_id, exact_minor_amount)
    
    Returns:
        {user_id: owed_minor} if validation passes
    
    Raises:
        ValueError: If the sum doesn't match total_minor
    """
    total_allocated = sum(amount for _, amount in participants)
    if total_allocated != total_minor:
        raise ValueError(
            f"Custom split amounts must sum to {total_minor}, got {total_allocated}"
        )
    
    return {user_id: amount for user_id, amount in participants}


# Example usage and tests
if __name__ == "__main__":
    # Test currency conversion
    usd_amount = to_minor(10.50, "USD")  # 1050 cents
    eur_amount = convert_currency(usd_amount, "USD", "EUR", 0.92)
    print(f"$10.50 = {from_minor(eur_amount, 'EUR')} EUR")
    
    # Test equal split
    total = 1000  # $10.00
    participants = [1, 2, 3]
    split = split_equal(total, participants)
    print(f"Split $10.00 among {participants}: {split}, sum={sum(split.values())}")
    
    # Test percentage split
    participants_pct = [(1, 5000), (2, 3000), (3, 2000)]  # 50%, 30%, 20%
    split_pct = split_percentage(total, participants_pct)
    print(f"Split $10.00 by 50/30/20: {split_pct}, sum={sum(split_pct.values())}")
    
    # Test custom split
    participants_custom = [(1, 400), (2, 300), (3, 300)]
    split_custom_result = split_custom(total, participants_custom)
    print(f"Custom split $10.00: {split_custom_result}, sum={sum(split_custom_result.values())}")