import re
from datetime import date, datetime, time, timedelta
from typing import Union


DateInput = Union[datetime, date, str, int, float]

class DateService:
    MONTHS_SHORT = ("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")
    TOKEN_REGEX = re.compile(r"YYYY|yy|MMM|MM|M|DD|D|HH|H|mm|m|ss|s")
    CUSTOM_MONTHS = {
        "JAN": 1,
        "FEB": 2,
        "MAR": 3,
        "APR": 4,
        "MAY": 5,
        "JUN": 6,
        "JUL": 7,
        "AUG": 8,
        "SEP": 9,
        "OCT": 10,
        "NOV": 11,
        "DEC": 12,
    }

    def _parse_input_date(self, value: DateInput) -> datetime:
        if isinstance(value, datetime):
            return value
        if isinstance(value, date):
            return datetime.combine(value, time.min)
        if isinstance(value, (int, float)):
            return datetime.fromtimestamp(value / 1000)
        if isinstance(value, str):
            normalized = value.replace("Z", "+00:00")
            try:
                return datetime.fromisoformat(normalized)
            except ValueError as exc:
                raise ValueError(f"Invalid date input string: {value}") from exc
        raise TypeError(f"Unsupported date input type: {type(value)!r}")

    def get_date_plus_milliseconds(self, input_value: DateInput, milliseconds: int) -> datetime:
        base_time = self._parse_input_date(input_value)
        return base_time + timedelta(milliseconds=milliseconds)

    def get_date_plus_days(self, input_value: DateInput, days: int) -> datetime:
        base_time = self._parse_input_date(input_value)
        midnight = base_time.replace(hour=0, minute=0, second=0, microsecond=0)
        return midnight + timedelta(days=days)

    def get_date_without_time_from_unix(self, unix_timestamp: int) -> datetime:
        base_time = datetime.fromtimestamp(unix_timestamp)
        return base_time.replace(hour=0, minute=0, second=0, microsecond=0)

    def date_formatter(self, input_date: DateInput, fmt: str) -> str:
        current = self._parse_input_date(input_date)
        year = current.year
        month = current.month
        day = current.day
        hour = current.hour
        minute = current.minute
        second = current.second

        def _pad2(number: int) -> str:
            return f"{number:02d}"

        def _replace(match: re.Match) -> str:
            token = match.group(0)
            if token == "YYYY":
                return str(year)
            if token == "yy":
                return str(year)[-2:]
            if token == "MMM":
                return self.MONTHS_SHORT[month - 1]
            if token == "MM":
                return _pad2(month)
            if token == "M":
                return str(month)
            if token == "DD":
                return _pad2(day)
            if token == "D":
                return str(day)
            if token == "HH":
                return _pad2(hour)
            if token == "H":
                return str(hour)
            if token == "mm":
                return _pad2(minute)
            if token == "m":
                return str(minute)
            if token == "ss":
                return _pad2(second)
            if token == "s":
                return str(second)
            return token

        return self.TOKEN_REGEX.sub(_replace, fmt)

    def parse_custom_date(self, date_str: str) -> datetime:
        if len(date_str) != 9:
            raise ValueError("Invalid date format. Expected DDMMMYYYY (e.g., 07MAR2025)")

        day = int(date_str[:2])
        month_str = date_str[2:5].upper()
        year = int(date_str[5:9])

        month = self.CUSTOM_MONTHS.get(month_str)
        if month is None:
            raise ValueError(f"Invalid month in date string: {month_str}")

        return datetime(year, month, day)

    def get_current_date_without_time(self) -> datetime:
        now = datetime.now()
        return now.replace(hour=0, minute=0, second=0, microsecond=0)
