from datetime import datetime, time, timezone

import regex as re
from dateutil.relativedelta import relativedelta
from tzlocal import get_localzone

from dateparser.utils import apply_timezone, localize_timezone, strip_braces

from .parser import time_parser
from .timezone_parser import pop_tz_offset_from_string

_UNITS = r"decade|year|month|week|day|hour|minute|second"
PATTERN = re.compile(r"([+-]?\s*\d++[.,]?\d*+)\s*(%s)\b" % _UNITS, re.I | re.S | re.U)


class FreshnessDateDataParser:
    """Parses date string like "1 year, 2 months ago" and "3 hours, 50 minutes ago" """

    def _are_all_words_units(self, date_string):
        skip = [_UNITS, r"ago|in|\d+", r":|[ap]m"]

        date_string = re.sub(r"\s+", " ", date_string.strip())

        words = [x for x in re.split(r"\W", date_string) if x]
        words = [x for x in words if not re.match(r"%s" % "|".join(skip), x)]
        return not words

    def _parse_time(self, date_string, settings):
        """Attempts to parse time part of date strings like '1 day ago, 2 PM'"""
        date_string = PATTERN.sub("", date_string)
        date_string = re.sub(r"\b(?:ago|in)\b", "", date_string)
        try:
            return time_parser(date_string)
        except Exception:
            pass

    def get_local_tz(self):
        return get_localzone()

    def parse(self, date_string, settings):
        date_string = strip_braces(date_string)
        date_string, ptz = pop_tz_offset_from_string(date_string)
        _time = self._parse_time(date_string, settings)

        _settings_tz = settings.TIMEZONE.lower()

        def apply_time(dateobj, timeobj):
            if not isinstance(_time, time):
                return dateobj

            return dateobj.replace(
                hour=timeobj.hour,
                minute=timeobj.minute,
                second=timeobj.second,
                microsecond=timeobj.microsecond,
            )

        if settings.RELATIVE_BASE:
            now = settings.RELATIVE_BASE

            if "local" not in _settings_tz:
                now = localize_timezone(now, settings.TIMEZONE)

            if ptz:
                if now.tzinfo:
                    now = now.astimezone(ptz)
                else:
                    if hasattr(ptz, "localize"):
                        now = ptz.localize(now)
                    else:
                        now = now.replace(tzinfo=ptz)

            if not now.tzinfo:
                now = now.replace(tzinfo=self.get_local_tz())

        elif ptz:
            localized_now = datetime.now(ptz)

            if "local" in _settings_tz:
                now = localized_now
            else:
                now = apply_timezone(localized_now, settings.TIMEZONE)

        else:
            if "local" not in _settings_tz:
                utc_dt = datetime.now(tz=timezone.utc)
                now = apply_timezone(utc_dt, settings.TIMEZONE)
            else:
                now = datetime.now(self.get_local_tz())

        date, period = self._parse_date(date_string, now, settings.PREFER_DATES_FROM)

        if date:
            old_date = date
            date = apply_time(date, _time)
            if settings.RETURN_TIME_AS_PERIOD and old_date != date:
                period = "time"

            if settings.TO_TIMEZONE:
                date = apply_timezone(date, settings.TO_TIMEZONE)

            if not settings.RETURN_AS_TIMEZONE_AWARE or (
                settings.RETURN_AS_TIMEZONE_AWARE
                and "default" == settings.RETURN_AS_TIMEZONE_AWARE
                and not ptz
            ):
                date = date.replace(tzinfo=None)

        return date, period

    def _parse_date(self, date_string, now, prefer_dates_from):
        if not self._are_all_words_units(date_string):
            return None, None

        result = self.get_kwargs(date_string)
        if isinstance(result, tuple):
            kwargs, explicit_signs = result
        else:
            kwargs = result
            explicit_signs = {}

        if not kwargs:
            return None, None
        period = "day"
        if "days" not in kwargs:
            for k in ["weeks", "months", "years", "decades"]:
                if k in kwargs:
                    period = "year" if k == "decades" else k[:-1]
                    break

        going_forward = (
            re.search(r"\bin\b", date_string)
            or re.search(r"\bfuture\b", prefer_dates_from)
            and not re.search(r"\bago\b", date_string)
        )

        adjusted_kwargs = {}
        for key, value in kwargs.items():
            if explicit_signs.get(key, False):
                adjusted_kwargs[key] = value
            else:
                if going_forward:
                    adjusted_kwargs[key] = value
                else:
                    adjusted_kwargs[key] = -value

        # Fold ``decades`` into ``years`` after each component's sign has been
        # resolved so that an unsigned component combined with an explicitly
        # signed one still follows the default ago/future context (fixes
        # #1304).
        if "decades" in adjusted_kwargs:
            adjusted_kwargs["years"] = adjusted_kwargs.get(
                "years", 0
            ) + 10 * adjusted_kwargs.pop("decades")

        td = relativedelta(**adjusted_kwargs)

        date = now + td

        return date, period

    def get_kwargs(self, date_string):
        m = PATTERN.findall(date_string)
        if not m:
            return {}

        kwargs = {}
        explicit_signs = {}

        for num, unit in m:
            has_explicit_sign = num.startswith("+") or num.startswith("-")
            explicit_signs[unit + "s"] = has_explicit_sign
            kwargs[unit + "s"] = float(num.replace(",", ".").replace(" ", ""))

        return kwargs, explicit_signs

    def get_date_data(self, date_string, settings=None):
        from dateparser.date import DateData

        date, period = self.parse(date_string, settings)
        return DateData(date_obj=date, period=period)


freshness_date_parser = FreshnessDateDataParser()
