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

from async_lru import alru_cache

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

BASE_URL: str = "https://www.alphavantage.co/query"


@alru_cache(maxsize=50, ttl=43_200)  # 12 hour cache
async def _get_raw_data(ticker: str) -> Dict[Any, Any]:
    api_key: str = await common.get_environmental_secret(SiriusEnvironmentSecretKey.ALPHA_VANTAGE_API_KEY)
    response: HTTPResponse = await AsyncHTTPSession(BASE_URL).get(f"{BASE_URL}", query_params={
        "function": "HISTORICAL_OPTIONS",
        "symbol": ticker,
        "apikey": api_key
    })

    return response.data


class AlphaVantageOption(Option):

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

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

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

            strike_price: Decimal = Decimal(data["strike"])
            option_type: OptionType = OptionType(str(data["type"]).upper())
            option_id: str = Option._get_id(ticker, expiry_date, strike_price, option_type)
            option_list.append(AlphaVantageOption(
                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"]:
        return [option for option in await AlphaVantageOption._find_all_from_ticker(ticker)
                if (option.expiry_date - datetime.datetime.now().date()).days <= number_of_days_to_expiry]


class AlphaVantageOptionMarketData(OptionMarketData):

    @staticmethod
    async def _get_latest(abstract_option: Option) -> "AlphaVantageOptionMarketData":
        raw_data: Dict[Any, Any] = await _get_raw_data(abstract_option.underlying_stock.ticker)
        for data in raw_data["data"]:
            expiry_date: datetime.date = datetime.datetime.strptime(data["expiration"], "%Y-%m-%d").date()
            strike_price: Decimal = Decimal(data["strike"])
            option_type: OptionType = OptionType(str(data["type"]).upper())
            option_id: str = Option._get_id(abstract_option.underlying_stock.ticker, expiry_date, strike_price, option_type)

            if abstract_option.id == option_id:
                return AlphaVantageOptionMarketData(option=abstract_option, last=data["last"])

        raise ApplicationException(f"Could not find latest market data for option with ID: {abstract_option.id}")
