import logging
from typing import List

import pandas as pd
import requests as r
import schedule
from markdownify import markdownify

from common.Symbol import Coin
from common.utilities import rate_limited

import time

log = logging.getLogger(__name__)


class cg_Crypto:
    """
    Functions for finding crypto info
    """

    vs_currency = "usd"  # simple/supported_vs_currencies for list of options

    trending_cache: List[str] = []

    def __init__(self) -> None:
        self.get_symbol_list()
        schedule.every().day.do(self.get_symbol_list)

    # Coingecko's rate limit is 30 requests per minute.
    # Since there are two bots sharing the same IP, we allocate half of that limit to each bot.
    # This results in a rate limit of 15 requests per minute for each bot.
    # Given this, the rate limit effectively becomes 1 request every 4 seconds for each bot.
    @rate_limited(0.25)
    def get(self, endpoint, params: dict = {}, timeout=10) -> dict:
        url = "https://api.coingecko.com/api/v3" + endpoint
        resp = r.get(url, params=params, timeout=timeout)
        # Make sure API returned a proper status code

        if resp.status_code == 429:
            log.warning(f"CoinGecko returned 429 - Too Many Requests for endpoint: {endpoint}. Sleeping and trying again.")
            time.sleep(10)
            return self.get(endpoint=endpoint,params=params,timeout=timeout)
            

        try:
            resp.raise_for_status()
        except r.exceptions.HTTPError as e:
            log.error(e)
            return {}

        # Make sure API returned valid JSON
        try:
            resp_json = resp.json()
            return resp_json
        except r.exceptions.JSONDecodeError as e:
            log.error(e)
            return {}

    def symbol_id(self, symbol) -> str:
        try:
            return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[0]
        except KeyError:
            return ""

    def get_symbol_list(self):
        raw_symbols = self.get("/coins/list")
        symbols = pd.DataFrame(data=raw_symbols)

        # Removes all binance-peg symbols
        symbols = symbols[~symbols["id"].str.contains("binance-peg")]

        symbols["description"] = "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"]
        symbols = symbols[["id", "symbol", "name", "description"]]
        symbols["type_id"] = "$$" + symbols["symbol"]

        self.symbol_list = symbols

    def status(self) -> str:
        """Checks CoinGecko /ping endpoint for API issues.

        Returns
        -------
        str
            Human readable text on status of CoinGecko API
        """
        status = r.get(
            "https://api.coingecko.com/api/v3/ping",
            timeout=5,
        )

        try:
            status.raise_for_status()
            return (
                f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
            )
        except r.HTTPError:
            return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} seconds."

    def price_reply(self, coin: Coin) -> str:
        """Returns current market price or after hours if its available for a given coin symbol.

        Parameters
        ----------
        symbols : list
            List of coin symbols.

        Returns
        -------
        Dict[str, str]
            Each symbol passed in is a key with its value being a human readable
            markdown formatted string of the symbols price and movement.
        """

        if resp := self.get(
            "/simple/price",
            params={
                "ids": coin.id,
                "vs_currencies": self.vs_currency,
                "include_24hr_change": "true",
            },
        ):
            try:
                data = resp[coin.id]

                price = data[self.vs_currency]
                change = data[self.vs_currency + "_24h_change"]
                if change is None:
                    change = 0
            except KeyError:
                return f"{coin.id} returned an error."

            message = f"The current price of {coin.name} is $**{price:,}**"

            # Determine wording of change text
            if change > 0:
                message += f", the coin is currently **up {change:.3f}%** for today"
            elif change < 0:
                message += f", the coin is currently **down {change:.3f}%** for today"
            else:
                message += ", the coin hasn't shown any movement today."

        else:
            message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`"

        return message

    def intra_reply(self, symbol: Coin) -> pd.DataFrame:
        """Returns price data for a symbol since the last market open.

        Parameters
        ----------
        symbol : str
            Stock symbol.

        Returns
        -------
        pd.DataFrame
            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
        """

        if resp := self.get(
            f"/coins/{symbol.id}/ohlc",
            params={"vs_currency": self.vs_currency, "days": 1},
        ):
            df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna()
            df["Date"] = pd.to_datetime(df["Date"], unit="ms")
            df = df.set_index("Date")
            return df

        return pd.DataFrame()

    def chart_reply(self, symbol: Coin) -> pd.DataFrame:
        """Returns price data for a symbol of the past month up until the previous trading days close.
        Also caches multiple requests made in the same day.

        Parameters
        ----------
        symbol : str
            Stock symbol.

        Returns
        -------
        pd.DataFrame
            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
        """

        if resp := self.get(
            f"/coins/{symbol.id}/ohlc",
            params={"vs_currency": self.vs_currency, "days": 30},
        ):
            df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna()
            df["Date"] = pd.to_datetime(df["Date"], unit="ms")
            df = df.set_index("Date")
            return df

        return pd.DataFrame()

    def stat_reply(self, symbol: Coin) -> str:
        """Gathers key statistics on coin. Mostly just CoinGecko scores.

        Parameters
        ----------
        symbol : Coin

        Returns
        -------
        str
            Preformatted markdown.
        """

        if data := self.get(
            f"/coins/{symbol.id}",
            params={
                "localization": "false",
            },
        ):
            return f"""
                [{data['name']}]({data['links']['homepage'][0]}) Statistics:
                Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,}
                Market Cap Ranking: {data.get('market_cap_rank',"Not Available")}
                CoinGecko Scores:
                    Overall: {data.get('coingecko_score','Not Available')}
                    Development: {data.get('developer_score','Not Available')}
                    Community: {data.get('community_score','Not Available')}
                    Public Interest: {data.get('public_interest_score','Not Available')}
                    """
        else:
            return f"{symbol.symbol} returned an error."

    def cap_reply(self, coin: Coin) -> str:
        """Gets market cap for Coin

        Parameters
        ----------
        coin : Coin

        Returns
        -------
        str
            Preformatted markdown.
        """

        if resp := self.get(
            "/simple/price",
            params={
                "ids": coin.id,
                "vs_currencies": self.vs_currency,
                "include_market_cap": "true",
            },
        ):
            log.debug(resp)
            try:
                data = resp[coin.id]

                price = data[self.vs_currency]
                cap = data[self.vs_currency + "_market_cap"]
            except KeyError:
                return f"{coin.id} returned an error."

            if cap == 0:
                return f"The market cap for {coin.name} is not available for unknown reasons."

            message = (
                f"The current price of {coin.name} is $**{price:,}** and"
                + " its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}"
            )

        else:
            message = f"The Coin: {coin.name} was not found or returned and error."

        return message

    def info_reply(self, symbol: Coin) -> str:
        """Gets coin description

        Parameters
        ----------
        symbol : Coin

        Returns
        -------
        str
            Preformatted markdown.
        """

        if data := self.get(
            f"/coins/{symbol.id}",
            params={"localization": "false"},
        ):
            try:
                return markdownify(data["description"]["en"])
            except KeyError:
                return f"{symbol} does not have a description available."

        return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."

    def spark_reply(self, symbol: Coin) -> str:
        change = self.get(
            "/simple/price",
            params={
                "ids": symbol.id,
                "vs_currencies": self.vs_currency,
                "include_24hr_change": "true",
            },
        )[symbol.id]["usd_24h_change"]

        return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%"

    def trending(self) -> list[str]:
        """Gets current coins trending on coingecko

        Returns
        -------
        list[str]
            list of $$ID: NAME, CHANGE%
        """

        coins = self.get("/search/trending")
        try:
            trending = []
            for coin in coins["coins"]:
                c = coin["item"]

                sym = c["symbol"].upper()
                name = c["name"]
                change = self.get(
                    "/simple/price",
                    params={
                        "ids": c["id"],
                        "vs_currencies": self.vs_currency,
                        "include_24hr_change": "true",
                    },
                )[c["id"]]["usd_24h_change"]

                msg = f"`$${sym}`: {name}, {change:.2f}%"

                trending.append(msg)

        except Exception as e:
            log.warning(e)
            return self.trending_cache

        self.trending_cache = trending
        return trending

    def batch_price(self, coins: list[Coin]) -> list[str]:
        """Gets price of a list of coins all in one API call

        Parameters
        ----------
        coins : list[Coin]

        Returns
        -------
        list[str]
            returns preformatted list of strings detailing price movement of each coin passed in.
        """
        query = ",".join([c.id for c in coins])

        prices = self.get(
            "/simple/price",
            params={
                "ids": query,
                "vs_currencies": self.vs_currency,
                "include_24hr_change": "true",
            },
        )

        replies = []
        for coin in coins:
            if coin.id in prices:
                p = prices[coin.id]

                if p.get("usd_24h_change") is None:
                    p["usd_24h_change"] = 0

                replies.append(
                    f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours."
                )

        return replies
