import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Literal, Tuple, Union
from uuid import uuid4

import httpx

from investiny.config import Config

logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)


def request_to_investing(
    endpoint: Literal["history", "search", "quotes", "symbols"], params: Dict[str, Any]
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
    """Sends an HTTP GET request to Investing.com API with the introduced params.

    Args:
        endpoint: Endpoint to send the request to.
        params: A dictionary with the params to send to Investing.com API.

    Returns:
        A dictionary with the response from Investing.com API.
    """
    url = f"https://tvc6.investing.com/{uuid4().hex}/0/0/0/0/{endpoint}"
    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like"
            " Gecko) Chrome/104.0.5112.102 Safari/537.36"
        ),
        "Referer": "https://tvc-invdn-com.investing.com/",
        "Content-Type": "application/json",
    }
    r = httpx.get(url, params=params, headers=headers)
    if r.status_code != 200:
        raise ConnectionError(
            f"Request to Investing.com API failed with error code: {r.status_code}."
        )
    d = r.json()

    if endpoint in ["history", "quotes"] and d["s"] != "ok":
        raise ConnectionError(
            f"Request to Investing.com API failed with error message: {d['s']}."
            if "nextTime" not in d
            else (
                f"Request to Investing.com API failed with error message: {d['s']}, the"
                " market was probably closed in the introduced dates, try again with"
                f" `from_date={datetime.fromtimestamp(d['nextTime'], tz=timezone.utc).strftime(Config.date_format)}`."
            )
        )
    return d  # type: ignore


def calculate_date_intervals(
    from_date: Union[str, None] = None,
    to_date: Union[str, None] = None,
    interval: Literal[1, 5, 15, 30, 60, 300, "D", "W", "M"] = "D",
) -> Tuple[List[datetime], List[datetime]]:
    """Calculates the intervals between the introduced dates.

    Args:
        from_date: Initial date to retrieve historical data (formatted as m/d/Y). Defaults to None.
        to_date: Final date to retrieve historical data (formatted as m/d/Y). Defaults to None.
        interval: Interval between each historical data point. Defaults to "D".

    Returns:
        A tuple with the from and to datetimes split by intervals.
    """
    interval2timedelta = {
        1: timedelta(minutes=30),  # 30 minutes (half an hour)
        5: timedelta(hours=1),  # 1 hour
        15: timedelta(hours=3),  # 3 hours
        30: timedelta(hours=6),  # 6 hours
        60: timedelta(hours=24),  # 24 hours (one day)
        300: timedelta(hours=120),  # 120 hours (five days)
        "D": timedelta(days=30),  # 30 days (~ 1 month)
        "W": timedelta(days=90),  # 3 months
        "M": timedelta(days=365),  # 1 year
    }

    if not from_date:
        to_datetimes = [datetime.now() + timedelta(days=1)]
        from_datetimes = [to_datetimes[0] - interval2timedelta[interval]]
        return (from_datetimes, to_datetimes)

    try:
        from_datetimes = [datetime.strptime(from_date, Config.date_format)]
        to_datetimes = [
            datetime.strptime(to_date, Config.date_format) + timedelta(days=1)
            if to_date
            else datetime.now() + timedelta(days=1)
        ]
        limit_datetime = datetime(1970, 1, 1)
    except ValueError:
        from_datetimes = [
            datetime.strptime(from_date, Config.time_format).astimezone(tz=timezone.utc)
        ]
        to_datetimes = [
            datetime.strptime(to_date, Config.time_format).astimezone(tz=timezone.utc)
            if to_date
            else datetime.now(tz=timezone.utc)
        ]
        limit_datetime = datetime(1970, 1, 1, tzinfo=timezone.utc)
    except Exception:
        raise ValueError(
            f"Only supported date formats are `{Config.date_format}` and"
            f" `{Config.time_format}`."
        )

    if from_datetimes[0] > to_datetimes[0]:
        raise ValueError("`from_date` cannot be greater than `to_date`")

    if to_datetimes[0] < limit_datetime:
        raise ValueError("`to_date` cannot be lower than 01/01/1970")

    if from_datetimes[0] < limit_datetime:
        from_datetimes = [limit_datetime]

    if interval in [
        "W",
        "M",
    ]:  # There are no data retrieval limits for weekly and monthly intervals
        return (from_datetimes, to_datetimes)

    if interval not in [1, 5, 15, 30, 60, "D"]:
        logging.warning(
            "Interval calculation just implemented for 1, 5, 15, 30, 60, 'D' intervals,"
            f" not for {interval}, wait for its implementation."
        )
        return (from_datetimes, to_datetimes)

    interval2limit = {
        1: timedelta(days=17),
        5: timedelta(days=91),
        15: timedelta(days=275),
        30: timedelta(days=554),
        60: timedelta(days=1030),
        300: timedelta(days=3282),
        "D": timedelta(
            days=6940
        ),  # round(365.25 * 19) = 6940 days (5000 days + bank holidays, weekends, etc.)
    }

    interval2increment = {
        1: timedelta(minutes=1),
        5: timedelta(minutes=5),
        15: timedelta(minutes=15),
        30: timedelta(minutes=30),
        60: timedelta(hours=1),
        300: timedelta(hours=5),
        "D": timedelta(days=1),
    }

    if interval != "D":
        no_more_than = {
            1: timedelta(days=186),
            5: timedelta(days=259),
            15: timedelta(days=458),
            30: timedelta(days=662),
            60: timedelta(days=1927),
            300: timedelta(days=4833),
        }

        if (
            datetime.now(tz=timezone.utc)
            - (from_datetimes[0] + interval2limit[interval])
            > no_more_than[interval]  # type: ignore
        ):
            raise ValueError(
                "Interval between `from_date` and `to_date` cannot be greater than"
                f" {no_more_than[interval].days} days."  # type: ignore
            )

        if datetime.now(tz=timezone.utc) - from_datetimes[0] > no_more_than[interval]:  # type: ignore
            logging.warning(
                "Note that even though the `from_date` parameter is more than"
                f" {no_more_than[interval].days} days ago, due to Investing.com"  # type: ignore
                f" limitations, just the last {no_more_than[interval].days} days of"  # type: ignore
                " data will be retrieved."
            )

    if to_datetimes[0] - from_datetimes[0] > interval2limit[interval]:
        max_to_datetime = to_datetimes[0]
        to_datetimes = [from_datetimes[0] + interval2limit[interval]]
        while max_to_datetime - to_datetimes[-1] > interval2limit[interval]:
            from_datetimes.append(to_datetimes[-1] + interval2increment[interval])
            to_datetimes.append(from_datetimes[-1] + interval2limit[interval])
        if to_datetimes[-1] != max_to_datetime:
            from_datetimes.append(to_datetimes[-1] + interval2increment[interval])
            to_datetimes.append(max_to_datetime)

    return (from_datetimes, to_datetimes)
