from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pandas as pd
import plotly.graph_objects as go
import streamlit as st
import yfinance as yf
from streamlit_autorefresh import st_autorefresh

from web_core.analytics import backtest_signals, simulate_trend_trades
from web_core.charting import build_figure
from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS, TREND_BEAR, TREND_BULL, TREND_NEUTRAL
from web_core.data import fetch_ohlc, maybe_drop_live_bar
from web_core.exporting import df_for_export
from web_core.strategy import classify_bars, detect_trends

SETTINGS_PATH = Path.home() / ".web_local_shell" / "settings.json"
LEGACY_SETTINGS_PATH = Path.home() / ".manesh_trader" / "settings.json"


def _clamp_int(value: Any, fallback: int, minimum: int, maximum: int) -> int:
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        return fallback
    return min(maximum, max(minimum, parsed))


def _clamp_float(value: Any, fallback: float, minimum: float, maximum: float) -> float:
    try:
        parsed = float(value)
    except (TypeError, ValueError):
        return fallback
    return min(maximum, max(minimum, parsed))


def _to_bool(value: Any, fallback: bool) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return value != 0
    if value is None:
        return fallback

    normalized = str(value).strip().lower()
    if normalized in {"1", "true", "yes", "y", "on"}:
        return True
    if normalized in {"0", "false", "no", "n", "off"}:
        return False
    return fallback


def _clamp_max_bars(value: Any, fallback: int = 500) -> int:
    return _clamp_int(value=value, fallback=fallback, minimum=20, maximum=5000)


def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
    raw = raw or {}
    defaults: dict[str, Any] = {
        "symbol": "AAPL",
        "interval": "1d",
        "period": "6mo",
        "max_bars": 500,
        "drop_live": True,
        "use_body_range": False,
        "volume_filter_enabled": False,
        "volume_sma_window": 20,
        "volume_multiplier": 1.0,
        "gray_fake": True,
        "hide_market_closed_gaps": True,
        "show_live_guide": True,
        "show_past_behavior": True,
        "show_trade_markers": True,
        "focus_chart_on_selected_example": False,
        "max_training_examples": 20,
        "enable_auto_refresh": False,
        "refresh_sec": 60,
    }

    symbol = str(raw.get("symbol", defaults["symbol"])).strip().upper()
    if not symbol:
        symbol = str(defaults["symbol"])

    interval = str(raw.get("interval", defaults["interval"]))
    if interval not in INTERVAL_OPTIONS:
        interval = str(defaults["interval"])

    period = str(raw.get("period", defaults["period"]))
    if period not in PERIOD_OPTIONS:
        period = str(defaults["period"])

    max_bars = _clamp_max_bars(raw.get("max_bars"), fallback=int(defaults["max_bars"]))
    drop_live = _to_bool(raw.get("drop_live"), fallback=bool(defaults["drop_live"]))
    use_body_range = _to_bool(raw.get("use_body_range"), fallback=bool(defaults["use_body_range"]))
    volume_filter_enabled = _to_bool(
        raw.get("volume_filter_enabled"), fallback=bool(defaults["volume_filter_enabled"])
    )
    volume_sma_window = _clamp_int(
        raw.get("volume_sma_window"),
        fallback=int(defaults["volume_sma_window"]),
        minimum=2,
        maximum=100,
    )
    volume_multiplier = round(
        _clamp_float(
            raw.get("volume_multiplier"),
            fallback=float(defaults["volume_multiplier"]),
            minimum=0.1,
            maximum=3.0,
        ),
        1,
    )
    gray_fake = _to_bool(raw.get("gray_fake"), fallback=bool(defaults["gray_fake"]))
    hide_market_closed_gaps = _to_bool(
        raw.get("hide_market_closed_gaps"),
        fallback=bool(defaults["hide_market_closed_gaps"]),
    )
    show_live_guide = _to_bool(raw.get("show_live_guide"), fallback=bool(defaults["show_live_guide"]))
    show_past_behavior = _to_bool(raw.get("show_past_behavior"), fallback=bool(defaults["show_past_behavior"]))
    show_trade_markers = _to_bool(raw.get("show_trade_markers"), fallback=bool(defaults["show_trade_markers"]))
    focus_chart_on_selected_example = _to_bool(
        raw.get("focus_chart_on_selected_example"),
        fallback=bool(defaults["focus_chart_on_selected_example"]),
    )
    max_training_examples = _clamp_int(
        raw.get("max_training_examples"),
        fallback=int(defaults["max_training_examples"]),
        minimum=5,
        maximum=100,
    )
    enable_auto_refresh = _to_bool(raw.get("enable_auto_refresh"), fallback=bool(defaults["enable_auto_refresh"]))
    refresh_sec = _clamp_int(
        raw.get("refresh_sec"),
        fallback=int(defaults["refresh_sec"]),
        minimum=10,
        maximum=600,
    )

    return {
        "symbol": symbol,
        "interval": interval,
        "period": period,
        "max_bars": max_bars,
        "drop_live": drop_live,
        "use_body_range": use_body_range,
        "volume_filter_enabled": volume_filter_enabled,
        "volume_sma_window": volume_sma_window,
        "volume_multiplier": volume_multiplier,
        "gray_fake": gray_fake,
        "hide_market_closed_gaps": hide_market_closed_gaps,
        "show_live_guide": show_live_guide,
        "show_past_behavior": show_past_behavior,
        "show_trade_markers": show_trade_markers,
        "focus_chart_on_selected_example": focus_chart_on_selected_example,
        "max_training_examples": max_training_examples,
        "enable_auto_refresh": enable_auto_refresh,
        "refresh_sec": refresh_sec,
    }


