"""StrategyContext — provides Clock, Cache, and Portfolio abstractions.

In live mode these delegate to the real broker. In backtest mode they
operate on simulated state. Strategies never touch the context internals
directly — they use the public API on the Strategy base class.
"""

from __future__ import annotations

import time
from collections import defaultdict, deque
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Deque, Dict, List, Optional

from ..model.events import BarEvent, FillEvent


class Clock:
    """System clock — real-time in live mode, simulated in backtest."""

    def __init__(self) -> None:
        self._simulated_ns: Optional[int] = None

    def set_time(self, ts_ns: int) -> None:
        self._simulated_ns = ts_ns

    def reset(self) -> None:
        self._simulated_ns = None

    def timestamp_ns(self) -> int:
        if self._simulated_ns is not None:
            return self._simulated_ns
        return int(time.time() * 1_000_000_000)

    def utc_now(self) -> datetime:
        ns = self.timestamp_ns()
        return datetime.fromtimestamp(ns / 1e9, tz=timezone.utc)

    @property
    def is_simulated(self) -> bool:
        return self._simulated_ns is not None


class Cache:
    """In-memory cache for bars, quotes, orders, and positions."""

    def __init__(self, max_bars: int = 500) -> None:
        self._max_bars = max_bars
        self._bars: Dict[str, Deque[BarEvent]] = defaultdict(lambda: deque(maxlen=max_bars))
        self._last_quote: Dict[str, Any] = {}
        self._orders: Dict[str, Any] = {}
        self._custom: Dict[str, Any] = {}

    def push_bar(self, bar: BarEvent) -> None:
        self._bars[bar.symbol].append(bar)

    def bars(self, symbol: str, limit: int | None = None) -> List[BarEvent]:
        all_bars = list(self._bars.get(symbol, []))
        if limit:
            return all_bars[-limit:]
        return all_bars

    def latest_bar(self, symbol: str) -> Optional[BarEvent]:
        dq = self._bars.get(symbol)
        return dq[-1] if dq else None

    def set_quote(self, symbol: str, quote: Any) -> None:
        self._last_quote[symbol] = quote

    def quote(self, symbol: str) -> Any:
        return self._last_quote.get(symbol)

    def set_order(self, order_id: str, order: Any) -> None:
        self._orders[order_id] = order

    def order(self, order_id: str) -> Any:
        return self._orders.get(order_id)

    def set(self, key: str, value: Any) -> None:
        self._custom[key] = value

    def get(self, key: str, default: Any = None) -> Any:
        return self._custom.get(key, default)

    def clear(self) -> None:
        self._bars.clear()
        self._last_quote.clear()
        self._orders.clear()
        self._custom.clear()


@dataclass
class PositionState:
    """Tracked position for a single symbol."""

    symbol: str = ""
    qty: float = 0.0
    avg_entry_price: float = 0.0
    realized_pnl: float = 0.0
    cost_basis: float = 0.0

    @property
    def is_long(self) -> bool:
        return self.qty > 0

    @property
    def is_short(self) -> bool:
        return self.qty < 0

    @property
    def is_flat(self) -> bool:
        return self.qty == 0

    def market_value(self, current_price: float) -> float:
        return self.qty * current_price

    def unrealized_pnl(self, current_price: float) -> float:
        if self.qty == 0:
            return 0.0
        return (current_price - self.avg_entry_price) * self.qty


class Portfolio:
    """Portfolio state — positions, equity, P&L tracking."""

    def __init__(self, initial_cash: float = 100_000.0) -> None:
        self._initial_cash = initial_cash
        self._cash = initial_cash
        self._positions: Dict[str, PositionState] = {}
        self._realized_pnl = 0.0
        self._commission_total = 0.0
        self._last_prices: Dict[str, float] = {}

    @property
    def cash(self) -> float:
        return self._cash

    @property
    def initial_cash(self) -> float:
        return self._initial_cash

    @property
    def realized_pnl(self) -> float:
        return self._realized_pnl

    @property
    def commission_total(self) -> float:
        return self._commission_total

    def equity(self) -> float:
        unrealized = sum(
            pos.unrealized_pnl(self._last_prices.get(sym, pos.avg_entry_price))
            for sym, pos in self._positions.items()
            if not pos.is_flat
        )
        return self._cash + sum(
            pos.market_value(self._last_prices.get(sym, pos.avg_entry_price))
            for sym, pos in self._positions.items()
            if not pos.is_flat
        )

    def update_price(self, symbol: str, price: float) -> None:
        self._last_prices[symbol] = price

    def position(self, symbol: str) -> Optional[PositionState]:
        pos = self._positions.get(symbol)
        if pos and pos.is_flat:
            return None
        return pos

    def positions(self) -> Dict[str, PositionState]:
        return {s: p for s, p in self._positions.items() if not p.is_flat}

    def is_long(self, symbol: str) -> bool:
        pos = self._positions.get(symbol)
        return pos is not None and pos.is_long

    def is_short(self, symbol: str) -> bool:
        pos = self._positions.get(symbol)
        return pos is not None and pos.is_short

    def is_flat(self, symbol: str) -> bool:
        pos = self._positions.get(symbol)
        return pos is None or pos.is_flat

    def is_completely_flat(self) -> bool:
        return all(p.is_flat for p in self._positions.values())

    def apply_fill(self, fill: FillEvent) -> None:
        """Update position state from a fill event."""
        sym = fill.symbol
        pos = self._positions.get(sym)
        if pos is None:
            pos = PositionState(symbol=sym)
            self._positions[sym] = pos

        signed_qty = fill.fill_qty if fill.side == "buy" else -fill.fill_qty
        cost = fill.fill_qty * fill.fill_price
        commission = fill.commission

        if pos.is_flat:
            pos.qty = signed_qty
            pos.avg_entry_price = fill.fill_price
            pos.cost_basis = cost
        elif (pos.qty > 0 and signed_qty > 0) or (pos.qty < 0 and signed_qty < 0):
            total_cost = pos.cost_basis + cost
            pos.qty += signed_qty
            pos.cost_basis = total_cost
            pos.avg_entry_price = total_cost / abs(pos.qty) if pos.qty != 0 else 0
        else:
            close_qty = min(abs(signed_qty), abs(pos.qty))
            pnl = close_qty * (fill.fill_price - pos.avg_entry_price) * (1 if pos.qty > 0 else -1)
            pos.realized_pnl += pnl
            self._realized_pnl += pnl
            pos.qty += signed_qty
            if abs(pos.qty) < 1e-10:
                pos.qty = 0.0
                pos.avg_entry_price = 0.0
                pos.cost_basis = 0.0
            else:
                pos.cost_basis = abs(pos.qty) * pos.avg_entry_price

        self._cash -= (cost if signed_qty > 0 else -cost)
        self._cash -= commission
        self._commission_total += commission
        self._last_prices[sym] = fill.fill_price

    def reset(self, initial_cash: float | None = None) -> None:
        if initial_cash is not None:
            self._initial_cash = initial_cash
        self._cash = self._initial_cash
        self._positions.clear()
        self._realized_pnl = 0.0
        self._commission_total = 0.0
        self._last_prices.clear()
