"""Support for recurring_ical_events parser."""

from datetime import date, datetime, timedelta
from typing import Optional, Union

import recurring_ical_events as rie
from icalendar import Calendar

from ..filter import Filter
from ..icalendarparser import ICalendarParser
from ..parserevent import ParserEvent
from ..utility import compare_event_dates


class ParserRIE(ICalendarParser):
    """Provide parser using recurring_ical_events."""

    def __init__(self):
        """Construct ParserRIE."""
        self._calendar = None
        self.oneday = timedelta(days=1)
        self.oneday2 = timedelta(hours=23, minutes=59, seconds=59)
        self._filter = Filter("", "")

    def set_content(self, content: str):
        """Parse content into a calendar object.

        This must be called at least once before get_event_list or
        get_current_event.
        :param content is the calendar data
        :type content str
        """
        self._calendar = Calendar.from_ical(content)

    def set_filter(self, filt: Filter):
        """Set a Filter object to filter events.

        :param filt: The Filter object
        :type exclude: Filter
        """
        self._filter = filt

    def get_event_list(
        self,
        start: datetime,
        end: datetime,
        include_all_day: bool,
        offset_hours: int = 0,
    ) -> list[ParserEvent]:
        """Get a list of events.

        Gets the events from start to end, including or excluding all day
        events.
        :param start the earliest start time of events to return
        :type datetime
        :param end the latest start time of events to return
        :type datetime
        :param include_all_day if true, all day events will be included.
        :type boolean
        :param offset_hours the number of hours to offset the event
        :type offset_hours int
        :returns a list of events, or an empty list
        :rtype list[ParserEvent]
        """
        event_list: list[ParserEvent] = []

        if self._calendar is not None:
            for event in rie.of(self._calendar, skip_bad_series=True).between(
                start - timedelta(hours=offset_hours),
                end - timedelta(hours=offset_hours),
            ):
                start, end, all_day = self.is_all_day(event, offset_hours)

                if all_day and not include_all_day:
                    continue

                calendar_event: ParserEvent = ParserEvent(
                    uid=event.get("UID"),
                    summary=event.get("SUMMARY"),
                    start=start,
                    end=end,
                    location=event.get("LOCATION"),
                    description=event.get("DESCRIPTION"),
                    # rrule=event.get("RRULE"),
                    recurrence_id=ParserRIE.get_date(
                        event.get("RECURRENCE-ID").dt
                    ),
                )
                if self._filter.filter_event(calendar_event):
                    event_list.append(calendar_event)

        return event_list

    def get_current_event(  # noqa: R701
        self,
        include_all_day: bool,
        now: datetime,
        days: int,
        offset_hours: int = 0,
    ) -> Optional[ParserEvent]:
        """Get the current or next event.

        Gets the current event, or the next upcoming event with in the
        specified number of days, if there is no current event.
        :param include_all_day if true, all day events will be included.
        :type boolean
        :param now the current date and time
        :type datetime
        :param days the number of days to check for an upcoming event
        :type int
        :param offset_hours the number of hours to offset the event
        :type offset_hours int
        :returns a ParserEvent or None
        """
        if self._calendar is None:
            return None

        temp_event = None
        temp_start: date | datetime = None
        temp_end: date | datetime = None
        temp_all_day: bool = None
        end: datetime = now + timedelta(days=days)
        for event in rie.of(self._calendar, skip_bad_series=True).between(
            now - timedelta(hours=offset_hours),
            end - timedelta(hours=offset_hours),
        ):
            start, end, all_day = self.is_all_day(event, offset_hours)

            if all_day and not include_all_day:
                continue

            if not self._filter.filter(
                event.get("SUMMARY"), event.get("DESCRIPTION")
            ):
                continue

            if temp_start is None or compare_event_dates(
                now, temp_end, temp_start, temp_all_day, end, start, all_day
            ):
                temp_event = event
                temp_start = start
                temp_end = end
                temp_all_day = all_day

        if temp_event is None:
            return None

        return ParserEvent(
            uid=temp_event.get("UID"),
            summary=temp_event.get("SUMMARY"),
            start=temp_start,
            end=temp_end,
            location=temp_event.get("LOCATION"),
            description=temp_event.get("DESCRIPTION"),
            # rrule=temp_event.get("RRULE"),
            recurrence_id=ParserRIE.get_date(
                temp_event.get("RECURRENCE-ID").dt
            ),
        )

    @staticmethod
    def get_date(date_time) -> Union[datetime, date]:
        """Get datetime with timezone information.

        If a date object is passed, it will first have a time component added,
        set to 0.
        :param date_time The date or datetime object
        :type date_time datetime or date
        :type: bool
        :returns The datetime.
        :rtype datetime
        """
        # Must use type here, since a datetime is also a date!
        if isinstance(date_time, date) and not isinstance(date_time, datetime):
            date_time = datetime.combine(date_time, datetime.min.time())
        return date_time.astimezone()

    def is_all_day(self, event, offset_hours: int):
        """Determine if the event is an all day event.

        Return all day status and start and end times for the event.
        :param event The event to examine
        :param offset_hours the number of hours to offset the event
        :type offset_hours int
        """
        start: datetime | date = ParserRIE.get_date(event.get("DTSTART").dt)
        end: datetime | date = ParserRIE.get_date(event.get("DTEND").dt)
        all_day = False
        diff = event.get("DURATION")
        if diff is not None:
            diff = diff.dt
        else:
            diff = end - start
        if (start == end or diff in {self.oneday, self.oneday2}) and all(
            x == 0 for x in [start.hour, start.minute, start.second]
        ):
            # if all_day, start and end must be date, not datetime!
            start = start.date()
            end = end.date()
            all_day = True
        else:
            start = start + timedelta(hours=offset_hours)
            end = end + timedelta(hours=offset_hours)
            if start.tzinfo is None:
                start = start.astimezone()
            if end.tzinfo is None:
                end = end.astimezone()

        return start, end, all_day
