import asyncio
import datetime
from abc import ABC, abstractmethod
from decimal import Decimal
from enum import Enum
from typing import List, Optional, Callable

import numpy
from async_lru import alru_cache
from pydantic import ConfigDict

from sirius.common import DataClass, Currency
from sirius.exceptions import SiriusException
from sirius.http_requests import ServerSideException


def calculate_annualized_return(starting: Decimal, ending: Decimal, number_of_days: int) -> Decimal:
    daily_return = (ending / starting)
    annualization_factor = Decimal("365") / Decimal(number_of_days)
    annualized_return = daily_return ** annualization_factor - Decimal("1")
    return annualized_return


class MarketDataException(SiriusException):
    pass


class Exchange(Enum):
    NASDAQ = "NASDAQ"
    NYSE = "NYSE"
    ARCA = "ARCA"


class OptionType(Enum):
    PUT = "PUT"
    CALL = "CALL"


class Asset(DataClass):
    name: str
    currency: Currency


class Stock(Asset, ABC):
    ticker: str

    @classmethod
    async def _get_local_object(cls, other: "Stock") -> "Stock":
        if isinstance(other, cls):
            return other
        else:
            stock: Stock | None = await cls._find(other.ticker)
            if not stock:
                raise MarketDataException(f"Could not find stock with ticker: {other.ticker}")

            return stock

    @staticmethod
    @abstractmethod
    async def _find(ticker: str) -> Optional["Stock"]:
        ...

    @staticmethod
    async def find(ticker: str) -> Optional["Stock"]:
        from sirius.market_data.massive import MassiveStock
        return await MassiveStock._find(ticker)

    @staticmethod
    async def get(ticker: str) -> "Stock":
        stock: Stock = await Stock.find(ticker)
        if not stock:
            raise MarketDataException(f"Could not find stock with ticker: {ticker}")

        return stock


class Option(Asset, ABC):
    id: str
    underlying_stock: Stock
    strike_price: Decimal
    expiry_date: datetime.date
    type: OptionType

    async def get_underlying_stock(self) -> Stock:
        return self.underlying_stock

    @classmethod
    async def _get_local_object(cls, other: "Option") -> "Option":
        if isinstance(other, cls):
            return other
        else:
            option: Option | None = await cls._find(other.id)
            if not option:
                raise MarketDataException(f"Could not find option with ID: {other.id}")

            return option

    @staticmethod
    @abstractmethod
    async def _find(option_id: str) -> Optional["Option"]:
        ...

    @staticmethod
    @abstractmethod
    async def _find_all_options(ticker: str, number_of_days_to_expiry: int) -> List["Option"]:
        ...

    @staticmethod
    def _get_id(ticker: str, expiry_date: datetime.date, strike_price: Decimal, option_type: OptionType) -> str:
        return f"{ticker}{expiry_date.strftime("%y%m%d")}{"C" if option_type == OptionType.CALL else "P"}{str(int(strike_price * 1000)).zfill(8)}"

    @staticmethod
    async def find_all_options(ticker: str, number_of_days_to_expiry: int) -> List["Option"]:
        from sirius.market_data.massive import MassiveOption
        return await MassiveOption._find_all_options(ticker, number_of_days_to_expiry)


class StockMarketData(DataClass, ABC):
    open: Decimal
    high: Decimal
    low: Decimal
    close: Decimal
    timestamp: datetime.datetime
    stock: Stock

    @staticmethod
    @abstractmethod
    async def _get(abstract_stock: Stock, from_datestamp: datetime.date, to_datestamp: datetime.date) -> List["StockMarketData"]:
        ...

    @staticmethod
    async def get(stock: Stock, from_datestamp: datetime.date, to_datestamp: datetime.date | None = None) -> List["StockMarketData"]:
        to_datestamp = datetime.date.today() if not to_datestamp else to_datestamp
        from sirius.market_data.massive import MassiveStockMarketData
        return await MassiveStockMarketData._get(stock, from_datestamp, to_datestamp)

    @staticmethod
    async def get_latest(stock: Stock) -> "StockMarketData":
        from sirius.market_data.massive import MassiveStockMarketData
        return await MassiveStockMarketData.get_latest(stock)


class OptionMarketData(DataClass, ABC):
    option: Option
    last: Decimal

    async def get_option(self) -> Option:
        return self.option

    @staticmethod
    @abstractmethod
    async def _get_latest(option: Option) -> "OptionMarketData":
        ...

    @staticmethod
    async def get_latest(option: Option) -> "OptionMarketData":
        from sirius.market_data.massive import MassiveOptionMarketData
        return await MassiveOptionMarketData._get_latest(option)


class StockPerformance(DataClass):
    position_open: StockMarketData
    position_close: StockMarketData
    absolute_return: Decimal
    annualized_return: Decimal
    model_config = ConfigDict(frozen=True)

    @staticmethod
    def _construct(position_open: StockMarketData, position_close: StockMarketData) -> "StockPerformance":
        open_price: Decimal = position_open.open if position_open == position_close else position_open.close
        absolute_return: Decimal = (position_close.close - open_price) / open_price
        number_of_days = (position_close.timestamp.date() - position_open.timestamp.date()).days
        annualized_return = calculate_annualized_return(open_price, position_close.close, max(1, number_of_days))

        return StockPerformance(
            position_open=position_open,
            position_close=position_close,
            absolute_return=absolute_return,
            annualized_return=annualized_return
        )

    @staticmethod
    @alru_cache(maxsize=50, ttl=600)  # 5 min cache
    async def get(ticker: str, from_datestamp: datetime.date, to_datestamp: datetime.date) -> Optional["StockPerformance"]:
        stock: Stock = await Stock.get(ticker)
        market_data_list: List[StockMarketData] = await StockMarketData.get(stock, from_datestamp, to_datestamp)

        if not market_data_list or (from_datestamp != to_datestamp and len(market_data_list) <= 1):
            return None

        position_open: StockMarketData = min(market_data_list, key=lambda m: m.timestamp)
        position_close: StockMarketData = max(market_data_list, key=lambda m: m.timestamp)
        return StockPerformance._construct(position_open, position_close)


