from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union

from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny

from freqtrade.constants import IntOrInf
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
from freqtrade.types import ValidExchangesType


class ExchangeModePayloadMixin(BaseModel):
    trading_mode: Optional[TradingMode] = None
    margin_mode: Optional[MarginMode] = None
    exchange: Optional[str] = None


class Ping(BaseModel):
    status: str


class AccessToken(BaseModel):
    access_token: str


class AccessAndRefreshToken(AccessToken):
    refresh_token: str


class Version(BaseModel):
    version: str


class StatusMsg(BaseModel):
    status: str


class BgJobStarted(StatusMsg):
    job_id: str


class BackgroundTaskStatus(BaseModel):
    job_id: str
    job_category: str
    status: str
    running: bool
    progress: Optional[float] = None
    error: Optional[str] = None


class BackgroundTaskResult(BaseModel):
    error: Optional[str] = None
    status: str


class ResultMsg(BaseModel):
    result: str


class Balance(BaseModel):
    currency: str
    free: float
    balance: float
    used: float
    bot_owned: Optional[float] = None
    est_stake: float
    est_stake_bot: Optional[float] = None
    stake: str
    # Starting with 2.x
    side: str
    leverage: float
    is_position: bool
    position: float
    is_bot_managed: bool


class Balances(BaseModel):
    currencies: List[Balance]
    total: float
    total_bot: float
    symbol: str
    value: float
    value_bot: float
    stake: str
    note: str
    starting_capital: float
    starting_capital_ratio: float
    starting_capital_pct: float
    starting_capital_fiat: float
    starting_capital_fiat_ratio: float
    starting_capital_fiat_pct: float


class Count(BaseModel):
    current: int
    max: int
    total_stake: float


class __BaseStatsModel(BaseModel):
    profit_ratio: float
    profit_pct: float
    profit_abs: float
    count: int


class Entry(__BaseStatsModel):
    enter_tag: str


class Exit(__BaseStatsModel):
    exit_reason: str


class MixTag(__BaseStatsModel):
    mix_tag: str


class PerformanceEntry(__BaseStatsModel):
    pair: str
    profit: float


class Profit(BaseModel):
    profit_closed_coin: float
    profit_closed_percent_mean: float
    profit_closed_ratio_mean: float
    profit_closed_percent_sum: float
    profit_closed_ratio_sum: float
    profit_closed_percent: float
    profit_closed_ratio: float
    profit_closed_fiat: float
    profit_all_coin: float
    profit_all_percent_mean: float
    profit_all_ratio_mean: float
    profit_all_percent_sum: float
    profit_all_ratio_sum: float
    profit_all_percent: float
    profit_all_ratio: float
    profit_all_fiat: float
    trade_count: int
    closed_trade_count: int
    first_trade_date: str
    first_trade_humanized: str
    first_trade_timestamp: int
    latest_trade_date: str
    latest_trade_humanized: str
    latest_trade_timestamp: int
    avg_duration: str
    best_pair: str
    best_rate: float
    best_pair_profit_ratio: float
    winning_trades: int
    losing_trades: int
    profit_factor: float
    winrate: float
    expectancy: float
    expectancy_ratio: float
    max_drawdown: float
    max_drawdown_abs: float
    max_drawdown_start: str
    max_drawdown_start_timestamp: int
    max_drawdown_end: str
    max_drawdown_end_timestamp: int
    trading_volume: Optional[float] = None
    bot_start_timestamp: int
    bot_start_date: str


class SellReason(BaseModel):
    wins: int
    losses: int
    draws: int


class Stats(BaseModel):
    exit_reasons: Dict[str, SellReason]
    durations: Dict[str, Optional[float]]


class DailyWeeklyMonthlyRecord(BaseModel):
    date: date
    abs_profit: float
    rel_profit: float
    starting_balance: float
    fiat_value: float
    trade_count: int


class DailyWeeklyMonthly(BaseModel):
    data: List[DailyWeeklyMonthlyRecord]
    fiat_display_currency: str
    stake_currency: str


class UnfilledTimeout(BaseModel):
    entry: Optional[int] = None
    exit: Optional[int] = None
    unit: Optional[str] = None
    exit_timeout_count: Optional[int] = None


class OrderTypes(BaseModel):
    entry: OrderTypeValues
    exit: OrderTypeValues
    emergency_exit: Optional[OrderTypeValues] = None
    force_exit: Optional[OrderTypeValues] = None
    force_entry: Optional[OrderTypeValues] = None
    stoploss: OrderTypeValues
    stoploss_on_exchange: bool
    stoploss_on_exchange_interval: Optional[int] = None