def load_web_settings() -> dict[str, Any]:
    source_path = SETTINGS_PATH if SETTINGS_PATH.exists() else LEGACY_SETTINGS_PATH
    if not source_path.exists():
        return normalize_web_settings(None)
    try:
        payload = json.loads(source_path.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            return normalize_web_settings(None)
        return normalize_web_settings(payload)
    except Exception:
        return normalize_web_settings(None)


def save_web_settings(settings: dict[str, Any]) -> None:
    SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
    SETTINGS_PATH.write_text(json.dumps(normalize_web_settings(settings), indent=2), encoding="utf-8")


@st.cache_data(show_spinner=False)
def load_help_content() -> tuple[str, bool]:
    help_html_paths = [
        Path(__file__).with_name("web_core").joinpath("help.html"),
        Path(__file__).with_name("help.html"),
    ]
    for help_html_path in help_html_paths:
        if help_html_path.exists():
            return help_html_path.read_text(encoding="utf-8"), True

    onboarding_path = Path(__file__).with_name("ONBOARDING.md")
    if onboarding_path.exists():
        return onboarding_path.read_text(encoding="utf-8"), False
    return "Help content not found.", False


@st.dialog("Help & Quick Start", width="large")
def help_dialog() -> None:
    content, is_html = load_help_content()
    st.markdown(content, unsafe_allow_html=is_html)


@st.cache_data(show_spinner=False, ttl=3600)
def lookup_symbol_candidates(query: str, max_results: int = 10) -> list[dict[str, str]]:
    cleaned = query.strip()
    if len(cleaned) < 2:
        return []

    try:
        search = yf.Search(cleaned, max_results=max_results)
        quotes = getattr(search, "quotes", []) or []
    except Exception:
        return []

    seen_symbols: set[str] = set()
    candidates: list[dict[str, str]] = []
    for quote in quotes:
        symbol = str(quote.get("symbol", "")).strip().upper()
        if not symbol or symbol in seen_symbols:
            continue

        name = str(quote.get("shortname") or quote.get("longname") or "").strip()
        exchange = str(quote.get("exchDisp") or quote.get("exchange") or "").strip()
        type_display = str(quote.get("typeDisp") or quote.get("quoteType") or "").strip()

        seen_symbols.add(symbol)
        candidates.append(
            {
                "symbol": symbol,
                "name": name,
                "exchange": exchange,
                "type": type_display,
            }
        )
    return candidates


@st.cache_data(show_spinner=False, ttl=3600)
def resolve_symbol_identity(symbol: str) -> dict[str, str]:
    normalized_symbol = symbol.strip().upper()
    if not normalized_symbol:
        return {"symbol": "", "name": "", "exchange": ""}

    def _from_quote(quote: dict[str, Any]) -> dict[str, str]:
        return {
            "symbol": normalized_symbol,
            "name": str(quote.get("shortname") or quote.get("longname") or "").strip(),
            "exchange": str(quote.get("exchDisp") or quote.get("exchange") or "").strip(),
        }

    try:
        search = yf.Search(normalized_symbol, max_results=8)
        quotes = getattr(search, "quotes", []) or []
        for quote in quotes:
            candidate_symbol = str(quote.get("symbol", "")).strip().upper()
            if candidate_symbol == normalized_symbol:
                return _from_quote(quote)
        if quotes:
            return _from_quote(quotes[0])
    except Exception:
        pass

    try:
        info = yf.Ticker(normalized_symbol).info
        return {
            "symbol": normalized_symbol,
            "name": str(info.get("shortName") or info.get("longName") or "").strip(),
            "exchange": str(info.get("exchange") or "").strip(),
        }
    except Exception:
        return {"symbol": normalized_symbol, "name": "", "exchange": ""}


def build_live_decision_guide(
    trend_now: str,
    previous_trend: str,
    latest_classification: str,
) -> dict[str, str]:
    if trend_now == TREND_BULL:
        bias = "Long Bias"
        action = "Prefer pullback longs and avoid fresh shorts while the bullish trend remains active."
        invalidation = "Bullish bias is invalidated only after 2 consecutive real bearish bars confirm a reversal."
    elif trend_now == TREND_BEAR:
        bias = "Short Bias"
        action = "Prefer short setups on pops and avoid fresh longs while the bearish trend remains active."
        invalidation = "Bearish bias is invalidated only after 2 consecutive real bullish bars confirm a reversal."
    else:
        bias = "Stand Aside / Neutral"
        action = "Wait for 2 consecutive real bars in one direction before taking directional exposure."
        invalidation = "No active trend yet; avoid forcing trades in noisy ranges."

    if trend_now in {TREND_BULL, TREND_BEAR} and trend_now != previous_trend:
        confirmation = "Fresh Confirmation"
        confirmation_detail = "Latest closed bar confirmed a new active trend."
    else:
        confirmation = "No New Confirmation"
        confirmation_detail = "Latest closed bar did not confirm a new trend reversal."

    classification_hint = {
        "real_bull": "Latest bar closed above the previous range.",
        "real_bear": "Latest bar closed below the previous range.",
        "fake": "Latest bar stayed inside the previous range (noise).",
        "unclassified": "Latest bar is unclassified.",
    }.get(latest_classification, "Latest bar classification unavailable.")

    return {
        "bias": bias,
        "action": action,
        "invalidation": invalidation,
        "confirmation": confirmation,
        "confirmation_detail": confirmation_detail,
        "classification_hint": classification_hint,
    }


def main() -> None:
    st.set_page_config(page_title="Real Bars vs Fake Bars Analyzer", layout="wide")
    st.title("Real Bars vs Fake Bars Trend Analyzer")
    st.caption(
        "Price-action tool that classifies closed bars, filters fake bars, and tracks trend persistence using only real bars."
    )

    with st.sidebar:
        st.header("Data Settings")
        if st.button("Help / Quick Start", use_container_width=True):
            help_dialog()
        st.divider()

        query_params = st.query_params
        persisted_settings = load_web_settings()

        def first_query_value(key: str) -> str | None:
            raw = query_params.get(key)
            if raw is None:
                return None
            if isinstance(raw, list):
                return str(raw[0]) if raw else None
            return str(raw)

        query_overrides: dict[str, Any] = {}
        for key in persisted_settings:
            candidate = first_query_value(key)
            if candidate is not None:
                query_overrides[key] = candidate
        effective_defaults = normalize_web_settings({**persisted_settings, **query_overrides})

        st.subheader("Find Symbol")
        symbol_search_query = st.text_input(
            "Search by company or ticker",
            value="",
            placeholder="Apple, Tesla, Bitcoin...",
            help="Type a name (e.g. Apple) and select a result to prefill Symbol.",
        )
        symbol_from_search: str | None = None
        if symbol_search_query.strip():
            candidates = lookup_symbol_candidates(symbol_search_query)
            if candidates:
                result_placeholder = "Select a result..."
                labels = [result_placeholder] + [
                    " | ".join(
                        [
                            candidate["symbol"],
                            candidate["name"] or "No name",
                            candidate["exchange"] or "Unknown exchange",
                        ]
                    )
                    for candidate in candidates
                ]
                selected_label = st.selectbox("Matches", labels, index=0)
                if selected_label != result_placeholder:
                    selected_index = labels.index(selected_label) - 1
                    symbol_from_search = candidates[selected_index]["symbol"]
            else:
                st.caption("No matches found. Try another company name.")

        symbol = st.text_input(
            "Symbol",
            value=symbol_from_search or str(effective_defaults["symbol"]),
            help="Ticker or pair to analyze, e.g. AAPL, MSFT, BTC-USD.",
        ).strip().upper()
        interval = st.selectbox(
            "Timeframe",
            INTERVAL_OPTIONS,
            index=INTERVAL_OPTIONS.index(str(effective_defaults["interval"])),
            help="Bar size for each candle. Shorter intervals are noisier; 1d is a good default.",
        )
        period = st.selectbox(
            "Period",
            PERIOD_OPTIONS,
            index=PERIOD_OPTIONS.index(str(effective_defaults["period"])),
            help="How much history to load for trend analysis.",
        )
        max_bars = st.number_input(
            "Max bars",
            min_value=20,
            max_value=5000,
            value=int(effective_defaults["max_bars"]),
            step=10,
            help="Limits loaded candles to keep charting responsive. 500 is a solid starting point.",
        )
        drop_live = st.checkbox("Ignore potentially live last bar", value=bool(effective_defaults["drop_live"]))

        st.header("Classification Filters")
        use_body_range = st.checkbox(
            "Use previous body range (ignore wicks)",
            value=bool(effective_defaults["use_body_range"]),
        )
        volume_filter_enabled = st.checkbox(
            "Enable volume filter",
            value=bool(effective_defaults["volume_filter_enabled"]),
        )
        volume_sma_window = st.slider(
            "Volume SMA window",
            2,
            100,
            int(effective_defaults["volume_sma_window"]),
        )
        volume_multiplier = st.slider(
            "Min volume / SMA multiplier",
            0.1,
            3.0,
            float(effective_defaults["volume_multiplier"]),
            0.1,
        )
        gray_fake = st.checkbox("Gray out fake bars", value=bool(effective_defaults["gray_fake"]))
        hide_market_closed_gaps = st.checkbox(
            "Hide market-closed gaps (stocks)",
            value=bool(effective_defaults["hide_market_closed_gaps"]),
        )

        st.header("Training & Guidance")
        show_live_guide = st.checkbox(
            "Show live decision guide",
            value=bool(effective_defaults["show_live_guide"]),
            help="Shows a plain-English interpretation of current trend state and confirmation status.",
        )
        show_past_behavior = st.checkbox(
            "Show past behavior examples",
            value=bool(effective_defaults["show_past_behavior"]),
            help="Displays historical example trades based on trend confirmation and reversal signals.",
        )
        show_trade_markers = st.checkbox(
            "Overlay example entries/exits on chart",
            value=bool(effective_defaults["show_trade_markers"]),
            help="Adds entry/exit markers for the training examples onto the main chart.",
        )
        focus_chart_on_selected_example = st.checkbox(
            "Focus chart on selected example",
            value=bool(effective_defaults["focus_chart_on_selected_example"]),
            help="When a training row is selected, zooms chart around that trade window.",
        )
        max_training_examples = st.slider(
            "Max training examples",
            5,
            100,
            int(effective_defaults["max_training_examples"]),
            5,
        )

        st.header("Monitoring")
        enable_auto_refresh = st.checkbox("Auto-refresh", value=bool(effective_defaults["enable_auto_refresh"]))
        refresh_sec = st.slider("Refresh interval (seconds)", 10, 600, int(effective_defaults["refresh_sec"]), 10)

        if enable_auto_refresh:
            st_autorefresh(interval=refresh_sec * 1000, key="data_refresh")

    try:
        save_web_settings(
            {
                "symbol": symbol,
                "interval": interval,
                "period": period,
                "max_bars": int(max_bars),
                "drop_live": bool(drop_live),
                "use_body_range": bool(use_body_range),
                "volume_filter_enabled": bool(volume_filter_enabled),
                "volume_sma_window": int(volume_sma_window),
                "volume_multiplier": float(volume_multiplier),
                "gray_fake": bool(gray_fake),
                "hide_market_closed_gaps": bool(hide_market_closed_gaps),
                "show_live_guide": bool(show_live_guide),
                "show_past_behavior": bool(show_past_behavior),
                "show_trade_markers": bool(show_trade_markers),
                "focus_chart_on_selected_example": bool(focus_chart_on_selected_example),
                "max_training_examples": int(max_training_examples),
                "enable_auto_refresh": bool(enable_auto_refresh),
                "refresh_sec": int(refresh_sec),
            }
        )
    except Exception:
        # Non-fatal: app should run even if local settings cannot be saved.
        pass

    if not symbol:
        st.error("Please enter a symbol.")
        st.stop()

    symbol_identity = resolve_symbol_identity(symbol)
    identity_name = symbol_identity["name"]
    identity_exchange = symbol_identity["exchange"]
    if identity_name:
        st.markdown(f"### {symbol} - {identity_name}")
        if identity_exchange:
            st.caption(f"Exchange: {identity_exchange}")
    else:
        st.markdown(f"### {symbol}")

    try:
        raw = fetch_ohlc(symbol=symbol, interval=interval, period=period)
        raw = maybe_drop_live_bar(raw, interval=interval, enabled=drop_live)
        if len(raw) > max_bars:
            raw = raw.iloc[-max_bars:].copy()

        if len(raw) < 3:
            st.error("Not enough bars to classify. Increase period or use a broader timeframe.")
            st.stop()

        classified = classify_bars(
            raw,
            use_body_range=use_body_range,
            volume_filter_enabled=volume_filter_enabled,
            volume_sma_window=volume_sma_window,
            volume_multiplier=volume_multiplier,
        )
        analyzed, events = detect_trends(classified)

    except Exception as exc:
        st.error(f"Data error: {exc}")
        st.stop()

    latest = analyzed.iloc[-1]
    trend_now = str(latest["trend_state"])

    bull_count = int((analyzed["classification"] == "real_bull").sum())
    bear_count = int((analyzed["classification"] == "real_bear").sum())
    fake_count = int((analyzed["classification"] == "fake").sum())

    c1, c2, c3, c4 = st.columns(4)
    c1.metric("Current Trend", trend_now)
    c2.metric("Real Bullish Bars", bull_count)
    c3.metric("Real Bearish Bars", bear_count)
    c4.metric("Fake Bars", fake_count)

    previous_trend = str(analyzed.iloc[-2]["trend_state"]) if len(analyzed) > 1 else TREND_NEUTRAL
    latest_classification = str(latest["classification"])
    live_guide = build_live_decision_guide(
        trend_now=trend_now,
        previous_trend=previous_trend,
        latest_classification=latest_classification,
    )

    if show_live_guide:
        st.subheader("Live Decision Guide")
        g1, g2, g3 = st.columns(3)
        g1.metric("Bias", live_guide["bias"])
        g2.metric("Signal Status", live_guide["confirmation"])
        g3.metric("Latest Bar", latest_classification)
        st.caption(live_guide["confirmation_detail"])
        st.caption(live_guide["classification_hint"])
        st.info(live_guide["action"])
        st.caption(f"Invalidation rule: {live_guide['invalidation']}")

    alert_key = f"{symbol}-{interval}-{period}"
    newest_event = events[-1].event if events else ""
    previous_event = st.session_state.get(f"last_event-{alert_key}", "")
    if newest_event and newest_event != previous_event:
        st.warning(f"Alert: {newest_event}")
        st.session_state[f"last_event-{alert_key}"] = newest_event

    example_trades = simulate_trend_trades(analyzed, max_examples=int(max_training_examples))
    selected_trade: pd.Series | None = None

    if show_past_behavior:
        st.subheader("Past Behavior Examples (Training)")
        if example_trades.empty:
            st.info("No closed example trades yet. Expand the period/timeframe to include more trend reversals.")
        else:
            wins = int((example_trades["outcome"] == "Win").sum())
            losses = int((example_trades["outcome"] == "Loss").sum())
            total_examples = int(len(example_trades))
            win_rate = round((wins / total_examples) * 100.0, 2) if total_examples else 0.0
            avg_pnl = round(float(example_trades["pnl_pct"].mean()), 2)

            t1, t2, t3, t4 = st.columns(4)
            t1.metric("Closed Examples", total_examples)
            t2.metric("Wins / Losses", f"{wins} / {losses}")
            t3.metric("Example Win Rate", f"{win_rate}%")
            t4.metric("Avg P/L per Example", f"{avg_pnl}%")

            latest_example = example_trades.iloc[-1]
            st.caption(
                "Latest closed example: "
                f"{latest_example['direction']} from {latest_example['entry_timestamp']} "
                f"to {latest_example['exit_timestamp']} "
                f"({latest_example['bars_held']} bars, {latest_example['pnl_pct']}%)."
            )
            st.caption("Click a row below to highlight that specific example on the chart.")
            st.caption(
                "Example method: enter on trend confirmation bar close and exit on opposite trend confirmation."
            )

            display_examples_raw = example_trades.iloc[::-1].reset_index(drop=True)
            display_examples = display_examples_raw.copy()
            display_examples["entry_timestamp"] = display_examples["entry_timestamp"].astype(str)
            display_examples["exit_timestamp"] = display_examples["exit_timestamp"].astype(str)

            table_event = st.dataframe(
                display_examples,
                use_container_width=True,
                on_select="rerun",
                selection_mode="single-row",
                key=f"training-examples-{alert_key}",
            )
            selected_rows: list[int] = []
            try:
                selected_rows = list(table_event.selection.rows)
            except Exception:
                selected_rows = []

            selected_row_state_key = f"selected-training-row-{alert_key}"
            if selected_rows:
                selected_row_idx = int(selected_rows[0])
                st.session_state[selected_row_state_key] = selected_row_idx
            else:
                selected_row_idx = int(st.session_state.get(selected_row_state_key, 0))

            if selected_row_idx < 0 or selected_row_idx >= len(display_examples_raw):
                selected_row_idx = 0

            selected_trade = display_examples_raw.iloc[selected_row_idx]
            direction = str(selected_trade["direction"])
            entry_ts = str(selected_trade["entry_timestamp"])
            exit_ts = str(selected_trade["exit_timestamp"])
            entry_price = float(selected_trade["entry_price"])
            exit_price = float(selected_trade["exit_price"])
            bars_held = int(selected_trade["bars_held"])
            pnl_pct = float(selected_trade["pnl_pct"])
            outcome = str(selected_trade["outcome"])

            pnl_text = f"+{pnl_pct}%" if pnl_pct > 0 else f"{pnl_pct}%"
            result_text = "profit" if pnl_pct > 0 else ("loss" if pnl_pct < 0 else "flat result")
            st.info(
                f"Selected example explained: {direction} entry at {entry_ts} ({entry_price}), "
                f"exit at {exit_ts} ({exit_price}), held {bars_held} bars, {result_text} {pnl_text}, outcome: {outcome}."
            )

    fig = build_figure(
        analyzed,
        gray_fake=gray_fake,
        interval=interval,
        hide_market_closed_gaps=hide_market_closed_gaps,
    )
    if show_trade_markers and not example_trades.empty:
        long_entries = example_trades[example_trades["direction"] == "LONG"]
        short_entries = example_trades[example_trades["direction"] == "SHORT"]
        win_exits = example_trades[example_trades["outcome"] == "Win"]
        non_win_exits = example_trades[example_trades["outcome"] != "Win"]

        if not long_entries.empty:
            fig.add_trace(
                go.Scatter(
                    x=long_entries["entry_timestamp"],
                    y=long_entries["entry_price"],
                    mode="markers",
                    name="Example Entry (Long)",
                    marker=dict(color="#1565C0", size=9, symbol="circle"),
                ),
                row=1,
                col=1,
            )
        if not short_entries.empty:
            fig.add_trace(
                go.Scatter(
                    x=short_entries["entry_timestamp"],
                    y=short_entries["entry_price"],
                    mode="markers",
                    name="Example Entry (Short)",
                    marker=dict(color="#EF6C00", size=9, symbol="diamond"),
                ),
                row=1,
                col=1,
            )
        if not win_exits.empty:
            fig.add_trace(
                go.Scatter(
                    x=win_exits["exit_timestamp"],
                    y=win_exits["exit_price"],
                    mode="markers",
                    name="Example Exit (Win)",
                    marker=dict(color="#2E7D32", size=10, symbol="x"),
                ),
                row=1,
                col=1,
            )
        if not non_win_exits.empty:
            fig.add_trace(
                go.Scatter(
                    x=non_win_exits["exit_timestamp"],
                    y=non_win_exits["exit_price"],
                    mode="markers",
                    name="Example Exit (Loss/Flat)",
                    marker=dict(color="#C62828", size=10, symbol="x"),
                ),
                row=1,
                col=1,
            )

    if selected_trade is not None:
        selected_entry_ts = pd.Timestamp(selected_trade["entry_timestamp"])
        selected_exit_ts = pd.Timestamp(selected_trade["exit_timestamp"])
        selected_entry_price = float(selected_trade["entry_price"])
        selected_exit_price = float(selected_trade["exit_price"])
        selected_direction = str(selected_trade["direction"])
        selected_outcome = str(selected_trade["outcome"])

        path_color = "#43A047" if selected_outcome == "Win" else ("#EF6C00" if selected_outcome == "Flat" else "#E53935")
        window_fill = "#BBDEFB" if selected_direction == "LONG" else "#FFE0B2"

        fig.add_vrect(
            x0=selected_entry_ts,
            x1=selected_exit_ts,
            fillcolor=window_fill,
            opacity=0.18,
            line_width=0,
            row=1,
            col=1,
        )
        fig.add_trace(
            go.Scatter(
                x=[selected_entry_ts, selected_exit_ts],
                y=[selected_entry_price, selected_exit_price],
                mode="lines+markers",
                name="Selected Example Path",
                line=dict(color=path_color, width=3, dash="dot"),
                marker=dict(color=path_color, size=11, symbol="star"),
            ),
            row=1,
            col=1,
        )

        if focus_chart_on_selected_example:
            entry_pos = int(analyzed.index.get_indexer([selected_entry_ts], method="nearest")[0])
            exit_pos = int(analyzed.index.get_indexer([selected_exit_ts], method="nearest")[0])
            left_pos = max(0, min(entry_pos, exit_pos) - 4)
            right_pos = min(len(analyzed) - 1, max(entry_pos, exit_pos) + 4)
            fig.update_xaxes(range=[analyzed.index[left_pos], analyzed.index[right_pos]])

    st.plotly_chart(fig, use_container_width=True)

    bt = backtest_signals(analyzed)
    st.subheader("Backtest Snapshot")
    b1, b2, b3, b4 = st.columns(4)
    b1.metric("Signals", int(bt["trades"]))
    b2.metric("Wins", int(bt["wins"]))
    b3.metric("Losses", int(bt["losses"]))
    b4.metric("Win Rate", f"{bt['win_rate']}%")
    st.caption("Method: trend-change signal, scored by next-bar direction. Educational only; not a trading recommendation.")

    st.subheader("Trend Events")
    if events:
        event_df = pd.DataFrame(
            {
                "timestamp": [str(e.timestamp) for e in events[-25:]][::-1],
                "event": [e.event for e in events[-25:]][::-1],
                "trend_after": [e.trend_after for e in events[-25:]][::-1],
            }
        )
        st.dataframe(event_df, use_container_width=True)
    else:
        st.info("No trend start/reversal events detected in the selected data window.")

    st.subheader("Export")
    export_df = df_for_export(analyzed)
    csv_bytes = export_df.to_csv(index=False).encode("utf-8")
    st.download_button(
        "Download classified data (CSV)",
        data=csv_bytes,
        file_name=f"{symbol}_{interval}_classified.csv",
        mime="text/csv",
    )

    try:
        pdf_bytes = fig.to_image(format="pdf")
        st.download_button(
            "Download chart (PDF)",
            data=pdf_bytes,
            file_name=f"{symbol}_{interval}_chart.pdf",
            mime="application/pdf",
        )
    except Exception:
        st.caption("PDF export unavailable. Install `kaleido` and rerun to enable chart PDF downloads.")

    with st.expander("Latest classified bars"):
        st.dataframe(export_df.tail(30), use_container_width=True)


if __name__ == "__main__":
    main()
