from datetime import datetime, timedelta, date as datetime_date
from typing import Union, Optional, TypedDict, Literal, overload, cast
from dateutil.parser import isoparse
from dateutil.relativedelta import relativedelta
from dateutil import tz
import pytz
from pytz.exceptions import NonExistentTimeError
import logging
from pandas import DatetimeIndex, date_range, DateOffset
from .util_types import Dateable, IntervalDict, SeriesDict, SeriesPronoDict, SeriesPronoSerializableDict, TVP, TVPProno, TVPPronoSerializable, Intervaleable, VariableDict, A5IntervalDict, A5VariableDict, SeriesSerializableDict, TVPserializable


@overload
def tryParseAndLocalizeDate(
        date_ : Dateable,
        timezone : str='America/Argentina/Buenos_Aires',
        *,
        allow_nonexistent : Literal[True]
    ) -> Optional[datetime]: ...
@overload
def tryParseAndLocalizeDate(
        date_ : Dateable,
        timezone : str='America/Argentina/Buenos_Aires',
        *,
        allow_nonexistent : Literal[False] = False
    ) -> datetime: ...
def tryParseAndLocalizeDate(
        date_ : Dateable,
        timezone : str='America/Argentina/Buenos_Aires',
        *,
        allow_nonexistent : bool=False
    ) -> Optional[datetime]:
    """
    Datetime parser. If duration is provided, computes date relative to now.

    Parameters:
    -----------
    date_string : str or float or datetime.datetime
        For absolute date: ISO-8601 datetime string or datetime.datetime or (year,month,date) tuple.
        For relative date: dict (duration key-values) or float (decimal number of days)
    
    timezone : str
        Time zone string identifier. Default: America/Argentina/Buenos_Aires
    
    Returns:
    --------
    datetime.datetime

    Examples:
    ---------
    ``` 
    tryParseAndLocalizeDate("2024-01-01T03:00:00.000Z")
    tryParseAndLocalizeDate(1.5)
    tryParseAndLocalizeDate({"days":1, "hours": 12}, timezone = "Africa/Casablanca")
    tryParseAndLocalizeDate((2000,1,1))
    ```
    """
    
    if isinstance(date_, str):
        date = isoparse(date_)
    elif isinstance(date_,dict):
        date = cast(datetime, datetime.now() + relativedelta(**date_))
    elif isinstance(date_,(int,float)):
        date = cast(datetime, datetime.now() + relativedelta(days=int(date_)))
    elif isinstance(date_, tuple):
        if len(date_) < 3:
            raise ValueError("Invalid date tuple: missing items (3 required)")
        date = datetime(*date_)
    elif isinstance(date_, datetime):
        date = date_
    elif isinstance(date_, datetime_date):
        date = datetime(date_.year, date_.month, date_.day)
    else:
        raise TypeError(f"invalid type ${type(date_)} for datetime parsing")
        
    if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
        try:
            tzone = pytz.timezone(timezone)
            date = cast(datetime, tzone.localize(date))
            # date = date.replace(tzinfo = pytz.timezone(timezone)) # ZoneInfo(timezone))
        except NonExistentTimeError as e:
            if allow_nonexistent:
                logging.warning("NonexistentTimeError: %s" % str(date))
                return None
            raise e
    else:
        date = date.astimezone(pytz.timezone(timezone)) # ZoneInfo(timezone))
    return date