class ShowConfig(BaseModel):
    version: str
    strategy_version: Optional[str] = None
    api_version: float
    dry_run: bool
    trading_mode: str
    short_allowed: bool
    stake_currency: str
    stake_amount: str
    available_capital: Optional[float] = None
    stake_currency_decimals: int
    max_open_trades: IntOrInf
    minimal_roi: Dict[str, Any]
    stoploss: Optional[float] = None
    stoploss_on_exchange: bool
    trailing_stop: Optional[bool] = None
    trailing_stop_positive: Optional[float] = None
    trailing_stop_positive_offset: Optional[float] = None
    trailing_only_offset_is_reached: Optional[bool] = None
    unfilledtimeout: Optional[UnfilledTimeout] = None  # Empty in webserver mode
    order_types: Optional[OrderTypes] = None
    use_custom_stoploss: Optional[bool] = None
    timeframe: Optional[str] = None
    timeframe_ms: int
    timeframe_min: int
    exchange: str
    strategy: Optional[str] = None
    force_entry_enable: bool
    exit_pricing: Dict[str, Any]
    entry_pricing: Dict[str, Any]
    bot_name: str
    state: str
    runmode: str
    position_adjustment_enable: bool
    max_entry_position_adjustment: int


class OrderSchema(BaseModel):
    pair: str
    order_id: str
    status: str
    remaining: Optional[float] = None
    amount: float
    safe_price: float
    cost: float
    filled: Optional[float] = None
    ft_order_side: str
    order_type: str
    is_open: bool
    order_timestamp: Optional[int] = None
    order_filled_timestamp: Optional[int] = None
    ft_fee_base: Optional[float] = None
    ft_order_tag: Optional[str] = None


class TradeSchema(BaseModel):
    trade_id: int
    pair: str
    base_currency: str
    quote_currency: str
    is_open: bool
    is_short: bool
    exchange: str
    amount: float
    amount_requested: float
    stake_amount: float
    max_stake_amount: Optional[float] = None
    strategy: str
    enter_tag: Optional[str] = None
    timeframe: int
    fee_open: Optional[float] = None
    fee_open_cost: Optional[float] = None
    fee_open_currency: Optional[str] = None
    fee_close: Optional[float] = None
    fee_close_cost: Optional[float] = None
    fee_close_currency: Optional[str] = None

    open_date: str
    open_timestamp: int
    open_fill_date: Optional[str]
    open_fill_timestamp: Optional[int]
    open_rate: float
    open_rate_requested: Optional[float] = None
    open_trade_value: float

    close_date: Optional[str] = None
    close_timestamp: Optional[int] = None
    close_rate: Optional[float] = None
    close_rate_requested: Optional[float] = None

    close_profit: Optional[float] = None
    close_profit_pct: Optional[float] = None
    close_profit_abs: Optional[float] = None

    profit_ratio: Optional[float] = None
    profit_pct: Optional[float] = None
    profit_abs: Optional[float] = None
    profit_fiat: Optional[float] = None

    realized_profit: float
    realized_profit_ratio: Optional[float] = None

    exit_reason: Optional[str] = None
    exit_order_status: Optional[str] = None

    stop_loss_abs: Optional[float] = None
    stop_loss_ratio: Optional[float] = None
    stop_loss_pct: Optional[float] = None
    stoploss_last_update: Optional[str] = None
    stoploss_last_update_timestamp: Optional[int] = None
    initial_stop_loss_abs: Optional[float] = None
    initial_stop_loss_ratio: Optional[float] = None
    initial_stop_loss_pct: Optional[float] = None

    min_rate: Optional[float] = None
    max_rate: Optional[float] = None
    has_open_orders: bool
    orders: List[OrderSchema]

    leverage: Optional[float] = None
    interest_rate: Optional[float] = None
    liquidation_price: Optional[float] = None
    funding_fees: Optional[float] = None
    trading_mode: Optional[TradingMode] = None

    amount_precision: Optional[float] = None
    price_precision: Optional[float] = None
    precision_mode: Optional[int] = None


class OpenTradeSchema(TradeSchema):
    stoploss_current_dist: Optional[float] = None
    stoploss_current_dist_pct: Optional[float] = None
    stoploss_current_dist_ratio: Optional[float] = None
    stoploss_entry_dist: Optional[float] = None
    stoploss_entry_dist_ratio: Optional[float] = None
    current_rate: float
    total_profit_abs: float
    total_profit_fiat: Optional[float] = None
    total_profit_ratio: Optional[float] = None


class TradeResponse(BaseModel):
    trades: List[TradeSchema]
    trades_count: int
    offset: int
    total_trades: int


ForceEnterResponse = RootModel[Union[TradeSchema, StatusMsg]]


class LockModel(BaseModel):
    id: int
    active: bool
    lock_end_time: str
    lock_end_timestamp: int
    lock_time: str
    lock_timestamp: int
    pair: str
    side: str
    reason: Optional[str] = None


class Locks(BaseModel):
    lock_count: int
    locks: List[LockModel]


class LocksPayload(BaseModel):
    pair: str
    side: str = "*"  # Default to both sides
    until: AwareDatetime
    reason: Optional[str] = None


