"""
This module contains the list of basic models
"""

from pydantic import BaseModel, field_validator, PrivateAttr, ConfigDict
from typing import Optional, List, Union
from copy import deepcopy
import pendulum
import logging


class QuantityMatch(BaseModel):
    buy: int = 0
    sell: int = 0

    @property
    def is_equal(self) -> bool:
        return True if self.buy == self.sell else False

    @property
    def not_matched(self) -> int:
        return self.buy - self.sell


class BasicPosition(BaseModel):
    """
    A simple position tracking mechanism
    """

    symbol: str
    buy_quantity: int = 0
    sell_quantity: int = 0
    buy_value: float = 0.0
    sell_value: float = 0.0

    @property
    def net_quantity(self) -> int:
        return self.buy_quantity - self.sell_quantity

    @property
    def average_buy_value(self) -> float:
        return self.buy_value / self.buy_quantity if self.buy_value > 0.0 else 0.0

    @property
    def average_sell_value(self) -> float:
        return self.sell_value / self.sell_quantity if self.sell_quantity > 0 else 0.0


class Quote(BaseModel):
    price: float
    quantity: int
    orders_count: Optional[int] = None

    @property
    def value(self):
        return self.price * self.quantity


class OrderBook(BaseModel):
    bid: List[Quote]
    ask: List[Quote]

    @property
    def is_bid_ask(self) -> bool:
        """
        return True if there is atleast one bid and ask else False
        """
        if len(self.bid) > 0 and len(self.ask) > 0:
            return True
        else:
            return False

    @property
    def spread(self) -> float:
        """
        returns the bid-ask spread, 0 if not available
        """
        if self.is_bid_ask:
            return self.ask[0].price - self.bid[0].price
        else:
            return 0.0

    @property
    def total_bid_quantity(self) -> int:
        """
        returns the total bid quantity
        """
        if self.is_bid_ask:
            return sum((x.quantity for x in self.bid))
        else:
            return 0

    @property
    def total_ask_quantity(self) -> int:
        """
        returns the total ask quantity
        """
        if self.is_bid_ask:
            return sum((x.quantity for x in self.ask))
        else:
            return 0


class Tracker(BaseModel):
    """
    A simple tracker to track the high low prices
    """

    name: str  # name of the symbol to be tracked
    last_price: float = 0
    # Initializing high and low to extreme values
    high: float = -1e100
    low: float = 1e100

    def update(self, last_price: float):
        self.last_price = last_price
        self.high = max(last_price, self.high)
        self.low = min(last_price, self.low)


class Timer(BaseModel):
    """
    A simple timer that could be attached to any model
    """

    start_time: pendulum.DateTime
    end_time: pendulum.DateTime
    timezone: Optional[str] = None

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("end_time")
    @classmethod
    def validate_times(cls, v, info):
        """
        Validate end_time greater than start_time
        and start_time greater than current time
        """
        start = info.data.get("start_time")
        tz = info.data.get("timezone")
        if v < start:
            raise ValueError("end time greater than start time")
        if start < pendulum.now(tz=tz):
            raise ValueError("start time lesser than current time")
        return v

    @property
    def has_started(self) -> bool:
        """
        Whether tracking has started
        """
        return True if pendulum.now(tz=self.timezone) > self.start_time else False

    @property
    def has_completed(self) -> bool:
        """
        Whether tracking has completed
        """
        return True if pendulum.now(tz=self.timezone) > self.end_time else False

    @property
    def is_running(self) -> bool:
        """
        Whether the timer is in progress
        returns True if it has started and not completed
        else False
        """
        if self.has_started and not self.has_completed:
            return True
        else:
            return False


class TimeTracker(Tracker, Timer):
    pass


