"""MockORBBroker: simulates Alpaca API for ORB paper trading tests.

Generates synthetic OHLCV data and simulates order fills without any
real Alpaca API calls. Suitable for offline unit/integration testing.
"""
from __future__ import annotations

import datetime as dt
import random
import uuid
from dataclasses import dataclass, field
from typing import Any
from zoneinfo import ZoneInfo

from apps.paper_trader.alpaca_broker import AccountInfo, Bar, Order, Position

_ET = ZoneInfo("America/New_York")


# ── Synthetic data generation ─────────────────────────────────────────────────

def make_daily_bars(
    ticker: str,
    date_str: str,
    n_days: int = 70,
    base_price: float = 50.0,
    daily_vol: float = 0.015,
    avg_volume: int = 3_000_000,
) -> list[Bar]:
    """Generate n_days of synthetic daily OHLCV bars ending at date_str.

    Designed to pass live_pre_screen() filters:
    - close >= 10 (min_price)
    - ATR14 >= 0.5 (min_atr_14)  → base_price * daily_vol * ~1.4 per day
    - avg_dollar_vol_30d >= 25M   → avg_volume * base_price
    """
    rng = random.Random(hash(ticker + date_str))
    today = dt.date.fromisoformat(date_str)
    price = base_price
    bars = []
    for i in range(n_days, 0, -1):
        bar_date = today - dt.timedelta(days=i)
        if bar_date.weekday() >= 5:   # skip weekends
            continue
        day_ret = rng.gauss(0, daily_vol)
        open_ = price * (1 + rng.gauss(0, 0.003))
        close = price * (1 + day_ret)
        high = max(open_, close) * (1 + abs(rng.gauss(0, 0.005)))
        low = min(open_, close) * (1 - abs(rng.gauss(0, 0.005)))
        vol = int(avg_volume * rng.uniform(0.7, 1.4))
        bars.append(Bar(
            date=bar_date.isoformat(),
            open=round(open_, 2),
            high=round(high, 2),
            low=round(low, 2),
            close=round(close, 2),
            volume=vol,
        ))
        price = close
    return bars


def make_intraday_bars(
    ticker: str,
    date_str: str,
    n_bars: int = 12,
    base_price: float = 50.0,
    bar_vol: float = 0.003,
    avg_volume_per_bar: int = 200_000,
    orb_minutes: int = 10,
) -> list[dict[str, Any]]:
    """Generate synthetic 5-min intraday bars starting at 9:30 ET.

    The ORB high is inflated slightly so breakout is clear.
    n_bars covers from 9:30 to ~10:30 (12 bars × 5 min).
    """
    rng = random.Random(hash(ticker + date_str + "intraday"))
    today = dt.date.fromisoformat(date_str)
    price = base_price
    bars = []
    orb_bar_count = orb_minutes // 5  # how many 5-min bars form the ORB window

    for i in range(n_bars):
        bar_start = dt.datetime(
            today.year, today.month, today.day, 9, 30, tzinfo=_ET
        ) + dt.timedelta(minutes=5 * i)

        is_orb = i < orb_bar_count
        # Make ORB bars slightly more bullish for a clean breakout signal
        ret = rng.gauss(0.002 if is_orb else 0.0, bar_vol)
        open_ = price
        close = price * (1 + ret)
        high = max(open_, close) * (1 + abs(rng.gauss(0, 0.002)))
        low = min(open_, close) * (1 - abs(rng.gauss(0, 0.002)))
        vol = int(avg_volume_per_bar * rng.uniform(0.5, 1.5))

        bars.append({
            "timestamp": bar_start.isoformat(),
            "open": round(open_, 2),
            "high": round(high, 2),
            "low": round(low, 2),
            "close": round(close, 2),
            "volume": vol,
        })
        price = close

    return bars


# ── MockORBBroker ─────────────────────────────────────────────────────────────

