"""Textual TUI dashboard for Horizon strategies."""

from __future__ import annotations

import logging
import time
from typing import Any, Callable

from textual.app import App, ComposeResult
from textual.timer import Timer
from textual.widgets import Footer, Header, Static

from horizon._horizon import Engine, Market
from horizon.context import FeedData


class PnLPanel(Static):
    """P&L display with equity curve sparkline."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._equity_history: list[float] = []

    def update_data(self, engine: Engine) -> None:
        status = engine.status()
        realized = status.total_realized_pnl
        unrealized = status.total_unrealized_pnl
        total = realized + unrealized
        daily_pnl = status.daily_pnl

        self._equity_history.append(total)
        # Keep last 60 data points
        if len(self._equity_history) > 60:
            self._equity_history = self._equity_history[-60:]

        # ASCII sparkline
        sparkline = _sparkline(self._equity_history, width=30)

        self.update(
            f"[b]P&L[/b]\n"
            f"Realized:   ${realized:>8.2f}\n"
            f"Unrealized: ${unrealized:>8.2f}\n"
            f"Total:      ${total:>8.2f}\n"
            f"Daily P&L:  ${daily_pnl:>8.2f}\n"
            f"Equity: {sparkline}"
        )


class PositionsPanel(Static):
    def update_data(self, engine: Engine) -> None:
        positions = engine.positions()
        lines = ["[b]Positions[/b]"]
        if not positions:
            lines.append("  (none)")
        for pos in positions[:10]:
            pnl = pos.realized_pnl + pos.unrealized_pnl
            pnl_str = f"[green]+{pnl:.2f}[/]" if pnl >= 0 else f"[red]{pnl:.2f}[/]"
            lines.append(
                f"  {pos.market_id[:20]:<20} {pos.side} "
                f"{pos.size:>6.1f} @ {pos.avg_entry_price:.4f}  {pnl_str}"
            )
        self.update("\n".join(lines))


class OrdersPanel(Static):
    def update_data(self, engine: Engine) -> None:
        orders = engine.open_orders()
        lines = [f"[b]Orders ({len(orders)} open)[/b]"]
        if not orders:
            lines.append("  (none)")
        for order in orders[:10]:
            lines.append(
                f"  {order.order_side} {order.price:.4f} x {order.size:.0f}  "
                f"{order.market_id[:20]}"
            )
        self.update("\n".join(lines))


class RiskPanel(Static):
    def update_data(self, engine: Engine) -> None:
        status = engine.status()
        ks = "[red]ON[/]" if status.kill_switch_active else "[green]off[/]"
        self.update(
            f"[b]Risk[/b]\n"
            f"  Positions: {status.active_positions}\n"
            f"  Orders:    {status.open_orders}\n"
            f"  Kill switch: {ks}\n"
            f"  Uptime: {status.uptime_secs}s"
        )


class FillsPanel(Static):
    def update_data(self, engine: Engine) -> None:
        fills = engine.recent_fills()
        lines = ["[b]Recent Fills[/b]"]
        if not fills:
            lines.append("  (none)")
        for fill in fills[-10:]:
            lines.append(
                f"  {fill.order_side} {fill.side} "
                f"{fill.size:.0f} @ {fill.price:.4f}  {fill.market_id[:16]}"
            )
        self.update("\n".join(lines))


class FeedsPanel(Static):
    """Feed display with bid/ask spread and staleness indicator."""

    def update_data(self, feed_data: dict[str, FeedData]) -> None:
        lines = ["[b]Feeds[/b]"]
        if not feed_data:
            lines.append("  (none)")
        for name, data in feed_data.items():
            if data.price > 0:
                age = time.time() - data.timestamp if data.timestamp > 0 else 999
                age_str = f"{age:.0f}s" if age < 999 else "?"
                stale_marker = "[red]![/]" if age > 30 else ""
                vol_str = f" vol={data.volume_24h:,.0f}" if data.volume_24h > 0 else ""
                src_str = f" [{data.source}]" if data.source else ""

                if data.bid > 0 and data.ask > 0:
                    spread = data.ask - data.bid
                    lines.append(
                        f"  {name}: {data.bid:.4f}/{data.ask:.4f} "
                        f"(s={spread:.4f}) {age_str}{vol_str}{src_str} {stale_marker}"
                    )
                else:
                    lines.append(
                        f"  {name}: ${data.price:,.4f} "
                        f"{age_str}{vol_str}{src_str} {stale_marker}"
                    )
            else:
                lines.append(f"  {name}: [dim]waiting...[/]")
        self.update("\n".join(lines))


class AlertsPanel(Static):
    """Display recent alerts."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._alerts: list[str] = []

    def add_alert(self, message: str) -> None:
        self._alerts.append(message)
        if len(self._alerts) > 20:
            self._alerts = self._alerts[-20:]
        self._render()

    def _render(self) -> None:
        lines = ["[b]Alerts[/b]"]
        if not self._alerts:
            lines.append("  (none)")
        for msg in self._alerts[-8:]:
            lines.append(f"  {msg}")
        self.update("\n".join(lines))

    def update_data(self) -> None:
        self._render()


