"""
Semantic analyzer for time expressions that evaluates the AST and produces datetime objects.
"""

import datetime
import calendar
from typing import Optional

from .ptime_ast import (
    TimeExpressionNode, DateNode, TimeNode, 
    RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode
)
from .err import TokenUnhandledException


class SemanticAnalyzer:
    """Semantic analyzer that evaluates time expression ASTs."""

    def __init__(self, now: Optional[datetime.datetime] = None):
        self.now = now or datetime.datetime.now()

    def evaluate_number(self, node: NumberNode) -> int:
        """Evaluate a number node."""
        return node.value

    def evaluate_date(self, node: DateNode) -> datetime.date:
        """Evaluate a date node."""
        year = self.now.year
        month = 1
        day = 1

        if node.year is not None:
            year = self.evaluate_number(node.year)
        if node.month is not None:
            month = self.evaluate_number(node.month)
        if node.day is not None:
            day = self.evaluate_number(node.day)

        return datetime.date(year, month, day)

    def evaluate_time(self, node: TimeNode) -> datetime.time:
        """Evaluate a time node."""
        hour = 0
        minute = 0
        second = 0

        if node.hour is not None:
            hour = self.evaluate_number(node.hour)
        if node.minute is not None:
            minute = self.evaluate_number(node.minute)
        if node.second is not None:
            second = self.evaluate_number(node.second)

        # Handle 24-hour vs 12-hour format
        if not node.is_24hour and node.period is not None:
            if node.period == "AM":
                if hour == 12:
                    hour = 0
            elif node.period == "PM":
                if hour != 12 and hour <= 12:
                    hour += 12

        # Validate time values
        if not (0 <= hour <= 23):
            raise TokenUnhandledException(f"Invalid hour: {hour}")
        if not (0 <= minute <= 59):
            raise TokenUnhandledException(f"Invalid minute: {minute}")
        if not (0 <= second <= 59):
            raise TokenUnhandledException(f"Invalid second: {second}")

        return datetime.time(hour, minute, second)

    def evaluate_relative_date(self, node: RelativeDateNode) -> datetime.timedelta:
        """Evaluate a relative date node."""
        # Start with current time
        result = self.now

        # Special case: If weeks contains a target day (hacky way to pass target day info)
        # This is for patterns like "下个月五号"
        if node.weeks > 0 and node.weeks <= 31:  # Valid day range
            target_day = node.weeks

            # Calculate the target month
            if node.months != 0:
                # Handle month arithmetic carefully
                total_months = result.month + node.months - 1
                new_year = result.year + total_months // 12
                new_month = total_months % 12 + 1

                # Handle day overflow (e.g., Jan 31 + 1 month = Feb 28/29)
                max_day_in_target_month = calendar.monthrange(new_year, new_month)[1]
                target_day = min(target_day, max_day_in_target_month)

                try:
                    result = result.replace(year=new_year, month=new_month, day=target_day)
                except ValueError:
                    # Handle edge cases
                    result = result.replace(year=new_year, month=new_month, day=max_day_in_target_month)

            # Return the difference between the new date and the original date
            return result - self.now

        # Apply years
        if node.years != 0:
            # Handle year arithmetic carefully due to leap years
            new_year = result.year + node.years
            try:
                result = result.replace(year=new_year)
            except ValueError:
                # Handle leap year edge case (Feb 29 -> Feb 28)
                result = result.replace(year=new_year, month=2, day=28)

        # Apply months
        if node.months != 0:
            # Check if this is a special marker for absolute month (negative offset)
            if node.months < 0:
                # This is an absolute month specification (e.g., from "明年五月")
                absolute_month = node.months + 100
                if 1 <= absolute_month <= 12:
                    result = result.replace(year=result.year, month=absolute_month, day=result.day)
            else:
                # Handle month arithmetic carefully
                total_months = result.month + node.months - 1
                new_year = result.year + total_months // 12
                new_month = total_months % 12 + 1

                # Handle day overflow (e.g., Jan 31 + 1 month = Feb 28/29)
                new_day = min(result.day, calendar.monthrange(new_year, new_month)[1])

                result = result.replace(year=new_year, month=new_month, day=new_day)

        # Apply weeks and days
        if node.weeks != 0 or node.days != 0:
            delta_days = node.weeks * 7 + node.days
            result = result + datetime.timedelta(days=delta_days)

        return result - self.now

    def evaluate_relative_time(self, node: RelativeTimeNode) -> datetime.timedelta:
        """Evaluate a relative time node."""
        # Convert all values to seconds for precise calculation
        total_seconds = (
            node.hours * 3600 +
            node.minutes * 60 +
            node.seconds
        )

        return datetime.timedelta(seconds=total_seconds)

    def evaluate_weekday(self, node: WeekdayNode) -> datetime.timedelta:
        """Evaluate a weekday node."""
        current_weekday = self.now.weekday()  # 0=Monday, 6=Sunday
        target_weekday = node.weekday

        if node.scope == "current":
            delta = target_weekday - current_weekday
        elif node.scope == "last":
            delta = target_weekday - current_weekday - 7
        elif node.scope == "next":
            delta = target_weekday - current_weekday + 7
        else:
            delta = target_weekday - current_weekday

        return datetime.timedelta(days=delta)

    def infer_smart_time(self, hour: int, minute: int = 0, second: int = 0, base_time: Optional[datetime.datetime] = None) -> datetime.datetime:
        """
        Smart time inference based on current time.

        For example:
        - If now is 14:30 and user says "3点", interpret as 15:00
        - If now is 14:30 and user says "1点", interpret as next day 01:00
        - If now is 8:00 and user says "3点", interpret as 15:00
        - If now is 8:00 and user says "9点", interpret as 09:00
        """
        # Use base_time if provided, otherwise use self.now
        now = base_time if base_time is not None else self.now

        # Handle 24-hour format directly (13-23)
        if 13 <= hour <= 23:
            candidate = now.replace(hour=hour, minute=minute, second=second, microsecond=0)
            if candidate <= now:
                candidate += datetime.timedelta(days=1)
            return candidate

        # Handle 12 (noon/midnight)
        if hour == 12:
            # For 12 specifically, we need to be more careful
            # Try noon first
            noon_candidate = now.replace(hour=12, minute=minute, second=second, microsecond=0)
            midnight_candidate = now.replace(hour=0, minute=minute, second=second, microsecond=0)

            # Special case: If it's afternoon or evening, "十二点" likely means next day midnight
            if now.hour >= 12:
                result = midnight_candidate + datetime.timedelta(days=1)
                return result

            # If noon is in the future and closer than midnight, use it
            if noon_candidate > now and (midnight_candidate <= now or noon_candidate < midnight_candidate):
                return noon_candidate
            # If midnight is in the future, use it
            elif midnight_candidate > now:
                return midnight_candidate
            # Both are in the past, use the closer one
            elif noon_candidate > midnight_candidate:
                return noon_candidate
            # Otherwise use midnight next day
            else:
                result = midnight_candidate + datetime.timedelta(days=1)
                return result

        # Handle 1-11 (12-hour format)
        if 1 <= hour <= 11:
            # Calculate 12-hour format candidates
            pm_hour = hour + 12
            pm_candidate = now.replace(hour=pm_hour, minute=minute, second=second, microsecond=0)
            am_candidate = now.replace(hour=hour, minute=minute, second=second, microsecond=0)

            # Special case: If it's afternoon (12:00-18:00) and the hour is 1-6,
            # user might mean either PM today or AM tomorrow.
            # But if PM is in the future, that's more likely what they mean.
            if 12 <= now.hour <= 18 and 1 <= hour <= 6:
                if pm_candidate > now:
                    return pm_candidate
                else:
                    # PM is in the past, so use AM tomorrow
                    result = am_candidate + datetime.timedelta(days=1)
                    return result

            # Special case: If it's late evening (after 22:00) and user specifies early morning hours (1-5),
            # user likely means next day early morning
            if now.hour >= 22 and 1 <= hour <= 5:
                result = am_candidate + datetime.timedelta(days=1)
                return result

            # Special case: In the morning (0-12:00)
            if now.hour < 12:
                # In the morning, for hours 1-11, generally prefer AM interpretation
                # unless it's a very early hour that's much earlier than current time
                # Only push to next day for very early hours (1-2) that are significantly earlier
                if hour <= 2 and hour < now.hour and now.hour - hour >= 6:
                    # Very early morning hour that's significantly earlier, use next day
                    result = am_candidate + datetime.timedelta(days=1)
                    return result
                else:
                    # For morning, generally prefer AM if it's in the future
                    if am_candidate > now:
                        return am_candidate
                    # If PM is in the future, use it
                    elif pm_candidate > now:
                        return pm_candidate
                    # Both are in the past, prefer AM if it's closer
                    elif am_candidate > pm_candidate:
                        return am_candidate
                    # Otherwise use PM next day
                    else:
                        result = pm_candidate + datetime.timedelta(days=1)
                        return result
            else:
                # General case: choose the one that's in the future and closer
                if pm_candidate > now and (am_candidate <= now or pm_candidate < am_candidate):
                    return pm_candidate
                elif am_candidate > now:
                    return am_candidate
                # Both are in the past, use the closer one
                elif pm_candidate > am_candidate:
                    return pm_candidate
                # Otherwise use AM next day
                else:
                    result = am_candidate + datetime.timedelta(days=1)
                    return result

        # Handle 0 (midnight)
        if hour == 0:
            candidate = now.replace(hour=0, minute=minute, second=second, microsecond=0)
            if candidate <= now:
                candidate += datetime.timedelta(days=1)
            return candidate

        # Default case (should not happen with valid input)
        candidate = now.replace(hour=hour, minute=minute, second=second, microsecond=0)
        if candidate <= now:
            candidate += datetime.timedelta(days=1)
        return candidate

    def evaluate(self, node: TimeExpressionNode) -> datetime.datetime:
        """Evaluate a complete time expression node."""
        result = self.now

        # Apply relative date (should set time to 00:00:00 for dates)
        if node.relative_date is not None:
            delta = self.evaluate_relative_date(node.relative_date)
            result = result + delta
            # For relative dates like "今天", "明天", set time to 00:00:00
            # But only for cases where we're dealing with days, not years/months
            if (node.date is None and node.time is None and node.weekday is None and
                node.relative_date.years == 0 and node.relative_date.months == 0):
                result = result.replace(hour=0, minute=0, second=0, microsecond=0)

        # Apply weekday
        if node.weekday is not None:
            delta = self.evaluate_weekday(node.weekday)
            result = result + delta
            # For weekdays, set time to 00:00:00
            if node.date is None and node.time is None:
                result = result.replace(hour=0, minute=0, second=0, microsecond=0)

        # Apply relative time
        if node.relative_time is not None:
            delta = self.evaluate_relative_time(node.relative_time)
            result = result + delta

        # Apply absolute date
        if node.date is not None:
            date = self.evaluate_date(node.date)
            result = result.replace(year=date.year, month=date.month, day=date.day)
            # For absolute dates without time, set time to 00:00:00
            if node.time is None:
                result = result.replace(hour=0, minute=0, second=0, microsecond=0)

        # Apply time
        if node.time is not None:
            time = self.evaluate_time(node.time)

            # Handle explicit period or student-friendly expressions
            if node.time.is_24hour or node.time.period is not None:
                # Handle explicit period
                if not node.time.is_24hour and node.time.period is not None:
                    hour = time.hour
                    minute = time.minute
                    second = time.second

                    if node.time.period == "AM":
                        if hour == 12:
                            hour = 0
                    elif node.time.period == "PM":
                        # Special case: "晚上十二点" should be interpreted as next day 00:00
                        if hour == 12 and minute == 0 and second == 0:
                            # Move to next day at 00:00:00
                            result = result.replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1)
                            # Skip the general replacement since we've already handled it
                            skip_general_replacement = True
                        else:
                            # For other PM times, convert to 24-hour format
                            if hour != 12 and hour <= 12:
                                hour += 12

                            # Validate hour
                            if not (0 <= hour <= 23):
                                raise TokenUnhandledException(f"Invalid hour: {hour}")

                    # Only do general replacement if we haven't handled it specially
                    if not locals().get('skip_general_replacement', False):
                        result = result.replace(hour=hour, minute=minute, second=second, microsecond=0)
                else:
                    # Already in 24-hour format
                    result = result.replace(hour=time.hour, minute=time.minute, second=time.second, microsecond=0)
            else:
                # Use smart time inference for regular times
                # But if we have an explicit date, treat the time as 24-hour format
                if node.date is not None or node.relative_date is not None:
                    # For explicit dates, treat time as 24-hour format
                    result = result.replace(hour=time.hour, minute=time.minute or 0, second=time.second or 0, microsecond=0)
                else:
                    # Use smart time inference for regular times
                    smart_time = self.infer_smart_time(time.hour, time.minute, time.second, base_time=result)
                    result = smart_time

        return result
