"""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.containers import Container, Horizontal, Vertical
from textual.timer import Timer
from textual.widgets import Footer, Header, Static

from horizon._horizon import Engine, Market, Quote, Side
from horizon.context import Context, FeedData, InventorySnapshot


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 ""

                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} {stale_marker}"
                    )
                else:
                    lines.append(f"  {name}: ${data.price:,.4f} {age_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],
        interval: float,
        params: dict[str, Any],
    ):
        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._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"
        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:
                ctx = _build_context(self.engine, market, self.feed_configs, self.params)

                # 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

                result = _run_pipeline(self.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,
                    )
                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}")
