import calendar
from datetime import date, timedelta
from decimal import ROUND_HALF_UP, Decimal

from ccma.domain.contributions import contribution_override_covers_month
from ccma.domain.dates import DateValidationError, parse_iso_date
from ccma.rules.api import RuleContext, create_claim
from ccma.rules.scripts._shared import CONTRIBUTION_STATUSES

RULE_ID = "contribution-claims"
ORDER = 40
CENT = Decimal("0.01")
MONTH_NAMES = (
    "",
    "Januar",
    "Februar",
    "März",
    "April",
    "Mai",
    "Juni",
    "Juli",
    "August",
    "September",
    "Oktober",
    "November",
    "Dezember",
)


def evaluate(context: RuleContext):
    member = context.member
    if member.honorary or member.status not in CONTRIBUTION_STATUSES:
        return []
    try:
        accepted_at = parse_iso_date(member.accepted_at, "Aufnahmebeschluss")
        started_at = parse_iso_date(member.membership_started_at, "Mitglied seit") or accepted_at
        ended_at = parse_iso_date(member.membership_ended_at, "Austrittsdatum")
    except DateValidationError:
        return []
    if not accepted_at or not started_at:
        return []

    actions = []
    admission_rule = _rule_for(context.repository_config, accepted_at)
    if admission_rule:
        admission_fee = Decimal(str(admission_rule.get("admission_fee", "0")))
        if admission_fee > 0:
            due_days = int(admission_rule.get("first_payment_due_days_after_acceptance", 28))
            admission_due = accepted_at + timedelta(days=due_days)
            if not ended_at or admission_due <= ended_at:
                actions.append(
                    create_claim(
                        rule_id=RULE_ID,
                        member=member,
                        claim_key="admission-fee",
                        payload={
                            "type": "admission_fee",
                            "title": "Aufnahmegebühr",
                            "amount": _money(admission_fee),
                            "due_date": admission_due.isoformat(),
                            "calculation": {"rule_id": admission_rule.get("rule_id", "")},
                        },
                    )
                )

    year_from = (
        started_at.year
        if getattr(context.settings, "retroactive_claims", False)
        else context.today.year
    )
    for year in range(year_from, context.today.year + 2):
        actions.extend(_membership_claims(context, started_at, accepted_at, ended_at, year))
    return actions


def _membership_claims(
    context: RuleContext,
    started_at: date,
    accepted_at: date,
    ended_at: date | None,
    year: int,
):
    member = context.member
    period_start = max(started_at, date(year, 1, 1))
    if period_start.year > year:
        return []
    rule = _rule_for(context.repository_config, period_start)
    if not rule:
        return []
    annual_amount = Decimal(str(rule.get("annual_amount", "0")))
    issue_days = int(rule.get("issue_days_before_due", 30))
    due_days_after_entry = int(rule.get("first_payment_due_days_after_acceptance", 28))
    if annual_amount <= 0:
        return []

    if member.payment_frequency == "semiannual":
        configured_due_dates = list(rule.get("semiannual_due") or ["01-31", "07-31"])
        while len(configured_due_dates) < 2:
            configured_due_dates.append(("01-31", "07-31")[len(configured_due_dates)])
        periods = [
            ("first-half", 1, 6, _due_date(year, configured_due_dates[0], "01-31")),
            ("second-half", 7, 12, _due_date(year, configured_due_dates[1], "07-31")),
        ]
    elif member.payment_frequency == "quarterly":
        periods = [
            (
                f"quarter-{quarter}",
                first_month,
                first_month + 2,
                _recurring_due_date(year, first_month, rule.get("annual_due"), "01-31"),
            )
            for quarter, first_month in enumerate((1, 4, 7, 10), start=1)
        ]
    elif member.payment_frequency == "monthly":
        periods = [
            (
                f"month-{month:02d}",
                month,
                month,
                _recurring_due_date(year, month, rule.get("annual_due"), "01-31"),
            )
            for month in range(1, 13)
        ]
    else:
        periods = [("annual", 1, 12, _due_date(year, rule.get("annual_due"), "01-31"))]

    actions = []
    overrides = context.contributions.contribution_overrides
    for suffix, first_month, last_month, regular_due in periods:
        # The entry year is intentionally billed from the entry month onward,
        # even when retroactive claims create old membership-fee claims.
        charged_from = max(first_month, period_start.month)
        months = max(0, last_month - charged_from + 1)
        if months == 0:
            continue
        # Blended month by month rather than a single rate for the whole period, so a
        # contribution override that starts or ends mid-period (e.g. a student discount
        # beginning in March for a member who pays semiannually) is billed correctly.
        month_amounts = [
            _monthly_amount(context.repository_config, overrides, year, month)
            for month in range(charged_from, last_month + 1)
        ]
        amount = sum((value for value, _override_id in month_amounts), Decimal("0")).quantize(
            CENT, rounding=ROUND_HALF_UP
        )
        applied_overrides = sorted({override_id for _value, override_id in month_amounts if override_id})
        entry_year = started_at.year == year
        issue_date = regular_due - timedelta(days=issue_days)
        if not entry_year and context.today < issue_date:
            continue
        due_date = regular_due
        if entry_year and regular_due < accepted_at + timedelta(days=due_days_after_entry):
            due_date = accepted_at + timedelta(days=due_days_after_entry)
        if ended_at and due_date > ended_at:
            continue
        claim_key = f"membership-fee:{year}:{suffix}"
        full_period_months = last_month - first_month + 1
        description = _title(year, suffix)
        if months < full_period_months:
            unit = "Monat" if months == 1 else "Monate"
            description = f"{description} ({months} {unit})"
        actions.append(
            create_claim(
                rule_id=RULE_ID,
                member=member,
                claim_key=claim_key,
                payload={
                    "type": "membership_fee",
                    "title": _title(year, suffix),
                    "description": description,
                    "amount": _money(amount),
                    "due_date": due_date.isoformat(),
                    "service_period": {
                        "from": date(year, charged_from, 1).isoformat(),
                        "until": date(year, last_month, calendar.monthrange(year, last_month)[1]).isoformat(),
                    },
                    "calculation": {
                        "rule_id": rule.get("rule_id", ""),
                        "annual_amount": _money(annual_amount),
                        "months": months,
                        "formula": "annual_amount * months / 12",
                        "contribution_override_ids": applied_overrides,
                    },
                },
            )
        )
    return actions