class MockORBBroker:
    """Simulates AlpacaBroker for ORB paper trading tests.

    Maintains in-memory positions and cash; no API calls.
    """

    def __init__(
        self,
        initial_equity: float = 10_000.0,
        tickers: list[str] | None = None,
    ) -> None:
        self._equity = initial_equity
        self._cash = float(initial_equity)
        self._tickers = tickers or ["AAPL", "NVDA", "MSFT"]
        self._positions: dict[str, dict[str, Any]] = {}  # ticker -> {qty, avg_price}
        self._orders: dict[str, Order] = {}
        self._filled_at: dict[str, float] = {}  # order_id -> fill price

        # Precompute base prices so they're consistent
        self._base_prices = {
            t: 40.0 + hash(t) % 60 for t in self._tickers
        }

    # ── Account ───────────────────────────────────────────────────────────────

    def get_account(self) -> AccountInfo:
        market_value = sum(
            p["qty"] * p["avg_price"] for p in self._positions.values()
        )
        return AccountInfo(
            equity=self._cash + market_value,
            cash=self._cash,
            buying_power=self._cash,
            long_market_value=market_value,
            unrealized_pl=0.0,
            portfolio_value=self._cash + market_value,
        )

    # ── Bars ──────────────────────────────────────────────────────────────────

    def get_bars(
        self,
        tickers: list[str],
        start: dt.date,
        end: dt.date,
    ) -> dict[str, list[Bar]]:
        """Return synthetic daily bars. Ignores start/end for simplicity."""
        result: dict[str, list[Bar]] = {}
        for ticker in tickers:
            base = self._base_prices.get(ticker, 50.0)
            result[ticker] = make_daily_bars(ticker, end.isoformat(), base_price=base)
        return result

    def get_intraday_bars(
        self,
        tickers: list[str],
        start: dt.datetime,
        end: dt.datetime,
        timeframe_minutes: int = 5,
    ) -> dict[str, list[dict[str, Any]]]:
        """Return synthetic 5-min intraday bars."""
        date_str = start.date().isoformat()
        result: dict[str, list[dict[str, Any]]] = {}
        for ticker in tickers:
            base = self._base_prices.get(ticker, 50.0)
            result[ticker] = make_intraday_bars(ticker, date_str, base_price=base)
        return result

    # ── Orders ────────────────────────────────────────────────────────────────

    def _make_order(self, ticker: str, qty: int, side: str, fill_price: float) -> Order:
        order_id = str(uuid.uuid4())[:8]
        order = Order(
            id=order_id,
            symbol=ticker,
            qty=qty,
            side=side,
            status="filled",
            filled_avg_price=fill_price,
            filled_qty=qty,
        )
        self._orders[order_id] = order
        self._filled_at[order_id] = fill_price
        return order

    def submit_market_buy(self, ticker: str, qty: int) -> Order:
        price = self._base_prices.get(ticker, 50.0)
        if ticker in self._positions:
            self._positions[ticker]["qty"] += qty
        else:
            self._positions[ticker] = {"qty": qty, "avg_price": price}
        self._cash -= price * qty
        return self._make_order(ticker, qty, "buy", price)

    def submit_market_sell(self, ticker: str, qty: int) -> Order:
        """Short sell."""
        price = self._base_prices.get(ticker, 50.0)
        self._cash += price * qty
        self._positions[ticker] = {"qty": -qty, "avg_price": price}
        return self._make_order(ticker, qty, "sell", price)

    def get_order(self, order_id: str) -> Order:
        return self._orders[order_id]

    def close_position(self, ticker: str) -> Order:
        pos = self._positions.pop(ticker, None)
        price = self._base_prices.get(ticker, 50.0)
        if pos:
            pnl = (price - pos["avg_price"]) * pos["qty"]
            self._cash += price * abs(pos["qty"]) + pnl
        return self._make_order(ticker, abs(pos["qty"]) if pos else 1, "sell", price)

    def list_positions(self) -> list[Position]:
        result = []
        for ticker, pos in self._positions.items():
            price = self._base_prices.get(ticker, 50.0)
            result.append(Position(
                symbol=ticker,
                qty=pos["qty"],
                avg_entry_price=pos["avg_price"],
                current_price=price,
                unrealized_pl=(price - pos["avg_price"]) * pos["qty"],
                market_value=price * pos["qty"],
            ))
        return result


# ── Mock Oracle snapshots ─────────────────────────────────────────────────────

def make_mock_snapshots(
    tickers: list[str],
    date_str: str,
    price_mult: float = 1.01,
) -> dict[str, Any]:
    """Return fake AlpacaSnapshot objects with price slightly above ORB high.

    Used to patch libs.oracle_client.alpaca.get_snapshots() in tests.
    price_mult > 1.0 ensures the snapshot price triggers a long breakout.
    """
    from libs.oracle_client.alpaca import AlpacaSnapshot

    result: dict[str, Any] = {}
    for ticker in tickers:
        base = 40.0 + hash(ticker) % 60
        snap_price = round(base * price_mult, 2)
        result[ticker] = AlpacaSnapshot(
            ticker=ticker,
            price=snap_price,
            bid=round(snap_price - 0.01, 2),
            ask=round(snap_price + 0.01, 2),
            prev_close=round(base * 0.99, 2),
            change=round(snap_price - base * 0.99, 2),
            change_pct=round((snap_price / (base * 0.99) - 1) * 100, 2),
        )
    return result
