from datetime import datetime, timezone
from enum import Enum
from typing import ClassVar, Optional, Union

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from freqtrade.persistence.base import ModelBase, SessionType


ValueTypes = Union[str, datetime, float, int]


class ValueTypesEnum(str, Enum):
    STRING = "str"
    DATETIME = "datetime"
    FLOAT = "float"
    INT = "int"


class KeyStoreKeys(str, Enum):
    BOT_START_TIME = "bot_start_time"
    STARTUP_TIME = "startup_time"


class _KeyValueStoreModel(ModelBase):
    """
    Pair Locks database model.
    """

    __tablename__ = "KeyValueStore"
    session: ClassVar[SessionType]

    id: Mapped[int] = mapped_column(primary_key=True)

    key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True)

    value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)

    string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
    datetime_value: Mapped[Optional[datetime]]
    float_value: Mapped[Optional[float]]
    int_value: Mapped[Optional[int]]


class KeyValueStore:
    """
    Generic bot-wide, persistent key-value store
    Can be used to store generic values, e.g. very first bot startup time.
    Supports the types str, datetime, float and int.
    """

    @staticmethod
    def store_value(key: KeyStoreKeys, value: ValueTypes) -> None:
        """
        Store the given value for the given key.
        :param key: Key to store the value for - can be used in get-value to retrieve the key
        :param value: Value to store - can be str, datetime, float or int
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(_KeyValueStoreModel.key == key)
            .first()
        )
        if kv is None:
            kv = _KeyValueStoreModel(key=key)
        if isinstance(value, str):
            kv.value_type = ValueTypesEnum.STRING
            kv.string_value = value
        elif isinstance(value, datetime):
            kv.value_type = ValueTypesEnum.DATETIME
            kv.datetime_value = value
        elif isinstance(value, float):
            kv.value_type = ValueTypesEnum.FLOAT
            kv.float_value = value
        elif isinstance(value, int):
            kv.value_type = ValueTypesEnum.INT
            kv.int_value = value
        else:
            raise ValueError(f"Unknown value type {kv.value_type}")
        _KeyValueStoreModel.session.add(kv)
        _KeyValueStoreModel.session.commit()

    @staticmethod
    def delete_value(key: KeyStoreKeys) -> None:
        """
        Delete the value for the given key.
        :param key: Key to delete the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(_KeyValueStoreModel.key == key)
            .first()
        )
        if kv is not None:
            _KeyValueStoreModel.session.delete(kv)
            _KeyValueStoreModel.session.commit()

    @staticmethod
    def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]:
        """
        Get the value for the given key.
        :param key: Key to get the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(_KeyValueStoreModel.key == key)
            .first()
        )
        if kv is None:
            return None
        if kv.value_type == ValueTypesEnum.STRING:
            return kv.string_value
        if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None:
            return kv.datetime_value.replace(tzinfo=timezone.utc)
        if kv.value_type == ValueTypesEnum.FLOAT:
            return kv.float_value
        if kv.value_type == ValueTypesEnum.INT:
            return kv.int_value
        # This should never happen unless someone messed with the database manually
        raise ValueError(f"Unknown value type {kv.value_type}")  # pragma: no cover

    @staticmethod
    def get_string_value(key: KeyStoreKeys) -> Optional[str]:
        """
        Get the value for the given key.
        :param key: Key to get the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(
                _KeyValueStoreModel.key == key,
                _KeyValueStoreModel.value_type == ValueTypesEnum.STRING,
            )
            .first()
        )
        if kv is None:
            return None
        return kv.string_value

    @staticmethod
    def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]:
        """
        Get the value for the given key.
        :param key: Key to get the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(
                _KeyValueStoreModel.key == key,
                _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME,
            )
            .first()
        )
        if kv is None or kv.datetime_value is None:
            return None
        return kv.datetime_value.replace(tzinfo=timezone.utc)

    @staticmethod
    def get_float_value(key: KeyStoreKeys) -> Optional[float]:
        """
        Get the value for the given key.
        :param key: Key to get the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(
                _KeyValueStoreModel.key == key,
                _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT,
            )
            .first()
        )
        if kv is None:
            return None
        return kv.float_value

    @staticmethod
    def get_int_value(key: KeyStoreKeys) -> Optional[int]:
        """
        Get the value for the given key.
        :param key: Key to get the value for
        """
        kv = (
            _KeyValueStoreModel.session.query(_KeyValueStoreModel)
            .filter(
                _KeyValueStoreModel.key == key, _KeyValueStoreModel.value_type == ValueTypesEnum.INT
            )
            .first()
        )
        if kv is None:
            return None
        return kv.int_value


def set_startup_time():
    """
    sets bot_start_time to the first trade open date - or "now" on new databases.
    sets startup_time to "now"
    """
    st = KeyValueStore.get_value("bot_start_time")
    if st is None:
        from freqtrade.persistence import Trade

        t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first()
        if t is not None:
            KeyValueStore.store_value("bot_start_time", t.open_date_utc)
        else:
            KeyValueStore.store_value("bot_start_time", datetime.now(timezone.utc))
    KeyValueStore.store_value("startup_time", datetime.now(timezone.utc))
