from __future__ import annotations

import calendar
import re
from dataclasses import dataclass
from datetime import date, timedelta


class IntervalValidationError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class AnniversaryInterval:
    value: int
    unit: str

    @property
    def token(self) -> str:
        return f"{self.value}{self.unit}"

    @property
    def label(self) -> str:
        labels = {
            "D": "Tag" if self.value == 1 else "Tage",
            "M": "Monat" if self.value == 1 else "Monate",
            "Y": "Jahr" if self.value == 1 else "Jahre",
        }
        return f"{self.value} {labels[self.unit]}"

    def target_date(self, start: date) -> date:
        if self.unit == "D":
            return start + timedelta(days=self.value)
        if self.unit == "M":
            return _add_months(start, self.value)
        return _add_years(start, self.value)


def parse_anniversary_intervals(value: str) -> list[AnniversaryInterval]:
    tokens = [token.strip().upper() for token in re.split(r"[;,]", value) if token.strip()]
    if not tokens:
        raise IntervalValidationError("Mindestens ein Jubiläumsintervall ist erforderlich.")
    intervals: list[AnniversaryInterval] = []
    seen: set[str] = set()
    maximums = {"D": 36_600, "M": 1_200, "Y": 100}
    for token in tokens:
        match = re.fullmatch(r"(\d+)([DMY]?)", token)
        if not match:
            raise IntervalValidationError(
                f"Ungültiges Intervall {token!r}. Erwartet werden Angaben wie 1, 30D, 2M oder 10Y."
            )
        amount, unit = int(match.group(1)), match.group(2) or "Y"
        if amount < 1 or amount > maximums[unit]:
            raise IntervalValidationError(f"Intervall {token!r} liegt außerhalb des erlaubten Bereichs.")
        normalized = f"{amount}{unit}"
        if normalized not in seen:
            intervals.append(AnniversaryInterval(amount, unit))
            seen.add(normalized)
    return intervals


def normalize_anniversary_intervals(value: str) -> str:
    return ";".join(interval.token for interval in parse_anniversary_intervals(value))


def _add_months(value: date, months: int) -> date:
    month_index = value.month - 1 + months
    year = value.year + month_index // 12
    month = month_index % 12 + 1
    day = min(value.day, calendar.monthrange(year, month)[1])
    return date(year, month, day)


def _add_years(value: date, years: int) -> date:
    target_year = value.year + years
    day = min(value.day, calendar.monthrange(target_year, value.month)[1])
    return date(target_year, value.month, day)