def createDatetimeSequence(
    datetime_index : Optional[DatetimeIndex]=None, 
    timeInterval : Union[relativedelta,dict,int,timedelta] = relativedelta(days=1), 
    timestart : Union[datetime,tuple,str,None] = None, 
    timeend : Union[datetime,tuple,str,None] = None, 
    timeOffset : Union[relativedelta,dict,None] = None
    ) -> DatetimeIndex:
    #Fechas desde timestart a timeend con un paso de timeInterval
    #data: dataframe con index tipo datetime64[ns, America/Argentina/Buenos_Aires]
    #timeOffset sólo para timeInterval n days
    if timestart is None:
        if datetime_index is None:
            raise Exception("Missing datetime_index or timestart+timeend")
        timestart = datetime_index.min()
    else:
        timestart = tryParseAndLocalizeDate(timestart)
    if timeend is None:
        if datetime_index is None:
            raise Exception("Missing datetime_index or timestart+timeend")
        timeend = datetime_index.max()
    else:
        timeend = tryParseAndLocalizeDate(timeend)
    timeInterval = relativedelta(**timeInterval) if isinstance(timeInterval,dict) else timedelta_to_relativedelta(timeInterval) if isinstance(timeInterval,timedelta) else relativedelta(days=timeInterval) if isinstance(timeInterval, int) else timeInterval
    timeOffset = relativedelta(**timeOffset) if isinstance(timeOffset,dict) else timeOffset
    timestart = roundDate(timestart,timeInterval,timeOffset,"up")
    timeend = roundDate(timeend,timeInterval,timeOffset,"down")
    timezone = pytz.timezone("America/Argentina/Buenos_Aires")
    is_subdaily = timeInterval.hours > 0 or timeInterval.minutes > 0 or timeInterval.seconds > 0 or timeInterval.microseconds > 0
    if not is_subdaily:
        if timestart.day == 1 and timeInterval.months > 0:
            freq = "%iMS" % timeInterval.months
        elif timestart.day == 1 and timestart.month == 1 and timeInterval.years > 0:
            freq = "%iYS" % timeInterval.years 
        elif timeInterval.days > 0:
            freq = "%iD" % timeInterval.days
        else:
            freq = relativedelta_to_freq(timeInterval)
        dts_utc = date_range(
            start=timestart.astimezone(tz.UTC), 
            end=timeend.astimezone(tz.UTC), 
            freq = freq
        )
        if timeOffset is not None:
            return DatetimeIndex([timezone.localize(datetime(dt.year,dt.month,dt.day,timeOffset.hours,timeOffset.minutes,timeOffset.seconds)) for dt in dts_utc])
        else:
            return DatetimeIndex([timezone.localize(datetime(dt.year,dt.month,dt.day)) for dt in dts_utc])
    else:
        freq = DateOffset(
                years=timeInterval.years,
                months=timeInterval.months,
                weeks=timeInterval.weeks,
                days=timeInterval.days, 
                hours=timeInterval.hours, 
                minutes = timeInterval.minutes,
                seconds = timeInterval.seconds,
                microseconds = timeInterval.microseconds
            )
        return date_range(
            start=timestart, 
            end=timeend, 
            freq=freq
        ) # .tz_convert(timestart.tzinfo)

def roundDate(date : datetime,timeInterval : relativedelta,timeOffset : Optional[relativedelta]=None, to="up") -> datetime:
    date_0 = tryParseAndLocalizeDate(datetime.combine(date.date(),datetime.min.time()))
    if timeOffset is not None:
        date_0 = date_0 + timeOffset 
    while date_0 < date:
        date_0 = date_0 + timeInterval
    if date_0 == date:
        return date_0
    elif to == "up":
        return date_0
    else:
        return date_0 - timeInterval

def dict2relativedelta(interval : IntervalDict) -> relativedelta:
    return relativedelta(**interval)
        # years: = 0,
        # months: int = 0,
        # days: int = 0,
        # leapdays: int = 0,
        # weeks: int = 0,
        # hours: int = 0,
        # minutes: int = 0,
        # seconds: int = 0,
        # microseconds: int = 0,
        # year: Unknown | None = None,
        # month: Unknown | None = None,
        # day: Unknown | None = None,
        # weekday: Unknown | None = None,
        # yearday: Unknown | None = None,
        # nlyearday: Unknown | None = None,
        # hour: Unknown | None = None,
        # minute: Unknown | None = None,
        # second: Unknown | None = None,
        # microsecond: Unknown | None = None

