"""Tachyon gate for freqtrade: pauses entries while the market is fragile.

    from tachyon_gate import TachyonGate

    class MyStrategy(TachyonGate, IStrategy):
        tachyon_api_key = "..."     # or set TACHYON_API_KEY

TachyonGate goes first, so its callbacks run. A strategy that defines one of
them itself must call `super()`. Open positions and exits are never blocked.
The defaults are the tiered policy in include/tachyon_policy.hpp, and every one
can be changed on the strategy. It only acts in live and dry-run: the state has
no history to backtest against.
"""
from __future__ import annotations

import logging
import os
import time
from datetime import datetime
from typing import Any, Optional

from tachyon_client import DEFAULT_BASE_URL, State, TachyonClient, TachyonError

logger = logging.getLogger(__name__)


def tachyon_pair(pair: str) -> str:
    """BTC/USDT:USDT -> BTCUSDT"""
    return pair.split(":")[0].replace("/", "")


class TachyonGate:
    tachyon_api_key: str = ""
    tachyon_base_url: str = DEFAULT_BASE_URL
    # States that block new entries.
    tachyon_pause_states = ("ANOMALY", "CIRCUIT_BREAKER")
    # Stake multiple in SHIFT, the "reduce size" reflex. 1.0 turns it off.
    tachyon_shift_stake: float = 0.5
    # Unknown is not NORMAL: no reading blocks entries. Set False to fail open.
    tachyon_block_when_unknown: bool = True
    # Exit open positions on CIRCUIT_BREAKER. Off: the gate only pauses entries.
    tachyon_exit_on_breaker: bool = False
    # Reads per pair are cached this long. The API allows 10 requests a second.
    tachyon_cache_s: float = 1.0

    _tachyon_client: Optional[TachyonClient] = None
    _tachyon_cache: Optional[dict] = None

    def _tachyon_active(self) -> bool:
        dp = getattr(self, "dp", None)
        return dp is not None and dp.runmode.value in ("live", "dry_run")

    def tachyon_state(self, pair: str) -> Optional[State]:
        """The current reading, or None if there is none."""
        if self._tachyon_client is None:
            key = self.tachyon_api_key or os.environ.get("TACHYON_API_KEY", "")
            self._tachyon_client = TachyonClient(key, self.tachyon_base_url)
            self._tachyon_cache = {}
        symbol = tachyon_pair(pair)
        now = time.monotonic()
        cached = self._tachyon_cache.get(symbol)
        if cached is not None and now - cached[0] < self.tachyon_cache_s:
            return cached[1]
        try:
            reading = self._tachyon_client.state(symbol)
            reading = reading if reading.known else None
        except TachyonError as error:
            logger.warning("Tachyon has no reading for %s: %s", symbol, error)
            reading = None
        self._tachyon_cache[symbol] = (now, reading)
        return reading

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: Optional[str],
        side: str,
        **kwargs: Any,
    ) -> bool:
        if self._tachyon_active():
            reading = self.tachyon_state(pair)
            if reading is None:
                if self.tachyon_block_when_unknown:
                    logger.info("Tachyon: entry on %s blocked, state unknown", pair)
                    return False
            elif reading.state in self.tachyon_pause_states:
                logger.info("Tachyon: entry on %s blocked, %s", pair, reading.state)
                return False
        return super().confirm_trade_entry(
            pair, order_type, amount, rate, time_in_force, current_time, entry_tag, side, **kwargs
        )

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: Optional[float],
        max_stake: float,
        leverage: float,
        entry_tag: Optional[str],
        side: str,
        **kwargs: Any,
    ) -> float:
        stake = super().custom_stake_amount(
            pair, current_time, current_rate, proposed_stake, min_stake, max_stake,
            leverage, entry_tag, side, **kwargs
        )
        if self._tachyon_active():
            reading = self.tachyon_state(pair)
            if reading is not None and reading.state == "SHIFT":
                return stake * self.tachyon_shift_stake
        return stake

    def custom_exit(
        self,
        pair: str,
        trade: Any,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> Optional[str]:
        if self.tachyon_exit_on_breaker and self._tachyon_active():
            reading = self.tachyon_state(pair)
            if reading is not None and reading.state == "CIRCUIT_BREAKER":
                return "tachyon_breaker"
        return super().custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)