def _sparkline(values: list[float], width: int = 30) -> str:
    """Generate an ASCII sparkline from a list of values."""
    if not values:
        return ""
    chars = "▁▂▃▄▅▆▇█"
    # Sample to width
    if len(values) > width:
        step = len(values) / width
        sampled = [values[int(i * step)] for i in range(width)]
    else:
        sampled = values

    lo = min(sampled)
    hi = max(sampled)
    rng = hi - lo if hi > lo else 1.0

    return "".join(
        chars[min(int((v - lo) / rng * (len(chars) - 1)), len(chars) - 1)]
        for v in sampled
    )


class HorizonApp(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 2 4;
        grid-gutter: 1;
    }
    .panel {
        border: solid green;
        padding: 1;
        height: 100%;
    }
    """

    BINDINGS = [
        ("q", "quit", "Quit"),
        ("k", "kill_switch", "Kill Switch"),
        ("p", "pause_resume", "Pause/Resume"),
    ]

    def __init__(
        self,
        name: str,
        engine: Engine,
        markets: list[Market],
        feeds: dict[str, Any],
        pipeline: list[Callable] | dict[str, list[Callable]],
        interval: float,
        params: dict[str, Any],
        event_by_market: dict[str, Any] | None = None,
    ):
        super().__init__()
        self.strategy_name = name
        self.engine = engine
        self.markets = markets
        self.feed_configs = feeds
        self.pipeline = pipeline
        self.interval = interval
        self.params = params
        self.event_by_market = event_by_market or {}
        self._timer: Timer | None = None
        self._tick_count: int = 0
        self._paused: bool = False

    def compose(self) -> ComposeResult:
        yield Header()
        yield PnLPanel(classes="panel", id="pnl")
        yield PositionsPanel(classes="panel", id="positions")
        yield OrdersPanel(classes="panel", id="orders")
        yield RiskPanel(classes="panel", id="risk")
        yield FillsPanel(classes="panel", id="fills")
        yield FeedsPanel(classes="panel", id="feeds")
        yield AlertsPanel(classes="panel", id="alerts")
        yield Static(classes="panel", id="info")
        yield Footer()

    def on_mount(self) -> None:
        exchange_name = self.engine.exchange_name().upper()
        self.title = f"Horizon: {self.strategy_name}"
        self.sub_title = f"{exchange_name} | {len(self.markets)} markets"

        # Set daily baseline (mirrors _run_loop lines 492-494)
        status = self.engine.status()
        baseline = status.total_realized_pnl + status.total_unrealized_pnl
        self.engine.set_daily_baseline(baseline)

        # Wire market expiry timestamps for lifecycle management (mirrors _run_loop lines 498-500)
        for market in self.markets:
            if hasattr(market, "expiry") and market.expiry:
                self.engine.set_market_expiry(market.id, market.expiry)

        self._timer = self.set_interval(self.interval, self._tick)

    def _tick(self) -> None:
        if self._paused:
            return

        try:
            self._tick_count += 1

            # Poll fills for live exchanges before running pipeline
            if self.engine.exchange_name() != "paper":
                self.engine.poll_fills()

            # Update daily P&L for drawdown tracking
            status = self.engine.status()
            current_pnl = status.total_realized_pnl + status.total_unrealized_pnl
            self.engine.update_daily_pnl(current_pnl)

            # Run pipeline for each market
            from horizon.strategy import _build_context, _process_result, _run_pipeline

            is_live = self.engine.exchange_name() != "paper"
            feed_stale_threshold = self.params.get("feed_stale_threshold", 30.0)

            for market in self.markets:
                event = self.event_by_market.get(market.id)
                ctx = _build_context(self.engine, market, self.feed_configs, self.params,
                                     cached_status=status, event=event)

                # Feed staleness check - skip quoting if any feed is stale (live only)
                if is_live:
                    stale_feeds = [
                        fname for fname, fdata in ctx.feeds.items()
                        if fdata.is_stale(feed_stale_threshold)
                    ]
                    if stale_feeds:
                        self.query_one("#alerts", AlertsPanel).add_alert(
                            f"Stale feeds for {market.id}: {stale_feeds}"
                        )
                        continue

                # Resolve per-market pipeline (dict pipeline support)
                if isinstance(self.pipeline, dict):
                    market_pipeline = self.pipeline.get(market.id, self.pipeline.get("*"))
                    if market_pipeline is None:
                        continue
                else:
                    market_pipeline = self.pipeline

                result = _run_pipeline(market_pipeline, ctx)
                if result is not None:
                    _process_result(self.engine, market, result, ctx)

            # Lifecycle check
            if self._tick_count % 10 == 0:
                acted = self.engine.check_lifecycle()
                for mid in acted:
                    self.query_one("#alerts", AlertsPanel).add_alert(
                        f"Lifecycle: canceled for {mid}"
                    )

            # Build feed data from engine's feed manager for display
            feed_data: dict[str, FeedData] = {}
            for feed_name in self.feed_configs:
                snapshot = self.engine.feed_snapshot(feed_name)
                if snapshot is not None:
                    feed_data[feed_name] = FeedData(
                        price=snapshot.price,
                        timestamp=snapshot.timestamp,
                        bid=snapshot.bid,
                        ask=snapshot.ask,
                        volume_24h=snapshot.volume_24h,
                        source=snapshot.source,
                    )
                else:
                    feed_data[feed_name] = FeedData()

            # Snapshot positions to DB periodically (every 50 ticks)
            if self._tick_count % 50 == 0:
                self.engine.snapshot_positions()

            # Update all panels
            self.query_one("#pnl", PnLPanel).update_data(self.engine)
            self.query_one("#positions", PositionsPanel).update_data(self.engine)
            self.query_one("#orders", OrdersPanel).update_data(self.engine)
            self.query_one("#risk", RiskPanel).update_data(self.engine)
            self.query_one("#fills", FillsPanel).update_data(self.engine)
            self.query_one("#feeds", FeedsPanel).update_data(feed_data)
            self.query_one("#alerts", AlertsPanel).update_data()

            # Update info panel
            info = self.query_one("#info", Static)
            info.update(
                f"[b]Info[/b]\n"
                f"  Cycle: {self._tick_count}\n"
                f"  Markets: {len(self.markets)}\n"
                f"  Feeds: {len(self.feed_configs)}\n"
                f"  Exchanges: {', '.join(self.engine.exchange_names())}"
            )
        except Exception as e:
            logging.getLogger("horizon").error("Dashboard tick error: %s", e, exc_info=True)

    def action_kill_switch(self) -> None:
        status = self.engine.status()
        if status.kill_switch_active:
            self.engine.deactivate_kill_switch()
        else:
            self.engine.activate_kill_switch("manual")

    def action_pause_resume(self) -> None:
        self._paused = not self._paused
        state = "PAUSED" if self._paused else "RUNNING"
        self.query_one("#alerts", AlertsPanel).add_alert(f"Strategy {state}")
