from __future__ import annotations

import logging
from enum import Enum
from typing import Iterable, cast

from betty.locale import DateRange, Date, Localizable, Localizer
from betty.model.ancestry import Person, Presence, Event, Subject, Ancestry
from betty.model.event_type import DerivableEventType, CreatableDerivableEventType, EventType


class Derivation(Enum):
    NONE = 1
    CREATE = 2
    UPDATE = 3


class Deriver(Localizable):
    def __init__(
        self,
        ancestry: Ancestry,
        lifetime_threshold: int,
        derivable_event_types: set[type[DerivableEventType]],
        *,
        localizer: Localizer | None = None,
    ):
        super().__init__(localizer=localizer)
        self._ancestry = ancestry
        self._lifetime_threshold = lifetime_threshold
        self._derivable_event_type = derivable_event_types

    async def derive(self) -> None:
        logger = logging.getLogger()
        for derivable_event_type in self._derivable_event_type:
            created_derivations = 0
            updated_derivations = 0
            for person in self._ancestry[Person]:
                created, updated = self._derive_person(person, derivable_event_type)
                created_derivations += created
                updated_derivations += updated
            if updated_derivations > 0:
                logger.info(self.localizer._('Updated {updated_derivations} {event_type} events based on existing information.').format(
                    updated_derivations=updated_derivations,
                    event_type=derivable_event_type.label(self.localizer)),
                )
            if created_derivations > 0:
                logger.info(self.localizer._('Created {created_derivations} additional {event_type} events based on existing information.').format(
                    created_derivations=created_derivations,
                    event_type=derivable_event_type.label(self.localizer)),
                )

    def _derive_person(self, person: Person, derivable_event_type: type[DerivableEventType]) -> tuple[int, int]:
        # Gather any existing events that could be derived, or create a new derived event if needed.
        derivable_events: list[tuple[Event, Derivation]] = list(map(lambda event: (event, Derivation.UPDATE), _get_derivable_events(person, derivable_event_type)))
        if not derivable_events:
            if list(filter(
                lambda presence: presence.event is not None and issubclass(presence.event.event_type, derivable_event_type),
                person.presences,
            )):
                return 0, 0
            if issubclass(
                derivable_event_type,
                CreatableDerivableEventType,
            ) and derivable_event_type.may_create(
                person,
                self._lifetime_threshold,
            ):
                derivable_events = [
                    (Event(None, derivable_event_type), Derivation.CREATE),
                ]
            else:
                return 0, 0

        # Aggregate event type order from references and backreferences.
        comes_before_event_types = derivable_event_type.comes_before()
        comes_after_event_types = derivable_event_type.comes_after()
        for other_event_type in self._derivable_event_type:
            if derivable_event_type in other_event_type.comes_before():
                comes_after_event_types.add(other_event_type)
            if derivable_event_type in other_event_type.comes_after():
                comes_before_event_types.add(other_event_type)

        created_derivations = 0
        updated_derivations = 0

        for derivable_event, derivation in derivable_events:
            dates_derived = False
            # We know _get_derivable_events() only returns events without a date or a with a date range, but Python
            # does not let us express that in a(n intersection) type, so we must instead cast here.
            derivable_date = cast(DateRange | None, derivable_event.date)

            if derivable_date is None or derivable_date.end is None:
                dates_derived = dates_derived or _ComesBeforeDateDeriver.derive(person, derivable_event, comes_before_event_types)

            if derivable_date is None or derivable_date.start is None:
                dates_derived = dates_derived or _ComesAfterDateDeriver.derive(person, derivable_event, comes_after_event_types)

            if dates_derived:
                self._ancestry.add(derivable_event)
                if derivation is Derivation.CREATE:
                    created_derivations += 1
                    presence = Presence(None, person, Subject(), derivable_event)
                    self._ancestry.add(presence)
                else:
                    updated_derivations += 1

        return created_derivations, updated_derivations