class StockPerformanceAnalysis(DataClass):
    stock_performance_list: List[StockPerformance]
    standard_deviation_absolute_return: Decimal
    standard_deviation_annualized_return: Decimal
    mean_absolute_return: Decimal
    mean_annualized_return: Decimal
    median_absolute_return: Decimal
    median_annualized_return: Decimal
    min_absolute_return: Decimal
    min_annualized_return: Decimal
    max_absolute_return: Decimal
    max_annualized_return: Decimal

    async def get_probability_of_stock_reaching_target(self, stock: Stock, target_price: Decimal) -> Decimal:
        latest_price: StockMarketData = await StockMarketData.get_latest(stock)
        absolute_return: Decimal = (target_price - latest_price.close) / latest_price.close
        stock_performance_list: List[StockPerformance] = self.stock_performance_list
        number_of_stock_performance_reaching_target: int = len([s for s in stock_performance_list if s.absolute_return >= absolute_return])

        return Decimal(number_of_stock_performance_reaching_target) / Decimal(len(stock_performance_list))

    @staticmethod
    @alru_cache(maxsize=50, ttl=600)  # 5 min cache
    async def get(ticker: str, number_of_days_invested: int, number_of_days_to_analyse: int | None = None, analysis_end_datestamp: datetime.date | None = None) -> "StockPerformanceAnalysis":
        today: datetime.date = datetime.datetime.now().date()
        number_of_days_to_analyse = (today - today.replace(year=today.year - 10)).days if not number_of_days_to_analyse else number_of_days_to_analyse
        analysis_end_datestamp = datetime.date.today() if not analysis_end_datestamp else analysis_end_datestamp
        analysis_dates_list: List[datetime.date] = [analysis_end_datestamp - datetime.timedelta(days=day + number_of_days_invested) for day in range(number_of_days_to_analyse)]
        raw_stock_performance_list: List[Optional[StockPerformance]] = list(await asyncio.gather(*[StockPerformance.get(ticker, date, date + datetime.timedelta(days=number_of_days_invested)) for date in analysis_dates_list]))
        stock_performance_list: List[StockPerformance] = [result for result in raw_stock_performance_list if result is not None]

        if not stock_performance_list:
            raise ServerSideException("Did not retrieve any Market Data")

        absolute_return_list: List[Decimal] = [stock_performance.absolute_return for stock_performance in stock_performance_list]
        annualized_return_list: List[Decimal] = [stock_performance.annualized_return for stock_performance in stock_performance_list]
        standard_deviation_absolute_return: Decimal = Decimal(str(numpy.std(absolute_return_list)))  # type: ignore[arg-type]
        standard_deviation_annualized_return: Decimal = Decimal(str(numpy.std(annualized_return_list)))  # type: ignore[arg-type]
        mean_absolute_return: Decimal = Decimal(str(numpy.mean(absolute_return_list)))  # type: ignore[arg-type]
        mean_annualized_return: Decimal = Decimal(str(numpy.mean(annualized_return_list)))  # type: ignore[arg-type]
        median_absolute_return: Decimal = Decimal(str(numpy.median(absolute_return_list)))  # type: ignore[arg-type]
        median_annualized_return: Decimal = Decimal(str(numpy.median(annualized_return_list)))  # type: ignore[arg-type]
        min_absolute_return: Decimal = Decimal(str(numpy.min(absolute_return_list)))  # type: ignore[arg-type]
        min_annualized_return: Decimal = Decimal(str(numpy.min(annualized_return_list)))  # type: ignore[arg-type]
        max_absolute_return: Decimal = Decimal(str(numpy.max(absolute_return_list)))  # type: ignore[arg-type]
        max_annualized_return: Decimal = Decimal(str(numpy.max(annualized_return_list)))  # type: ignore[arg-type]

        return StockPerformanceAnalysis(
            stock_performance_list=stock_performance_list,
            standard_deviation_absolute_return=standard_deviation_absolute_return,
            standard_deviation_annualized_return=standard_deviation_annualized_return,
            mean_absolute_return=mean_absolute_return,
            mean_annualized_return=mean_annualized_return,
            median_absolute_return=median_absolute_return,
            median_annualized_return=median_annualized_return,
            min_absolute_return=min_absolute_return,
            min_annualized_return=min_annualized_return,
            max_absolute_return=max_absolute_return,
            max_annualized_return=max_annualized_return
        )


class OptionPerformanceAnalysis(DataClass):
    option: Option
    stock_performance_analysis: StockPerformanceAnalysis
    itm_probability: Decimal

    @staticmethod
    async def get(option_contract: Option) -> "OptionPerformanceAnalysis":
        number_of_days_invested: int = (option_contract.expiry_date - datetime.datetime.now().date()).days
        underlying_stock: Stock = await option_contract.get_underlying_stock()
        stock_performance_analysis: StockPerformanceAnalysis = await StockPerformanceAnalysis.get(underlying_stock.ticker, number_of_days_invested)
        itm_probability: Decimal = await stock_performance_analysis.get_probability_of_stock_reaching_target(underlying_stock, option_contract.strike_price)

        return OptionPerformanceAnalysis(
            option=option_contract,
            stock_performance_analysis=stock_performance_analysis,
            itm_probability=itm_probability
        )
