from __future__ import annotations

from datetime import datetime, timedelta
from typing import Optional, Tuple
import re

from dateutil import parser as dateutil_parser

from analogai.foundation.core.value_objects.time import Time


class TimeParser:
    _NUMBER_WORDS = {
        "zero": 0,
        "one": 1,
        "two": 2,
        "three": 3,
        "four": 4,
        "five": 5,
        "six": 6,
        "seven": 7,
        "eight": 8,
        "nine": 9,
        "ten": 10,
        "eleven": 11,
        "twelve": 12,
        "thirteen": 13,
        "fourteen": 14,
        "fifteen": 15,
        "sixteen": 16,
        "seventeen": 17,
        "eighteen": 18,
        "nineteen": 19,
        "twenty": 20,
        "thirty": 30,
        "forty": 40,
        "fifty": 50,
        "sixty": 60,
        "seventy": 70,
        "eighty": 80,
        "ninety": 90,
    }

    _TIME_UNITS = {
        "second": 1,
        "seconds": 1,
        "sec": 1,
        "secs": 1,
        "minute": 60,
        "minutes": 60,
        "min": 60,
        "mins": 60,
        "hour": 3600,
        "hours": 3600,
        "hr": 3600,
        "hrs": 3600,
        "day": 86400,
        "days": 86400,
        "week": 604800,
        "weeks": 604800,
        "month": 2592000,
        "months": 2592000,
        "year": 31536000,
        "years": 31536000,
    }

    _WEEKDAYS = {
        "monday": 0,
        "mon": 0,
        "tuesday": 1,
        "tue": 1,
        "tues": 1,
        "wednesday": 2,
        "wed": 2,
        "thursday": 3,
        "thu": 3,
        "thur": 3,
        "thurs": 3,
        "friday": 4,
        "fri": 4,
        "saturday": 5,
        "sat": 5,
        "sunday": 6,
        "sun": 6,
    }

    @staticmethod
    def parse(time_str: str, timezone: Optional[str] = None) -> Optional[Time]:
        if not time_str:
            return None

        time_str = time_str.strip().lower()

        from analogai.foundation.common.utils.time_utils.timezone_utils import TimezoneUtils
        now = TimezoneUtils.get_timezone_aware_datetime(timezone)

        handlers = (
            TimeParser._parse_now_keyword,
            TimeParser._parse_relative_time_handler,
            TimeParser._parse_year,
            TimeParser._parse_day_keyword,
            TimeParser._parse_weekday,
            TimeParser._parse_compound_date_time,
            TimeParser._parse_time_only,
            TimeParser._parse_date_only,
            TimeParser._parse_dateutil,
        )

        for handler in handlers:
            parsed = handler(time_str, now)
            if parsed is not None:
                return parsed

        return None

    @staticmethod
    def parse_day_range(time_str: str, timezone: Optional[str] = None) -> Tuple[Optional[Time], Optional[Time]]:
        from analogai.foundation.common.utils.time_utils.timezone_utils import TimezoneUtils

        if not time_str:
            return None, None

        time_str = time_str.strip().lower()
        now = TimezoneUtils.get_timezone_aware_datetime(timezone)

        handlers = (
            TimeParser._parse_day_range_now,
            TimeParser._parse_day_range_today,
            TimeParser._parse_day_range_tomorrow,
            TimeParser._parse_day_range_yesterday,
        )

        for handler in handlers:
            parsed = handler(time_str, now)
            if parsed is not None:
                return parsed

        parsed_time = TimeParser.parse(time_str, timezone)
        return parsed_time, None

    @staticmethod
    def _parse_now_keyword(time_str: str, now: datetime) -> Optional[Time]:
        if time_str != "now":
            return None
        return Time(
            year=int(now.year),
            month=int(now.month),
            day=int(now.day),
            hour=int(now.hour),
            minute=None,
        )

    @staticmethod
    def _parse_relative_time_handler(time_str: str, now: datetime) -> Optional[Time]:
        return TimeParser._parse_relative_time(time_str, now)

    @staticmethod
    def _parse_year(time_str: str, now: datetime) -> Optional[Time]:
        if time_str.isdigit() and len(time_str) == 4:
            return Time(year=int(time_str))
        return None

    @staticmethod
    def _parse_day_keyword(time_str: str, now: datetime) -> Optional[Time]:
        if time_str == "today":
            return Time(year=int(now.year), month=int(now.month), day=int(now.day))
        if time_str == "tomorrow":
            tomorrow = now + timedelta(days=1)
            return Time(year=int(tomorrow.year), month=int(tomorrow.month), day=int(tomorrow.day))
        if time_str == "yesterday":
            yesterday = now - timedelta(days=1)
            return Time(year=int(yesterday.year), month=int(yesterday.month), day=int(yesterday.day))
        return None

    @staticmethod
    def _parse_weekday(time_str: str, now: datetime) -> Optional[Time]:
        if not time_str:
            return None
        parts = time_str.split(" ", 1)
        weekday_key = parts[0].strip().lower()
        weekday = TimeParser._WEEKDAYS.get(weekday_key)
        if weekday is None:
            return None

        delta_days = (weekday - now.weekday()) % 7
        target_date = now + timedelta(days=delta_days)

        hour = None
        minute = None
        if len(parts) > 1 and ":" in parts[1]:
            time_part = parts[1].strip()
            try:
                time_parts = time_part.split(":")
                hour = int(time_parts[0])
                minute = int(time_parts[1]) if len(time_parts) > 1 else 0
            except (ValueError, AttributeError, IndexError):
                hour = None
                minute = None

        return Time(
            year=int(target_date.year),
            month=int(target_date.month),
            day=int(target_date.day),
            hour=hour,
            minute=minute,
        )

    @staticmethod
    def _parse_compound_date_time(time_str: str, now: datetime) -> Optional[Time]:
        try:
            if " " not in time_str:
                return None
            parts = time_str.split(" ", 1)
            date_part = parts[0]
            time_part = parts[1] if len(parts) > 1 else None

            if date_part not in ["today", "tomorrow", "yesterday"]:
                return None

            base_date = now
            if date_part == "tomorrow":
                base_date = now + timedelta(days=1)
            elif date_part == "yesterday":
                base_date = now - timedelta(days=1)

            if time_part and ":" in time_part:
                time_parts = time_part.split(":")
                hour = int(time_parts[0])
                minute = int(time_parts[1]) if len(time_parts) > 1 else 0
                return Time(
                    year=int(base_date.year),
                    month=int(base_date.month),
                    day=int(base_date.day),
                    hour=hour,
                    minute=minute,
                )

            return Time(
                year=int(base_date.year),
                month=int(base_date.month),
                day=int(base_date.day),
            )
        except (ValueError, AttributeError, IndexError):
            return None

    @staticmethod
    def _parse_time_only(time_str: str, now: datetime) -> Optional[Time]:
        try:
            if ":" not in time_str:
                return None
            time_parts = time_str.split(":")
            hour = int(time_parts[0])
            minute = int(time_parts[1]) if len(time_parts) > 1 else 0
            return Time(
                year=int(now.year),
                month=int(now.month),
                day=int(now.day),
                hour=hour,
                minute=minute,
            )
        except (ValueError, AttributeError, IndexError):
            return None

    @staticmethod
    def _parse_date_only(time_str: str, now: datetime) -> Optional[Time]:
        try:
            if "-" not in time_str:
                return None
            parts = time_str.split("-")
            if len(parts) != 3:
                return None
            year = int(parts[0])
            month = int(parts[1])
            day = int(parts[2])
            return Time(year=year, month=month, day=day)
        except (ValueError, AttributeError, IndexError):
            return None

    @staticmethod
    def _parse_dateutil(time_str: str, now: datetime) -> Optional[Time]:
        if not re.search(r"\d", time_str):
            return None
        try:
            parsed = dateutil_parser.parse(
                time_str,
                dayfirst=True,
                yearfirst=False,
                fuzzy=False,
                default=now,
            )
        except (ValueError, TypeError, OverflowError):
            return None

        has_time = bool(re.search(r"\b\d{1,2}:\d{2}\b", time_str)) or "am" in time_str or "pm" in time_str
        hour = int(parsed.hour) if has_time else None
        minute = int(parsed.minute) if has_time else None

        return Time(
            year=int(parsed.year),
            month=int(parsed.month),
            day=int(parsed.day),
            hour=hour,
            minute=minute,
        )

    @staticmethod
    def _parse_day_range_now(time_str: str, now: datetime) -> Optional[Tuple[Time, Time]]:
        if time_str != "now":
            return None
        day_time = Time(
            year=int(now.year),
            month=int(now.month),
            day=int(now.day),
            hour=int(now.hour),
            minute=None,
        )
        return day_time, day_time

    @staticmethod
    def _parse_day_range_today(time_str: str, now: datetime) -> Optional[Tuple[Time, Time]]:
        if time_str != "today":
            return None
        start_time = Time(year=int(now.year), month=int(now.month), day=int(now.day), hour=0, minute=0)
        end_time = Time(year=int(now.year), month=int(now.month), day=int(now.day), hour=23, minute=59)
        return start_time, end_time

    @staticmethod
    def _parse_day_range_tomorrow(time_str: str, now: datetime) -> Optional[Tuple[Time, Time]]:
        if time_str != "tomorrow":
            return None
        tomorrow = now + timedelta(days=1)
        start_time = Time(year=int(tomorrow.year), month=int(tomorrow.month), day=int(tomorrow.day), hour=0, minute=0)
        end_time = Time(year=int(tomorrow.year), month=int(tomorrow.month), day=int(tomorrow.day), hour=23, minute=59)
        return start_time, end_time

    @staticmethod
    def _parse_day_range_yesterday(time_str: str, now: datetime) -> Optional[Tuple[Time, Time]]:
        if time_str != "yesterday":
            return None
        yesterday = now - timedelta(days=1)
        start_time = Time(year=int(yesterday.year), month=int(yesterday.month), day=int(yesterday.day), hour=0, minute=0)
        end_time = Time(year=int(yesterday.year), month=int(yesterday.month), day=int(yesterday.day), hour=23, minute=59)
        return start_time, end_time

    @staticmethod
    def _convert_number_word_to_int(word: str) -> Optional[int]:
        word = word.strip().lower()

        if word in ["a", "an"]:
            return 1

        if word in TimeParser._NUMBER_WORDS:
            return TimeParser._NUMBER_WORDS[word]

        if "-" in word:
            parts = word.split("-")
            if len(parts) == 2:
                first = TimeParser._NUMBER_WORDS.get(parts[0])
                second = TimeParser._NUMBER_WORDS.get(parts[1])
                if first is not None and second is not None and first >= 20 and second < 20:
                    return first + second

        return None

    @staticmethod
    def _parse_relative_time(time_str: str, now: datetime) -> Optional[Time]:
        time_str = time_str.strip().lower()

        is_past = "ago" in time_str
        has_in_prefix = time_str.startswith("in ")

        if has_in_prefix:
            time_str = time_str[3:].strip()
            is_past = False

        pattern = (
            r"\b(a|an|one|\d+|\w+(?:-\w+)?)\s+"
            r"(second|seconds|sec|secs|minute|minutes|min|mins|hour|hours|hr|hrs|day|days|week|weeks|month|months|year|years)\b"
        )
        match = re.search(pattern, time_str)

        if not match:
            return None

        quantity_str = match.group(1).strip().lower()
        unit_str = match.group(2).strip().lower()

        if unit_str not in TimeParser._TIME_UNITS:
            return None

        unit_seconds = TimeParser._TIME_UNITS[unit_str]

        quantity = TimeParser._convert_number_word_to_int(quantity_str)
        if quantity is None:
            if quantity_str.isdigit():
                quantity = int(quantity_str)
            else:
                return None

        if quantity == 0:
            return None

        total_seconds = quantity * unit_seconds

        object_time = now - timedelta(seconds=total_seconds) if is_past else now + timedelta(seconds=total_seconds)

        return Time(
            year=int(object_time.year),
            month=int(object_time.month),
            day=int(object_time.day),
            hour=int(object_time.hour),
            minute=int(object_time.minute),
        )