import datetime
from decimal import Decimal
from typing import Optional, Any, Dict, List

from async_lru import alru_cache
from dateutil.relativedelta import relativedelta

from sirius import common
from sirius.common import Currency
from sirius.constants import SiriusEnvironmentSecretKey
from sirius.http_requests import HTTPResponse, AsyncHTTPSession
from sirius.market_data import Stock, StockMarketData, Option, OptionType, OptionMarketData

BASE_URL: str = "https://api.massive.com"


class MassiveStock(Stock):

    @staticmethod
    @alru_cache(maxsize=50, ttl=86_400)  # 24 hour cache
    async def _get_raw_ticker_search_data(ticker: str) -> List[Dict[str, Any]]:
        api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.MASSIVE_API_KEY)
        response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}/v3/reference/tickers", query_params={
            "ticker": ticker,
            "market": "stocks",
            "apikey": api_key
        })

        return list(filter(lambda r: r["currency_name"] == "usd", response.data["results"]))

    @staticmethod
    async def _find(ticker: str) -> Optional["Stock"]:
        raw_data_list: List[Dict[str, Any]] = await MassiveStock._get_raw_ticker_search_data(ticker)
        if not raw_data_list:
            return None

        raw_data: Dict[str, Any] = raw_data_list[0]
        return MassiveStock(
            name=raw_data["name"],
            currency=Currency(raw_data["currency_name"].upper()),
            ticker=raw_data["ticker"]
        )


class MassiveStockMarketData(StockMarketData):

    @staticmethod
    @alru_cache(maxsize=50, ttl=43_200)  # 12 hour cache
    async def _get_raw_time_series_data(ticker: str) -> Dict[datetime.datetime, Any]:
        api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.MASSIVE_API_KEY)
        from_date_str: str = (datetime.datetime.now() - relativedelta(years=5)).strftime("%Y-%m-%d")
        to_date_str: str = datetime.datetime.now().strftime("%Y-%m-%d")
        response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}/v2/aggs/ticker/{ticker}/range/1/day/{from_date_str}/{to_date_str}", query_params={
            "adjusted": "true",
            "limit": 50000,
            "apikey": api_key
        })

        return {
            datetime.datetime.fromtimestamp(data["t"] / 1000): data
            for data in response.data["results"]
        }

    @staticmethod
    async def _get(abstract_stock: Stock, from_datestamp: datetime.date, to_datestamp: datetime.date) -> List["StockMarketData"]:
        raw_data: Dict[datetime.datetime, Any] = await MassiveStockMarketData._get_raw_time_series_data(abstract_stock.ticker)
        return [MassiveStockMarketData(
            open=Decimal(raw_data[timestamp]["o"]),
            high=Decimal(raw_data[timestamp]["h"]),
            low=Decimal(raw_data[timestamp]["l"]),
            close=Decimal(raw_data[timestamp]["c"]),
            timestamp=timestamp,
            stock=abstract_stock
        ) for timestamp in raw_data.keys()
            if from_datestamp <= timestamp.date() <= to_datestamp]

    @staticmethod
    @alru_cache(maxsize=50, ttl=60)  # 60 second cache
    async def _get_raw_latest_stock_market_data(ticker: str) -> Dict[str, Any]:
        api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.MASSIVE_API_KEY)
        response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}/v2/snapshot/locale/us/markets/stocks/tickers", query_params={
            "tickers": ticker,
            "apikey": api_key
        })

        return response.data["tickers"][0]

    @staticmethod
    async def get_latest(abstract_stock: Stock) -> "MassiveStockMarketData":
        raw_data: Dict[str, Any] = await MassiveStockMarketData._get_raw_latest_stock_market_data(abstract_stock.ticker)
        is_latest_market_data: bool = raw_data["min"]["t"] != 0

        if raw_data["min"]["t"] != 0:
            raw_data_2: Dict[str, int] = raw_data["min"]
            timestamp: datetime.datetime = datetime.datetime.fromtimestamp(raw_data["updated"] / 1_000_000_000)
        else:
            raw_data_2 = raw_data["prevDay"]
            previous_business_day: datetime.date = common.get_previous_business_day(datetime.datetime.now())
            timestamp = datetime.datetime.combine(previous_business_day, datetime.time(23, 59, 59))

        return MassiveStockMarketData(
            open=Decimal(raw_data_2["o"]),
            high=Decimal(raw_data_2["h"]),
            low=Decimal(raw_data_2["l"]),
            close=Decimal(raw_data_2["c"]),
            timestamp=timestamp,
            stock=abstract_stock
        )