def _monthly_amount(config, overrides, year: int, month: int) -> tuple[Decimal, str]:
    """Returns the effective monthly rate for a single calendar month, and the
    override's id if one applied (empty string otherwise)."""
    rule = _rule_for(config, date(year, month, 1))
    base_annual = Decimal(str(rule.get("annual_amount", "0"))) if rule else Decimal("0")
    month_key = f"{year:04d}-{month:02d}"
    override = next(
        (item for item in overrides if contribution_override_covers_month(item, month_key)), None
    )
    if override is None:
        return base_annual / Decimal(12), ""
    kind = str(override.get("kind", ""))
    value = Decimal(str(override.get("value", "0")))
    if kind == "amount":
        annual = value
    elif kind == "percent":
        annual = base_annual * (Decimal(100) - value) / Decimal(100)
    else:
        annual = base_annual
    return annual / Decimal(12), str(override.get("override_id", ""))


def _rule_for(config, target: date):
    selected = None
    for rule in config.get("contribution_rules", []):
        try:
            valid_from = date.fromisoformat(str(rule.get("valid_from", "")))
            valid_until_raw = rule.get("valid_until")
            valid_until = date.fromisoformat(str(valid_until_raw)) if valid_until_raw else None
        except ValueError:
            continue
        if valid_from <= target and (not valid_until or target <= valid_until):
            if selected is None or valid_from > selected[0]:
                selected = (valid_from, rule)
    return selected[1] if selected else None


def _money(value: Decimal) -> str:
    return str(value.quantize(CENT, rounding=ROUND_HALF_UP))


def _due_date(year: int, value, fallback: str) -> date:
    try:
        month, day = (int(part) for part in str(value or fallback).split("-", 1))
        return date(year, month, day)
    except (TypeError, ValueError):
        month, day = (int(part) for part in fallback.split("-", 1))
        return date(year, month, day)


def _recurring_due_date(year: int, month: int, value, fallback: str) -> date:
    try:
        _configured_month, day = (int(part) for part in str(value or fallback).split("-", 1))
    except (TypeError, ValueError):
        day = int(fallback.split("-", 1)[1])
    return date(year, month, min(day, calendar.monthrange(year, month)[1]))


def _title(year: int, suffix: str) -> str:
    if suffix.startswith("month-"):
        month = int(suffix.removeprefix("month-"))
        return f"Mitgliedsbeitrag {MONTH_NAMES[month]} {year}"
    if suffix.startswith("quarter-"):
        quarter = int(suffix.removeprefix("quarter-"))
        return f"Mitgliedsbeitrag {quarter}. Quartal {year}"
    if suffix == "first-half":
        return f"Mitgliedsbeitrag 1. Halbjahr {year}"
    if suffix == "second-half":
        return f"Mitgliedsbeitrag 2. Halbjahr {year}"
    return f"Mitgliedsbeitrag {year}"