class _DateDeriver:
    @classmethod
    def derive(cls, person: Person, derivable_event: Event, reference_event_types: set[type[EventType]]) -> bool:
        assert issubclass(derivable_event.event_type, DerivableEventType)

        if not reference_event_types:
            return False

        reference_events = _get_reference_events(person, reference_event_types, derivable_event.event_type)
        reference_events_dates: Iterable[tuple[Event, Date]] = filter(
            lambda x: x[1].comparable,
            cls._get_events_dates(reference_events)
        )
        if derivable_event.date is not None:
            reference_events_dates = filter(lambda x: cls._compare(cast(DateRange, derivable_event.date), x[1]), reference_events_dates)
        sorted_reference_events_dates = cls._sort(reference_events_dates)
        try:
            reference_event, reference_date = sorted_reference_events_dates[0]
        except IndexError:
            return False

        if derivable_event.date is None:
            derivable_event.date = DateRange()
        cls._set(
            cast(DateRange, derivable_event.date),
            Date(reference_date.year, reference_date.month, reference_date.day, fuzzy=reference_date.fuzzy),
        )
        derivable_event.citations.add(*reference_event.citations)

        return True

    @classmethod
    def _get_events_dates(cls, events: Iterable[Event]) -> Iterable[tuple[Event, Date]]:
        for event in events:
            if isinstance(event.date, Date):
                yield event, event.date
            if isinstance(event.date, DateRange):
                for date in cls._get_date_range_dates(event.date):
                    yield event, date

    @classmethod
    def _get_date_range_dates(cls, date: DateRange) -> Iterable[Date]:
        raise NotImplementedError(repr(cls))

    @classmethod
    def _compare(cls, derivable_date: DateRange, reference_date: Date) -> bool:
        raise NotImplementedError(repr(cls))

    @classmethod
    def _sort(cls, events_dates: Iterable[tuple[Event, Date]]) -> list[tuple[Event, Date]]:
        raise NotImplementedError(repr(cls))

    @classmethod
    def _set(cls, derivable_date: DateRange, derived_date: Date) -> None:
        raise NotImplementedError(repr(cls))


class _ComesBeforeDateDeriver(_DateDeriver):
    @classmethod
    def _get_date_range_dates(cls, date: DateRange) -> Iterable[Date]:
        if date.start is not None and not date.start_is_boundary:
            yield date.start
        if date.end is not None:
            yield date.end

    @classmethod
    def _compare(cls, derivable_date: DateRange, reference_date: Date) -> bool:
        return derivable_date < reference_date

    @classmethod
    def _sort(cls, events_dates: Iterable[tuple[Event, Date]]) -> list[tuple[Event, Date]]:
        return sorted(events_dates, key=lambda x: x[1])

    @classmethod
    def _set(cls, derivable_date: DateRange, derived_date: Date) -> None:
        derivable_date.end = derived_date
        derivable_date.end_is_boundary = True


class _ComesAfterDateDeriver(_DateDeriver):
    @classmethod
    def _get_date_range_dates(cls, date: DateRange) -> Iterable[Date]:
        if date.start is not None:
            yield date.start
        if date.end is not None and not date.end_is_boundary:
            yield date.end

    @classmethod
    def _compare(cls, derivable_date: DateRange, reference_date: Date) -> bool:
        return derivable_date > reference_date

    @classmethod
    def _sort(cls, events_dates: Iterable[tuple[Event, Date]]) -> list[tuple[Event, Date]]:
        return sorted(events_dates, key=lambda x: x[1], reverse=True)

    @classmethod
    def _set(cls, derivable_date: DateRange, derived_date: Date) -> None:
        derivable_date.start = derived_date
        derivable_date.start_is_boundary = True


def _get_derivable_events(person: Person, derivable_event_type: type[EventType]) -> Iterable[Event]:
    for presence in person.presences:
        event = presence.event

        if event is None:
            continue

        # Ignore events of the wrong type.
        if not issubclass(event.event_type, derivable_event_type):
            continue

        # Ignore events with enough date information that nothing more can be derived.
        if isinstance(event.date, Date):
            continue
        if isinstance(event.date, DateRange) and (not event.event_type.comes_after() or event.date.start is not None) and (not event.event_type.comes_before() or event.date.end is not None):
            continue

        yield event


def _get_reference_events(
    person: Person,
    reference_event_types: set[type[EventType]],
    derivable_event_type: type[EventType],
) -> Iterable[Event]:
    for presence in person.presences:
        reference_event = presence.event

        if reference_event is None:
            continue

        if reference_event.date is None:
            continue

        if isinstance(reference_event.date, DateRange):
            if reference_event.event_type in derivable_event_type.comes_before():
                reference_date = reference_event.date.start
            else:
                reference_date = reference_event.date.end
            if reference_date is None:
                continue
            if reference_date.fuzzy:
                continue

        # Ignore reference events of the wrong type.
        if not issubclass(reference_event.event_type, tuple(reference_event_types)):
            continue

        yield reference_event