class MassiveOption(Option):

    @staticmethod
    async def _find(option_id: str) -> Optional["Option"]:
        ticker: str = option_id.split(" | ")[0]
        option_list: List[MassiveOption] = await MassiveOption._find_all_from_ticker(ticker)
        option_map: Dict[str, MassiveOption] = {option.id: option for option in option_list}
        return option_map.get(option_id)

    @staticmethod
    @alru_cache(maxsize=50, ttl=60)  # 60 second cache
    async def _get_raw_data(ticker: str) -> List[Dict[str, Any]]:
        api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.MASSIVE_API_KEY)
        response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}/v3/snapshot/options/{ticker}", query_params={
            "limit": "250",
            "apikey": api_key
        })

        all_data: List[Dict[str, Any]] = response.data["results"]
        while "next_url" in response.data:
            response = await AsyncHTTPSession(BASE_URL).get(f"{response.data["next_url"]}&apikey={api_key}")
            all_data = all_data + response.data["results"]

        return all_data

    @staticmethod
    async def _find_all_from_ticker(ticker: str) -> List["MassiveOption"]:
        option_list: List[MassiveOption] = []
        underlying_stock: Stock = await Stock.find(ticker)
        if not underlying_stock:
            return []

        for data in await MassiveOption._get_raw_data(ticker):
            expiry_date: datetime.date = datetime.datetime.strptime(data["details"]["expiration_date"], "%Y-%m-%d").date()
            if expiry_date < datetime.datetime.now().date():
                continue

            strike_price: Decimal = Decimal(data["details"]["strike_price"])
            option_type: OptionType = OptionType(str(data["details"]["contract_type"]).upper())
            option_id: str = Option._get_id(ticker, expiry_date, strike_price, option_type)
            option_list.append(MassiveOption(
                id=option_id,
                underlying_stock=underlying_stock,
                strike_price=strike_price,
                expiry_date=expiry_date,
                type=option_type,
                currency=underlying_stock.currency,
                name=option_id
            ))

        return option_list

    @staticmethod
    async def _find_all_options(ticker: str, number_of_days_to_expiry: int) -> List["Option"]:
        data = await MassiveOption._find_all_from_ticker(ticker)
        return [option for option in data
                if (option.expiry_date - datetime.datetime.now().date()).days == number_of_days_to_expiry]


class MassiveOptionMarketData(OptionMarketData):

    @staticmethod
    @alru_cache(maxsize=50, ttl=60)  # 60 second cache
    async def _get_raw_data(ticker: str) -> Dict[str, Any] | None:
        api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.MASSIVE_API_KEY)
        from_date_str: str = (datetime.datetime.now() - relativedelta(days=5)).strftime("%Y-%m-%d")
        to_date_str: str = datetime.datetime.now().strftime("%Y-%m-%d")
        response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}/v2/aggs/ticker/O:{ticker}/range/1/second/{from_date_str}/{to_date_str}", query_params={
            "limit": "1",
            "adjusted": "true",
            "sort": "desc",
            "apikey": api_key
        })

        return response.data["results"][0] if "results" in response.data else None


    @staticmethod
    async def _get_latest(abstract_option: Option) -> "MassiveOptionMarketData":
        raw_data: Dict[str, Any] = await MassiveOptionMarketData._get_raw_data(abstract_option.id)
        last: Decimal = Decimal(raw_data["l"] if raw_data else "0")
        return MassiveOptionMarketData(option=abstract_option, last=last)
