from __future__ import annotations

from datetime import date
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from typing import Any

from ccma.domain.models import ContributionData

CENT = Decimal("0.01")

CLAIM_STATUS_LABELS = {
    "open": "OFFEN",
    "partially_paid": "TEILBEZAHLT",
    "paid": "BEZAHLT",
    "overpaid": "ÜBERZAHLT",
    "overdue": "ÜBERFÄLLIG",
    "credit": "GUTSCHRIFT",
    "cancelled": "STORNIERT",
}

DONATION_STATUS_LABELS = {
    "open": "OFFEN",
    "partially_allocated": "TEILWEISE ZUGEORDNET",
    "allocated": "ZUGEORDNET",
    "overallocated": "ÜBERZUGEORDNET",
}

CONTRIBUTION_OVERRIDE_KIND_LABELS = {
    "amount": "BETRAG",
    "percent": "PROZENT",
}


def decimal_value(value: Any, field_name: str = "Betrag") -> Decimal:
    text = str(value).strip().replace(",", ".")
    try:
        return Decimal(text).quantize(CENT, rounding=ROUND_HALF_UP)
    except (InvalidOperation, ValueError) as exc:
        raise ValueError(f"{field_name} ist kein gültiger Geldbetrag.") from exc


def money_text(value: Decimal | str) -> str:
    return f"{decimal_value(value):.2f}"


def claim_items(claim: dict[str, Any]) -> list[dict[str, Any]]:
    items = claim.get("items")
    if isinstance(items, list) and items:
        return items
    amount = decimal_value(claim.get("amount", "0"))
    return [
        {
            "item_id": "legacy-base",
            "type": "base",
            "description": str(claim.get("title") or "Forderung"),
            "quantity": "1.00",
            "unit_price": money_text(amount),
            "amount": money_text(amount),
        }
    ]


def materialize_claim_items(claim: dict[str, Any]) -> list[dict[str, Any]]:
    if not isinstance(claim.get("items"), list) or not claim["items"]:
        claim["items"] = claim_items(claim)
    return claim["items"]


def claim_total(claim: dict[str, Any]) -> Decimal:
    return sum((decimal_value(item.get("amount", "0")) for item in claim_items(claim)), Decimal("0"))


def allocation_effect(data: ContributionData, allocation: dict[str, Any]) -> Decimal:
    amount = decimal_value(allocation.get("amount", "0"))
    if str(allocation.get("credit_id", "")):
        return -amount
    return amount


def allocated_total(data: ContributionData, claim_id: str) -> Decimal:
    return sum(
        (
            allocation_effect(data, allocation)
            for allocation in data.allocations
            if str(allocation.get("claim_id", "")) == claim_id
        ),
        Decimal("0"),
    )


def claim_settled_total(data: ContributionData, claim: dict[str, Any]) -> Decimal:
    allocated = allocated_total(data, str(claim.get("claim_id", "")))
    if claim_total(claim) < 0:
        return abs(allocated).quantize(CENT)
    return allocated.quantize(CENT)


def payment_allocated_total(data: ContributionData, payment_id: str) -> Decimal:
    return sum(
        (
            decimal_value(allocation.get("amount", "0"))
            for allocation in data.allocations
            if str(allocation.get("payment_id", "")) == payment_id
        ),
        Decimal("0"),
    )


def credit_allocated_total(data: ContributionData, credit_id: str) -> Decimal:
    return sum(
        (
            decimal_value(allocation.get("amount", "0"))
            for allocation in data.allocations
            if str(allocation.get("credit_id", "")) == credit_id
        ),
        Decimal("0"),
    )


def claim_balance(data: ContributionData, claim: dict[str, Any]) -> Decimal:
    return (claim_total(claim) - allocated_total(data, str(claim.get("claim_id", "")))).quantize(CENT)


def donation_allocated_total(data: ContributionData, donation_id: str) -> Decimal:
    return sum(
        (
            decimal_value(allocation.get("amount", "0"))
            for allocation in data.allocations
            if str(allocation.get("donation_id", "")) == donation_id
        ),
        Decimal("0"),
    )


def donation_amount(donation: dict[str, Any]) -> Decimal:
    return decimal_value(donation.get("amount", "0"))


def donation_balance(data: ContributionData, donation: dict[str, Any]) -> Decimal:
    donation_id = str(donation.get("donation_id", ""))
    return (donation_amount(donation) - donation_allocated_total(data, donation_id)).quantize(CENT)


def donation_status(data: ContributionData, donation: dict[str, Any]) -> str:
    balance = donation_balance(data, donation)
    allocated = donation_allocated_total(data, str(donation.get("donation_id", "")))
    if balance < 0:
        return "overallocated"
    if balance == 0:
        return "allocated"
    if allocated > 0:
        return "partially_allocated"
    return "open"


def reminder_items_total(reminder: dict[str, Any]) -> Decimal:
    return sum(
        (decimal_value(item.get("amount", "0")) for item in reminder.get("items") or []),
        Decimal("0"),
    )


def contribution_override_covers_month(override: dict[str, Any], month: str) -> bool:
    valid_from = str(override.get("valid_from", ""))
    valid_until = str(override.get("valid_until", "") or "")
    if valid_from and month < valid_from:
        return False
    if valid_until and month > valid_until:
        return False
    return True


def contribution_override_ranges_overlap(
    valid_from: str, valid_until: str, other_from: str, other_until: str
) -> bool:
    """Month ranges ('YYYY-MM', empty valid_until/other_until = open-ended) overlap
    if each range starts no later than the other one ends."""
    starts_before_other_ends = not other_until or valid_from <= other_until
    other_starts_before_ends = not valid_until or other_from <= valid_until
    return starts_before_other_ends and other_starts_before_ends


def claim_status(data: ContributionData, claim: dict[str, Any], *, today: date | None = None) -> str:
    if str(claim.get("status", "")) == "cancelled":
        return "cancelled"
    total = claim_total(claim)
    paid = allocated_total(data, str(claim.get("claim_id", "")))
    balance = total - paid
    if total < 0:
        return "credit"
    if balance < 0:
        return "overpaid"
    if balance == 0:
        return "paid"
    if paid > 0:
        return "partially_paid"
    try:
        due = date.fromisoformat(str(claim.get("due_date", "")))
    except ValueError:
        due = None
    if due and due < (today or date.today()):
        return "overdue"
    return "open"
