import re
from dataclasses import dataclass


@dataclass
class IntBound:
    value: int


@dataclass
class DeltaBound:
    sign: int  # +1 or -1
    amount: int
    unit: str  # 'd', 'h', 'm'


@dataclass
class CalendarBound:
    offset: int  # 0 = current, -1 = previous, +1 = next
    period: str  # 'M', 'W', 'Y'


@dataclass
class IndexPosition:
    bound: IntBound


@dataclass
class SlicePosition:
    start: "IntBound | DeltaBound | CalendarBound | None"
    end: "IntBound | DeltaBound | CalendarBound | None"


_INT_RE = re.compile(r"^(-?\d+)$")
_DELTA_RE = re.compile(r"^([+-])(\d+)([dhm])$")
_CALENDAR_RE = re.compile(r"^([+-]?)(\d+)([MWY])$")


def _parse_bound(s: str) -> "IntBound | DeltaBound | CalendarBound":
    m = _INT_RE.match(s)
    if m:
        return IntBound(int(m.group(1)))

    m = _DELTA_RE.match(s)
    if m:
        sign = 1 if m.group(1) == "+" else -1
        return DeltaBound(sign, int(m.group(2)), m.group(3))

    m = _CALENDAR_RE.match(s)
    if m:
        sign_str, amount_str, period = m.group(1), m.group(2), m.group(3)
        amount = int(amount_str)
        offset = -amount if sign_str == "-" else amount
        return CalendarBound(offset, period)

    raise ValueError(f"Invalid bound expression: {s!r}")


def parse_position(position: str) -> "IndexPosition | SlicePosition":
    s = position.strip()
    if s.startswith("[") and s.endswith("]"):
        s = s[1:-1]

    if s.count(":") > 1:
        raise ValueError(f"Too many colons in position expression: {position!r}")

    if ":" not in s:
        m = _INT_RE.match(s)
        if m:
            return IndexPosition(IntBound(int(m.group(1))))
        if _DELTA_RE.match(s) or _CALENDAR_RE.match(s):
            raise ValueError(
                f"Calendar and time-delta expressions are not valid as a single index: {s!r}"
            )
        raise ValueError(f"Invalid position expression: {position!r}")

    left, right = s.split(":", 1)
    start = _parse_bound(left) if left else None
    end = _parse_bound(right) if right else None
    return SlicePosition(start, end)