class OrderLock(BaseModel):
    """
    Lock order placement, modification and cancellation
    for a few seconds
    """

    max_order_creation_lock_time: float = 60
    max_order_modification_lock_time: float = 60
    max_order_cancellation_lock_time: float = 60
    timezone: Optional[str] = None
    _creation_lock_till: pendulum.DateTime = PrivateAttr(default=None)
    _modification_lock_till: pendulum.DateTime = PrivateAttr(default=None)
    _cancellation_lock_till: pendulum.DateTime = PrivateAttr(default=None)

    def __init__(self, **data):
        super().__init__(**data)
        self._creation_lock_till = pendulum.now(tz=self.timezone)
        self._modification_lock_till = pendulum.now(tz=self.timezone)
        self._cancellation_lock_till = pendulum.now(tz=self.timezone)

    @property
    def creation_lock_till(self):
        return self._creation_lock_till

    @property
    def modification_lock_till(self):
        return self._modification_lock_till

    @property
    def cancellation_lock_till(self):
        return self._cancellation_lock_till

    def create(self, seconds: float) -> pendulum.DateTime:
        """
        Lock the create_order function for the given number of seconds
        """
        seconds = min(seconds, self.max_order_creation_lock_time)
        self._creation_lock_till = pendulum.now(tz=self.timezone).add(
            seconds=int(seconds)
        )
        return self.creation_lock_till

    def modify(self, seconds: float) -> pendulum.DateTime:
        """
        Lock the modify_order function for the given number of seconds
        """
        seconds = min(seconds, self.max_order_modification_lock_time)
        self._modification_lock_till = pendulum.now(tz=self.timezone).add(
            seconds=int(seconds)
        )
        return self.modification_lock_till

    def cancel(self, seconds: float) -> pendulum.DateTime:
        """
        Lock the cancel_order function for the given number of seconds
        """
        seconds = min(seconds, self.max_order_cancellation_lock_time)
        self._cancellation_lock_till = pendulum.now(tz=self.timezone).add(
            seconds=int(seconds)
        )
        return self.cancellation_lock_till

    @property
    def can_create(self) -> bool:
        """
        returns True if order can be created else False
        """
        return (
            True if pendulum.now(tz=self.timezone) > self.creation_lock_till else False
        )

    @property
    def can_modify(self) -> bool:
        """
        returns True if order can be modified else False
        """
        return (
            True
            if pendulum.now(tz=self.timezone) > self.modification_lock_till
            else False
        )

    @property
    def can_cancel(self) -> bool:
        """
        returns True is order can be canceled else False
        """
        return (
            True
            if pendulum.now(tz=self.timezone) > self.cancellation_lock_till
            else False
        )


class Candle(BaseModel):
    """
    A model representing a single candle
    """

    timestamp: pendulum.DateTime
    open: float
    high: float
    low: float
    close: float
    volume: Optional[float] = None
    info: Optional[str] = None

    model_config = ConfigDict(arbitrary_types_allowed=True)