def interval2relativedelta(interval : Intervaleable) -> relativedelta:
    if isinstance(interval, relativedelta):
        return interval
    if isinstance(interval, dict):
        return dict2relativedelta(interval)
    td = interval2timedelta(interval)
    return timedelta_to_relativedelta(td)

def interval2timedelta(interval : Intervaleable) -> timedelta:
    """Parses duration dict or number of days into datetime.timedelta object
    
    Parameters:
    -----------
    interval : dict or float (decimal number of days) or datetime.timedelta
        If dict, allowed keys are:
        - days
        - seconds
        - microseconds
        - minutes
        - hours
        - weeks
    
    Returns:
    --------
    duration : datetime.timedelta

    Examples:

    ```
    interval2timedelta({"hours":1, "minutes": 30})
    interval2timedelta(1.5/24)
    ```
    """
    if isinstance(interval,timedelta):
        return interval
    if isinstance(interval, relativedelta):
        return relativedelta_to_timedelta(interval)
    if isinstance(interval,(float,int)):
        return timedelta(days=interval)
    days = 0
    seconds = 0
    microseconds = 0
    milliseconds = 0
    minutes = 0
    hours = 0
    weeks = 0
    for k in interval:
        if k in ("milliseconds","millisecond"):
            v = interval.get(k)
            if v is not None:
                milliseconds = v
        elif k in ("seconds","second"):
            v = interval.get(k)
            if v is not None:
                seconds = v
        elif k in ("minutes","minute"):
            v = interval.get(k)
            if v is not None:
                minutes = v
        elif k in ("hours","hour"):
            v = interval.get(k)
            if v is not None:
                hours = v
        elif k in ("days","day"):
            v = interval.get(k)
            if v is not None:
                days = v
        elif k in ("weeks","week"):
            v = interval.get(k)
            if v is not None:
                weeks = v * 86400 * 7
    return timedelta(days=days, seconds=seconds, microseconds=microseconds, milliseconds=milliseconds, minutes=minutes, hours=hours, weeks=weeks)

def timedelta_to_relativedelta(td: timedelta) -> relativedelta:
    total_seconds = td.total_seconds()
    
    days, remainder = divmod(total_seconds, 86400)  # seconds in a day
    hours, remainder = divmod(remainder, 3600)
    minutes, seconds = divmod(remainder, 60)

    return relativedelta(
        days=int(days),
        hours=int(hours),
        minutes=int(minutes),
        seconds=int(seconds)
    )

def relativedelta_to_timedelta(rd: relativedelta) -> timedelta:
    if not isinstance(rd, relativedelta):
        raise TypeError("Value must be of type relativedelta")
    total_seconds = relativedeltaToSeconds(rd)
    days, remainder = divmod(total_seconds, 86400)  # seconds in a day
    hours, remainder = divmod(remainder, 3600)
    minutes, seconds = divmod(remainder, 60)

    return timedelta(
        days=int(days),
        hours=int(hours),
        minutes=int(minutes),
        seconds=int(seconds)
    )

def relativedeltaToSeconds(rd : relativedelta) -> int:
    if not isinstance(rd, relativedelta):
        raise TypeError("Value must be of type relativedelta")
    now = datetime.now()
    future = now + rd
    delta = future - now
    return int(delta.total_seconds())

def relativedelta_to_freq(rd: relativedelta) -> str:
    if rd.years:
        return f"{rd.years}Y"
    if rd.months:
        return f"{rd.months}M"
    if rd.days:
        return f"{rd.days}D"
    if rd.hours:
        return f"{rd.hours}H"
    if rd.minutes:
        return f"{rd.minutes}T"
    if rd.seconds:
        return f"{rd.seconds}S"
    raise ValueError("Unsupported relativedelta")


@overload
def serieObsToProno(
        serie : Union[SeriesDict,SeriesSerializableDict], 
        serializable: Literal[True]=True
        ) -> SeriesPronoSerializableDict: ...
