"""
Sarmat.
Ядро пакета.
Описание бизнес логики.
Вспомогательные инструменты.
Вычисления с участием Sarmat объектов.
Вычисление интервала.
"""
from datetime import (
    datetime,
    timedelta,
)
from enum import Enum
from typing import Generator

from sarmat.core.constants import (
    DurationMonthCalcStrategy,
    IntervalType, SarmatWrongValueError,
)
from sarmat.core.context.models import IntervalItemModel

from .const import months_in_year
from .duration import get_month_increase


class LoopType(Enum):
    """Типы зацикленных значений."""

    DAY = "DAY"
    DAY_OF_WEEK = "DOW"


def get_timedelta_for_day(
    interval_value: int,
    calc_strategy: DurationMonthCalcStrategy,
) -> Generator[timedelta, None, None]:
    """Получение приращения по дате для помесячной генерации.

    Args:
        interval_value: значение для приращения
        calc_strategy: стратегия расчёта по месяцам

    Send args:
        start_datetime (datetime): момент отсчёта

    Yields: приращение по времени
    """
    res = timedelta(seconds=0)
    first_is_requested = False

    while True:
        start_datetime = yield res
        if start_datetime is None:
            raise SarmatWrongValueError(description="Не указано начало интервала")

        if not first_is_requested:
            first_is_requested = True
            continue

        val = get_month_increase(
            interval_value,
            start_datetime.date(),
            calc_strategy,
        )
        args = {IntervalType.DAY.as_cypher: val}
        res = timedelta(**args)


def get_timedelta_from_interval(
    interval_item: IntervalItemModel,
) -> Generator[timedelta, None, None]:
    """Получение простого приращения по дате.

    Args:
        interval_item: элемент интервала

    Send args:
        start_datetime (datetime): момент отсчёта

    Yields: приращение по времени
    """
    res = timedelta(seconds=0)
    args = {interval_item.interval_type.as_cypher: interval_item.values[0]}
    first_is_requested = False

    while True:
        _ = yield res
        if not first_is_requested:
            first_is_requested = True
            continue

        res = timedelta(**args)


def get_even_odd_timedelta(
    for_even: bool,
) -> Generator[timedelta, None, None]:
    """Получение приращения для чётных-нечётных чередований

    Args:
        for_even: признак расчёта для чётных дней

    Send args:
        start_datetime (datetime): момент отсчёта

    Yields: приращение по времени
    """
    res = timedelta(seconds=0)
    first_is_requested = False

    while True:
        start_datetime = yield res
        if start_datetime is None:
            raise SarmatWrongValueError(description="Не указано начало интервала")

        start_day = start_datetime.day

        # по чётным числам
        if for_even:
            if start_day % 2:
                val = 1
            else:
                val = 2
                if not first_is_requested:
                    first_is_requested = True
                    continue
        # по нечётным числам
        else:
            if start_day % 2:
                val = 2
                if not first_is_requested:
                    first_is_requested = True
                    continue
            else:
                val = 1

        first_is_requested = True
        args = {IntervalType.DAY.as_cypher: val}
        res = timedelta(**args)


def get_looped_sequence_timedelta(
    loop_type: LoopType,
    sequence: list[int],
    end_datetime: datetime,
) -> Generator[timedelta, None, None]:
    """Получение приращения для повторяющихся последовательностей

    Args:
        loop_type: тип последовательности
        sequence: набор значений
        end_datetime: дата окончания периода

    Send args:
        start_datetime (datetime): момент отсчёта

    Yields: приращение по времени
    """

    def get_calculated_day(dt_start: datetime) -> int:
        """Определение дня для вычисления.

        Args:
            dt_start: момент отсчёта времени

        Returns: высчитанный день недели или календарное число
        """
        if loop_type == LoopType.DAY_OF_WEEK:
            return dt_start.isoweekday()
        return dt_start.day

    res = timedelta(seconds=0)
    first_is_requested = False
    while True:
        delta = 0
        start_datetime = yield res
        if start_datetime is None:
            raise SarmatWrongValueError(description="Не указано начало интервала")

        if first_is_requested:
            delta += 1
            start_datetime += timedelta(days=1)

        days = (end_datetime - start_datetime).days

        for i in range(days + 1):
            calc_day = get_calculated_day(start_datetime + timedelta(days=i))
            if calc_day in sequence:
                break
            delta += 1
        else:
            break

        first_is_requested = True
        res = timedelta(days=delta)


def get_datetime_sequence_from_interval_item(
    interval_item: IntervalItemModel,
    start_datetime: datetime,
    stop_datetime: datetime,
    calc_strategy: DurationMonthCalcStrategy,
) -> Generator[datetime, None, None]:
    """Преобразование элемента из интервала в последовательность дат.

    Args:
        interval_item: элемент интервала
        start_datetime: начальный момент времени
        stop_datetime: конечный момент времени
        calc_strategy: стратегия расчёта времени для месяцев

    Yields: даты с приращением по интервалу
    """
    if interval_item.interval_type == IntervalType.MONTH:
        gen_timedelta = get_timedelta_for_day(
            interval_item.values[0],
            calc_strategy,
        )
    elif interval_item.interval_type == IntervalType.YEAR:
        gen_timedelta = get_timedelta_for_day(
            interval_item.values[0] * months_in_year,
            calc_strategy,
        )
    elif interval_item.interval_type == IntervalType.EVEN:
        gen_timedelta = get_even_odd_timedelta(for_even=True)
    elif interval_item.interval_type == IntervalType.ODD:
        gen_timedelta = get_even_odd_timedelta(for_even=False)
    elif interval_item.interval_type == IntervalType.DAYS_OF_MONTH:
        gen_timedelta = get_looped_sequence_timedelta(
            LoopType.DAY,
            interval_item.values,
            end_datetime=stop_datetime,
        )
    elif interval_item.interval_type == IntervalType.DAYS_OF_WEEK:
        gen_timedelta = get_looped_sequence_timedelta(
            LoopType.DAY_OF_WEEK,
            interval_item.values,
            end_datetime=stop_datetime,
        )
    else:
        gen_timedelta = get_timedelta_from_interval(interval_item)

    next(gen_timedelta)

    next_step = start_datetime
    while next_step <= stop_datetime:
        try:
            next_step += gen_timedelta.send(next_step)  # type: ignore [arg-type]
        except StopIteration:
            break

        if next_step <= stop_datetime:
            yield next_step
