"""Semester helper class

A small utility around the *German* academic calendar that distinguishes
between *Wintersemester* (WiSe) and *Sommersemester* (SoSe).

Key points
----------
* A **`Semester`** is identified by a *term* ("SoSe" or "WiSe") and the last two
  digits of the calendar year in which the term *starts*.
* Formatting **never** pads the year with a leading zero – so ``6`` stays ``6``.
* ``offset(n)`` and the static ``generate_missing`` reliably walk the timeline
  one semester at a time with correct year transitions:

  SoSe 6 → **WiSe 6/7** → SoSe 7 → WiSe 7/8 → …
"""

from __future__ import annotations
import datetime
import re
from dataclasses import dataclass
import loguru
import sys
from src import LOG_DIR
log = loguru.logger
log.remove()
log.add(sys.stdout, level="INFO")
log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days")



# @dataclass
class Semester:
    """Represents a German university semester (WiSe or SoSe)."""

    # ------------------------------------------------------------------
    # Class‑level defaults – will be *copied* to each instance and then
    # potentially overwritten in ``__init__``.
    # ------------------------------------------------------------------
    _year: int | None = int(str(datetime.datetime.now().year)[2:])  # 24 → 24
    _semester: str | None = None  # "WiSe" or "SoSe" – set later
    _month: int | None = datetime.datetime.now().month
    value: str | None = None  # Human‑readable label, e.g. "WiSe 23/24"

    # ------------------------------------------------------------------
    # Construction helpers
    # ------------------------------------------------------------------
    def __init__(
        self,
        year: int | None = None,
        semester: str | None = None,
        month: int | None = None,
    ) -> None:
        if year is not None:
            self._year = int(year)
        if semester is not None:
            if semester not in ("WiSe", "SoSe"):
                raise ValueError("semester must be 'WiSe' or 'SoSe'")
            self._semester = semester
        if month is not None:
            self._month = month

        self.__post_init__()

    def __post_init__(self) -> None:  # noqa: D401 – keep original name
        if self._year is None:
            self._year = int(str(datetime.datetime.now().year)[2:])
        if self._month is None:
            self._month = datetime.datetime.now().month
        if self._semester is None:
            self._generate_semester_from_month()
        self._compute_value()

    # ------------------------------------------------------------------
    # Dunder helpers
    # ------------------------------------------------------------------
    def __str__(self) -> str:  # noqa: D401 – keep original name
        return self.value or "<invalid Semester>"

    def __repr__(self) -> str:  # Helpful for debugging lists
        return f"Semester({self._year!r}, {self._semester!r})"

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------
    def _generate_semester_from_month(self) -> None:
        """Infer *WiSe* / *SoSe* from the month attribute."""
        self._semester = "WiSe" if (self._month <= 3 or self._month > 9) else "SoSe"

    def _compute_value(self) -> None:
        """Human‑readable semester label – e.g. ``WiSe 23/24`` or ``SoSe 24``."""
        year = self._year
        if self._semester == "WiSe":
            next_year = (year + 1) % 100  # wrap 99 → 0
            self.value = f"WiSe {year}/{next_year}"
        else:  # SoSe
            self.value = f"SoSe {year}"

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def offset(self, value: int) -> "Semester":
        """Return a new :class:`Semester` *value* steps away.

        The algorithm maps every semester to a monotonically increasing
        *linear index* so that simple addition suffices:

        ``index = year * 2 + (0 if SoSe else 1)``.
        """
        if not isinstance(value, int):
            raise TypeError("value must be an int (number of semesters to jump)")
        if value == 0:
            return Semester(self._year, self._semester)

        current_idx = self._year * 2 + (0 if self._semester == "SoSe" else 1)
        target_idx = current_idx + value
        if target_idx < 0:
            raise ValueError("offset would result in a negative year – not supported")

        new_year, semester_bit = divmod(target_idx, 2)
        new_semester = "SoSe" if semester_bit == 0 else "WiSe"
        return Semester(new_year, new_semester)

    # ------------------------------------------------------------------
    # Comparison helpers
    # ------------------------------------------------------------------
    def isPastSemester(self, other: "Semester") -> bool:
        if self.year < other.year:
            return True
        if self.year == other.year:
            return (
                self.semester == "WiSe" and other.semester == "SoSe"
            )  # WiSe before next SoSe
        return False

    def isFutureSemester(self, other: "Semester") -> bool:
        if self.year > other.year:
            return True
        if self.year == other.year:
            return (
                self.semester == "SoSe" and other.semester == "WiSe"
            )  # SoSe after WiSe of same year
        return False

    def isMatch(self, other: "Semester") -> bool:
        return self.year == other.year and self.semester == other.semester

    # ------------------------------------------------------------------
    # Convenience properties
    # ------------------------------------------------------------------
    @property
    def next(self) -> "Semester":
        return self.offset(1)

    @property
    def previous(self) -> "Semester":
        return self.offset(-1)

    @property
    def year(self) -> int:
        return self._year

    @property
    def semester(self) -> str:
        return self._semester

    # ------------------------------------------------------------------
    # Static helpers
    # ------------------------------------------------------------------
    @staticmethod
    def generate_missing(start: "Semester", end: "Semester") -> list[str]:
        """Return all consecutive semesters from *start* to *end* (inclusive)."""
        if not isinstance(start, Semester) or not isinstance(end, Semester):
            raise TypeError("start and end must be Semester instances")
        if start.isFutureSemester(end) and not start.isMatch(end):
            raise ValueError("'start' must not be after 'end'")

        chain: list[Semester] = [start.value]
        current = start
        while not current.isMatch(end):
            current = current.next
            chain.append(current.value)
            if len(chain) > 1000:  # sanity guard
                raise RuntimeError("generate_missing exceeded sane iteration limit")
        return chain

    # ------------------------------------------------------------------
    # Parsing helper
    # ------------------------------------------------------------------
    @classmethod
    def from_string(cls, s: str) -> "Semester":
        """Parse a human‑readable semester label and return a :class:`Semester`.

        Accepted formats (case‑insensitive)::

            "SoSe <YY>"            → SoSe of year YY
            "WiSe <YY>/<YY+1>"     → Winter term starting in YY
            "WiSe <YY>"            → Shorthand for the above (next year implied)

        ``YY`` may contain a leading zero ("06" → 6).
        """
        if not isinstance(s, str):
            raise TypeError("s must be a string")

        pattern = r"\s*(WiSe|SoSe)\s+(\d{1,2})(?:\s*/\s*(\d{1,2}))?\s*"
        m = re.fullmatch(pattern, s, flags=re.IGNORECASE)
        if not m:
            raise ValueError(
                "invalid semester string format – expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)"
            )

        term_raw, y1_str, y2_str = m.groups()
        term = term_raw.capitalize()  # normalize case → "WiSe" or "SoSe"
        year = int(y1_str.lstrip("0") or "0")  # "06" → 6, "0" stays 0

        if term == "SoSe":
            if y2_str is not None:
                raise ValueError(
                    "SoSe string should not contain '/' followed by a second year"
                )
            return cls(year, "SoSe")

        # term == "WiSe"
        if y2_str is not None:
            next_year = int(y2_str.lstrip("0") or "0")
            expected_next = (year + 1) % 100
            if next_year != expected_next:
                raise ValueError("WiSe second year must equal first year + 1 (mod 100)")
        # Accept both explicit "WiSe 6/7" and shorthand "WiSe 6"
        return cls(year, "WiSe")


# ------------------------- quick self‑test -------------------------
if __name__ == "__main__":
    # Chain generation demo ------------------------------------------------
    s_start = Semester(6, "SoSe")  # SoSe 6
    s_end = Semester(25, "WiSe")  # WiSe 25/26
    chain = Semester.generate_missing(s_start, s_end)
    print("generate_missing:", [str(s) for s in chain])

    # Parsing demo ---------------------------------------------------------
    for label in ["SoSe 6", "WiSe 6/7", "wise 23/24", "WiSe 9"]:
        print("from_string:", label, "→", Semester.from_string(label))