@overload
def serieObsToProno(
        serie : Union[SeriesDict,SeriesSerializableDict], 
        serializable: Literal[False],
        ) -> SeriesPronoDict: ...
def serieObsToProno(
        serie : Union[SeriesDict,SeriesSerializableDict], 
        serializable: bool=True
        ) -> Union[SeriesPronoDict, SeriesPronoSerializableDict]:
    if serializable:
        s : SeriesPronoSerializableDict =  {
            "series_id" : serie["series_id"],
            "series_table" : serie["series_table"],
            "pronosticos": [ tvpToProno(tvp) for tvp in serie["observaciones"]],
            "qualifier": None
        }
        return s
    else:
        s_ : SeriesPronoDict =  {
            "series_id" : serie["series_id"],
            "series_table" : serie["series_table"],
            "pronosticos": [tvpToProno(tvp, False) for tvp in serie["observaciones"]],
            "qualifier": None
        }
        return s_

@overload
def tvpToProno(
        tvp : Union[TVP, TVPserializable, TVPProno, TVPPronoSerializable], 
        serializable : Literal[True]=True
        ) -> TVPPronoSerializable: ...
@overload
def tvpToProno(
        tvp : Union[TVP, TVPserializable, TVPProno, TVPPronoSerializable], 
        serializable : Literal[False],
        ) -> TVPProno: ...
def tvpToProno(
        tvp : Union[TVP, TVPserializable, TVPProno, TVPPronoSerializable], 
        serializable : bool=True
        ) -> Union[TVPProno,TVPPronoSerializable]:
    timestart : datetime = tryParseAndLocalizeDate(tvp["timestart"])
    timeend : Optional[datetime]
    if "timeend" in tvp:
        if tvp["timeend"] is not None:
            timeend = tryParseAndLocalizeDate(tvp["timeend"])
        else:
            timeend = None
    else:
        timeend = None
    if serializable:
        return {
            "timestart": timestart.isoformat(),
            "timeend": timeend.isoformat() if timeend is not None else None, 
            "valor": tvp["valor"],
            "series_id": tvp["series_id"] if "series_id" in tvp else None 
        }
    else:
        return {
            "timestart": timestart,
            "timeend": timeend, 
            "valor": tvp["valor"],
            "series_id": tvp["series_id"] if "series_id" in tvp else None 
        }

def parseA5Interval(i : A5IntervalDict) -> relativedelta:
    return relativedelta(
        years= i["years"] if "years" in i else 0,
        months= i["months"] if "months" in i else 0,
        minutes=i["minutes"] if "minutes" in i else 0,
        seconds=i["seconds"] if "seconds" in i else 0,
        microseconds=int(i["milliseconds"] * 1000) if "milliseconds" in i else 0
    )

def parseVar(v : A5VariableDict) -> VariableDict:
    timeSupport = parseA5Interval(v["timeSupport"])
    def_unit_id = v.get("def_unit_id")
    def_unit_id = int(def_unit_id) #  if def_unit_id is not None else None
    return {
        "id": v.get("id"),
        "var": v.get("var"),
        "nombre": v.get("nombre"),
        "abrev": v.get("abrev"),
        "type": v.get("type"),
        "datatype": v.get("datatype"),
        "valuetype": v.get("valuetype"),
        "GeneralCategory": v.get("GeneralCategory"),
        "VariableName": v.get("VariableName"),
        "SampleMedium": v.get("SampleMedium"),
        "def_unit_id": def_unit_id,
        "timeSupport": timeSupport
    }

def getSeriesTable(tipo : Optional[str]="puntual") -> Literal["series","series_areal","series_rast"]:
    """Retrieve series table name (of a5 schema) for this timeseries"""
    return "series" if tipo is None or tipo == "puntual" else "series_areal" if tipo == "areal" else "series_rast" if tipo == "raster" else "series"
