import datetime
import time
from typing import Union

from django.db.models import Q
from django.utils import formats, timezone


def normalize_date_range(
        start_date: Union[datetime.datetime, datetime.date] = None,
        end_date: Union[datetime.datetime, datetime.date] = None,
        days: int = None,
        default_to_current_month=False,
        *,
        allow_exceeds_now: bool = False,
) -> (datetime.datetime, datetime.datetime):
    # Return start_date, end_date normalized based on given parameters
    # default to:
    #   last 30 days
    #   this month if default_to_current_month=True
    # if one of start_date or end_date are given it adjust other one not to exceeds today. (allow_exceeds_now=False)
    # if start_date and end_date are both given, just normalize them.

    now = timezone.localtime()
    if start_date is None and end_date is None and default_to_current_month:
        start_date = start_of_month(now)
        end_date = end_of_month(now)

    if (start_date is None or end_date is None) and days is None:
        days = 30

    # change to datetime if they are date instances
    if start_date:
        start_date = normalize_date(start_date)
    if end_date:
        end_date = normalize_date(end_date)

    if start_date and end_date is None:
        if allow_exceeds_now:
            end_date = start_date + datetime.timedelta(days=days)
        elif start_date > now:
            end_date = start_date
        else:
            end_date = start_date + datetime.timedelta(days=min(days, (now - start_date).days))

    elif start_date is None:
        if end_date is None:
            end_date = now
        start_date = end_date - datetime.timedelta(days=days)

    if start_date:
        start_date = start_of_day(start_date)
    if end_date:
        end_date = end_of_day(end_date)

    return start_date, end_date


def start_of_day(dt: Union[datetime.datetime, datetime.date]):
    dt = normalize_date(dt, preserve_time=False)
    return dt


def end_of_day(dt: Union[datetime.datetime, datetime.date]):
    dt = (start_of_day(dt) + datetime.timedelta(days=1)) - datetime.timedelta(microseconds=1)
    return dt


def normalize_month_range(start_date=None, end_date=None, future_months=0):
    days = None
    if not start_date and not end_date:
        days = 365 - 31  # lead to one year
    if future_months and end_date is None:
        end_date = timezone.localtime() + datetime.timedelta(days=future_months * 31)
    start_date, end_date = normalize_date_range(start_date, end_date, days)
    start_month = start_of_month(start_date)
    end_month = end_of_month(end_date)
    return start_month, end_month


def start_of_month(dt: datetime.datetime):
    dt = normalize_date(dt.replace(day=1), preserve_time=False)
    return dt


def end_of_month(dt: datetime.datetime):
    dt = (start_of_month(dt) + datetime.timedelta(days=31)).replace(day=1) - datetime.timedelta(microseconds=1)
    return dt


def get_month_range(request, future_months=0):
    start_month, end_month = normalize_month_range(
        future_months=future_months, **get_date_range_kwargs_from_request(request)
    )
    date_range = []
    i = 0

    while True:
        dt = timezone.make_aware(
            datetime.datetime.fromtimestamp(time.mktime((end_month.year, end_month.month - i, 1, 0, 0, 0, 0, 0, -1)))
        )
        i += 1
        if dt < start_month:
            break
        date_range.insert(0, dt)

    return date_range


def normalize_week_range(start_date=None, end_date=None):
    days = None
    if not start_date and not end_date:
        days = 12 * 7  # lead to 12 weeks ago
    start_date, end_date = normalize_date_range(start_date, end_date, days)
    start_week = start_of_week(start_date)
    end_week = end_of_week(end_date)
    return start_week, end_week


def start_of_week(dt: datetime.datetime):
    dt = normalize_date(dt, preserve_time=False)
    dt = dt - datetime.timedelta(days=dt.weekday())
    dt = normalize_date(dt)  # used when pass a DST edge
    return dt


def end_of_week(dt: datetime.datetime):
    dt = (start_of_week(dt) + datetime.timedelta(days=7)) - datetime.timedelta(microseconds=1)
    return dt


def get_week_range(request):
    start_week, end_week = normalize_week_range(**get_date_range_kwargs_from_request(request))
    date_range = []
    i = 0

    while True:
        dt = timezone.make_aware(
            datetime.datetime.fromtimestamp(
                time.mktime((start_week.year, start_week.month, start_week.day + i * 7, 0, 0, 0, 0, 0, -1))
            )
        )
        i += 1
        if dt > end_week:
            break
        date_range.append(dt)

    return date_range


def get_day_range(request):
    start_date, end_date = normalize_date_range(**get_date_range_kwargs_from_request(request))
    date_range = [
        normalize_date(start_date + datetime.timedelta(days=i))  #
        for i in range((end_date - start_date).days + 1)
    ]
    return date_range


def normalize_date(d: Union[datetime.datetime, datetime.date], *, preserve_time=False) -> datetime.datetime:
    tz = timezone.get_default_timezone()

    if isinstance(d, datetime.date) and not (isinstance(d, datetime.datetime) and preserve_time):
        d = datetime.datetime.combine(d, datetime.datetime.min.time())

    if timezone.is_naive(d):
        d = tz.localize(d)

    d = tz.normalize(d)

    return d


def get_date_range_kwargs_from_request(request):
    kwargs = {}

    start_date = request.GET.get('start_date')
    if start_date:
        try:
            start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d')
        except ValueError:
            pass
        else:
            kwargs['start_date'] = start_date

    end_date = request.GET.get('end_date')
    if end_date:
        try:
            end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d')
        except ValueError:
            pass
        else:
            kwargs['end_date'] = end_date

    return kwargs


def get_filter_by_date_range_Q(
        field_name, *, start_date=None, end_date=None, days=None, default_to_current_month=False
):
    start_date, end_date = normalize_date_range(start_date, end_date, days, default_to_current_month)
    return Q(**{
        f'{field_name}__gte': start_date,
        f'{field_name}__lte': end_date,
    })


def timedelta_str(timedelta):
    mm, ss = divmod(timedelta.seconds, 60)
    hh, mm = divmod(mm, 60)
    hh += timedelta.days * 24
    s = '%02d:%02d' % (hh, mm)
    return s


def get_date_formats():
    class Formats:
        DATE_INPUT_FORMAT = formats.get_format_lazy('DATE_INPUT_FORMATS')[0]
        DATE_PLACEHOLDER = DATE_INPUT_FORMAT \
            .replace('%Y', 'yyyy').replace('%m', 'mm').replace('%d', 'dd').upper()

        DATETIME_INPUT_FORMAT = formats.get_format_lazy('DATETIME_INPUT_FORMATS')[0].replace(':%S', '')
        DATETIME_PLACEHOLDER = DATETIME_INPUT_FORMAT \
            .replace('%Y', 'yyyy').replace('%m', 'mm').replace('%d', 'dd') \
            .replace('%H', 'hh').replace('%M', 'ii').replace(':%S', '').upper()

        TIME_INPUT_FORMAT = DATETIME_INPUT_FORMAT.split(' ', 1)[-1]

    return Formats()