class CandleStick(BaseModel):
    """
    Class to work on candlesticks
    """

    symbol: str
    candles: List[Candle] = []
    initial_price: float = 0
    interval: int = 300  # in seconds
    timer: Optional[Timer] = None
    timezone: str = "local"
    ltp: float = 0
    high: float = -1e100  # Initialize to a impossible value
    low: float = 1e100  # Initialize to a impossible value
    bar_open: float = 0
    bar_high: float = -1e100  # Initialize to a impossible value
    bar_low: float = 1e100  # Initialize to a impossible value
    next_interval: Optional[pendulum.DateTime] = None
    periods: List[pendulum.DateTime] = []
    _last_ltp: float = PrivateAttr()  # to track bar close price

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def __init__(self, **data) -> None:
        super().__init__(**data)
        self._last_ltp = 0
        if self.timer is None:
            timer = Timer(
                start_time=pendulum.today(tz=self.timezone).add(hours=9, minutes=15),
                end_time=pendulum.today(tz=self.timezone).add(hours=15, minutes=30),
            )
            self.timer = timer
        period = pendulum.interval(self.timer.start_time, self.timer.end_time)
        for p in period.range("seconds", self.interval):
            self.periods.append(p)
        # The first period is popped since it is the start
        self.periods.pop(0)
        if self.next_interval is None:
            self.next_interval = self.periods.pop(0)

    def add_candle(self, candle: Candle) -> None:
        """
        Add a candle
        """
        self.candles.append(deepcopy(candle))

    def _update_prices(self):
        """
        Update running candle
        """
        ltp = self.ltp
        if self.initial_price == 0:
            self.initial_price = ltp
        if self.bar_open == 0:
            self.bar_open = ltp
        self.bar_high = max(self.bar_high, ltp)
        self.bar_low = min(self.bar_low, ltp)
        self.high = max(self.high, ltp)
        self.low = min(self.low, ltp)

    def update_candle(self, timestamp: pendulum.DateTime = pendulum.now()) -> Candle:
        """
        Update and append the existing candle
        returns the updated candle
        """
        if len(self.candles) == 0:
            open_price = self.initial_price
        else:
            open_price = self.bar_open
        candle = Candle(
            timestamp=timestamp,
            open=open_price,
            high=self.bar_high,
            low=self.bar_low,
            close=self._last_ltp,
        )
        self.add_candle(candle)
        self.bar_high = -1e100
        self.bar_low = 1e100
        self.bar_open = 0
        self._update_prices()
        return candle

    @property
    def bullish_bars(self) -> int:
        """
        Returns the number of bullish bars
        """
        count = 0
        for candle in self.candles:
            if candle.close > candle.open:
                count += 1
        return count

    @property
    def bearish_bars(self) -> int:
        """
        Returns the number of bullish bars
        """
        count = 0
        for candle in self.candles:
            if candle.close < candle.open:
                count += 1
        return count

    def get_next_interval(self) -> Union[pendulum.DateTime, None]:
        """
        Get the next time interval
        returns None if all intervals are completed
        """
        if len(self.periods) == 0:
            return None
        to_remove = []
        period = None
        for p in self.periods:
            if p > pendulum.now(tz=self.timezone):
                period = p
                to_remove.append(p)
                break
            else:
                to_remove.append(p)
        for p in to_remove:
            try:
                self.periods.remove(p)
            except ValueError:
                logging.error(f"Period {period} cannot be found in the list of periods")
        return period

    def update(self, ltp: float) -> None:
        if self.timer and self.next_interval:
            if self.timer.is_running:
                self._last_ltp = self.ltp
                self.ltp = ltp
                now = pendulum.now(tz=self.timezone)
                if now > self.next_interval:
                    self.update_candle(timestamp=self.next_interval)
                    self.next_interval = self.get_next_interval()
                else:
                    self._update_prices()

    @property
    def last_bullish_bar_index(self) -> int:
        """
        Get the index of the last bullish bar candle
        """
        l = len(self.candles)
        if l == 0:
            return 0
        for i, candle in enumerate(reversed(self.candles)):
            if candle.close > candle.open:
                return l - i
        # If no bullish candle
        return 0

    @property
    def last_bearish_bar_index(self) -> int:
        """
        Get the index of the last bullish bar candle
        """
        l = len(self.candles)
        if l == 0:
            return 0
        for i, candle in enumerate(reversed(self.candles)):
            if candle.close < candle.open:
                return l - i
        # If no bearish candle
        return 0

    @property
    def last_bullish_bar(self) -> Optional[Candle]:
        """
        Return the latest bullish bar
        """
        l = len(self.candles)
        if l == 0:
            return None
        i = self.last_bullish_bar_index
        if i == 0:
            return None
        else:
            return self.candles[i - 1]

    @property
    def last_bearish_bar(self) -> Optional[Candle]:
        """
        Return the latest bearish bar
        """
        l = len(self.candles)
        if l == 0:
            return None
        i = self.last_bearish_bar_index
        if i == 0:
            return None
        else:
            return self.candles[i - 1]