class DeleteLockRequest(BaseModel):
    pair: Optional[str] = None
    lockid: Optional[int] = None


class Logs(BaseModel):
    log_count: int
    logs: List[List]


class ForceEnterPayload(BaseModel):
    pair: str
    side: SignalDirection = SignalDirection.LONG
    price: Optional[float] = None
    ordertype: Optional[OrderTypeValues] = None
    stakeamount: Optional[float] = None
    entry_tag: Optional[str] = None
    leverage: Optional[float] = None


class ForceExitPayload(BaseModel):
    tradeid: Union[str, int]
    ordertype: Optional[OrderTypeValues] = None
    amount: Optional[float] = None


class BlacklistPayload(BaseModel):
    blacklist: List[str]


class BlacklistResponse(BaseModel):
    blacklist: List[str]
    blacklist_expanded: List[str]
    errors: Dict
    length: int
    method: List[str]


class WhitelistResponse(BaseModel):
    whitelist: List[str]
    length: int
    method: List[str]


class WhitelistEvaluateResponse(BackgroundTaskResult):
    result: Optional[WhitelistResponse] = None


class DeleteTrade(BaseModel):
    cancel_order_count: int
    result: str
    result_msg: str
    trade_id: int


class PlotConfig_(BaseModel):
    main_plot: Dict[str, Any]
    subplots: Dict[str, Any]


PlotConfig = RootModel[Union[PlotConfig_, Dict]]


class StrategyListResponse(BaseModel):
    strategies: List[str]


class ExchangeListResponse(BaseModel):
    exchanges: List[ValidExchangesType]


class PairListResponse(BaseModel):
    name: str
    description: str
    is_pairlist_generator: bool
    params: Dict[str, Any]


class PairListsResponse(BaseModel):
    pairlists: List[PairListResponse]


class PairListsPayload(ExchangeModePayloadMixin, BaseModel):
    pairlists: List[Dict[str, Any]]
    blacklist: List[str]
    stake_currency: str


class FreqAIModelListResponse(BaseModel):
    freqaimodels: List[str]


class StrategyResponse(BaseModel):
    strategy: str
    code: str
    timeframe: Optional[str]


class AvailablePairs(BaseModel):
    length: int
    pairs: List[str]
    pair_interval: List[List[str]]


class PairCandlesRequest(BaseModel):
    pair: str
    timeframe: str
    limit: Optional[int] = None
    columns: Optional[List[str]] = None


class PairHistoryRequest(PairCandlesRequest):
    timerange: str
    strategy: str
    freqaimodel: Optional[str] = None


class PairHistory(BaseModel):
    strategy: str
    pair: str
    timeframe: str
    timeframe_ms: int
    columns: List[str]
    all_columns: List[str] = []
    data: SerializeAsAny[List[Any]]
    length: int
    buy_signals: int
    sell_signals: int
    enter_long_signals: int
    exit_long_signals: int
    enter_short_signals: int
    exit_short_signals: int
    last_analyzed: datetime
    last_analyzed_ts: int
    data_start_ts: int
    data_start: str
    data_stop: str
    data_stop_ts: int


class BacktestFreqAIInputs(BaseModel):
    identifier: str


class BacktestRequest(BaseModel):
    strategy: str
    timeframe: Optional[str] = None
    timeframe_detail: Optional[str] = None
    timerange: Optional[str] = None
    max_open_trades: Optional[IntOrInf] = None
    stake_amount: Optional[Union[str, float]] = None
    enable_protections: bool
    dry_run_wallet: Optional[float] = None
    backtest_cache: Optional[str] = None
    freqaimodel: Optional[str] = None
    freqai: Optional[BacktestFreqAIInputs] = None


class BacktestResponse(BaseModel):
    status: str
    running: bool
    status_msg: str
    step: str
    progress: float
    trade_count: Optional[float] = None
    # TODO: Properly type backtestresult...
    backtest_result: Optional[Dict[str, Any]] = None


# TODO: This is a copy of BacktestHistoryEntryType
class BacktestHistoryEntry(BaseModel):
    filename: str
    strategy: str
    run_id: str
    backtest_start_time: int
    notes: Optional[str] = ""
    backtest_start_ts: Optional[int] = None
    backtest_end_ts: Optional[int] = None
    timeframe: Optional[str] = None
    timeframe_detail: Optional[str] = None


class BacktestMetadataUpdate(BaseModel):
    strategy: str
    notes: str = ""


class BacktestMarketChange(BaseModel):
    columns: List[str]
    length: int
    data: List[List[Any]]


class SysInfo(BaseModel):
    cpu_pct: List[float]
    ram_pct: float


class Health(BaseModel):
    last_process: Optional[datetime] = None
    last_process_ts: Optional[int] = None
    bot_start: Optional[datetime] = None
    bot_start_ts: Optional[int] = None
    bot_startup: Optional[datetime] = None
    bot_startup_ts: Optional[int] = None
