from __future__ import annotations

import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from .constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL


def _is_intraday_interval(interval: str) -> bool:
    return interval in {"1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h"}


def _is_daily_interval(interval: str) -> bool:
    return interval == "1d"


def _intraday_day_ticks(index: pd.DatetimeIndex) -> tuple[list[pd.Timestamp], list[str]]:
    if len(index) == 0:
        return [], []

    normalized = index.normalize()
    first_mask = ~normalized.duplicated()
    tickvals = [index[pos] for pos, keep in enumerate(first_mask) if bool(keep)]
    ticktext = [f"{ts.month}/{ts.day}" for ts in tickvals]
    return tickvals, ticktext


def _missing_calendar_day_values(df: pd.DataFrame) -> list[str]:
    if df.empty:
        return []

    index = pd.DatetimeIndex(df.index)
    session_days = pd.DatetimeIndex(index.normalize().unique()).sort_values()
    if len(session_days) < 2:
        return []

    if session_days.tz is None:
        all_days = pd.date_range(start=session_days[0], end=session_days[-1], freq="D")
    else:
        all_days = pd.date_range(start=session_days[0], end=session_days[-1], freq="D", tz=session_days.tz)

    missing_days = all_days.difference(session_days)
    # Weekend gaps are already handled by sat->mon bounds; keep explicit values
    # for weekday closures (e.g., exchange holidays) to avoid overlap artifacts.
    weekday_missing = [day for day in missing_days if day.dayofweek < 5]
    return [day.strftime("%Y-%m-%d") for day in weekday_missing]


def build_figure(
    df: pd.DataFrame,
    gray_fake: bool,
    *,
    interval: str,
    hide_market_closed_gaps: bool,
) -> go.Figure:
    fig = make_subplots(
        rows=2,
        cols=1,
        row_heights=[0.8, 0.2],
        vertical_spacing=0.03,
        shared_xaxes=True,
    )

    bull_mask = df["classification"] == "real_bull"
    bear_mask = df["classification"] == "real_bear"

    if gray_fake:
        fig.add_trace(
            go.Candlestick(
                x=df.index,
                open=df["Open"],
                high=df["High"],
                low=df["Low"],
                close=df["Close"],
                name="All Bars",
                increasing_line_color="#B0B0B0",
                decreasing_line_color="#808080",
                opacity=0.35,
            ),
            row=1,
            col=1,
        )
    else:
        fig.add_trace(
            go.Candlestick(
                x=df.index,
                open=df["Open"],
                high=df["High"],
                low=df["Low"],
                close=df["Close"],
                name="All Bars",
                increasing_line_color="#2E8B57",
                decreasing_line_color="#B22222",
                opacity=0.6,
            ),
            row=1,
            col=1,
        )

    fig.add_trace(
        go.Scatter(
            x=df.index[bull_mask],
            y=df.loc[bull_mask, "Close"],
            mode="markers",
            name="Real Bullish",
            marker=dict(color="#00C853", size=9, symbol="triangle-up"),
        ),
        row=1,
        col=1,
    )

    fig.add_trace(
        go.Scatter(
            x=df.index[bear_mask],
            y=df.loc[bear_mask, "Close"],
            mode="markers",
            name="Real Bearish",
            marker=dict(color="#D50000", size=9, symbol="triangle-down"),
        ),
        row=1,
        col=1,
    )

    trend_color = df["trend_state"].map(
        {
            TREND_BULL: "#00C853",
            TREND_BEAR: "#D50000",
            TREND_NEUTRAL: "#9E9E9E",
        }
    )
    fig.add_trace(
        go.Bar(
            x=df.index,
            y=df["Volume"],
            marker_color=trend_color,
            name="Volume",
            opacity=0.65,
        ),
        row=2,
        col=1,
    )

    fig.update_layout(
        template="plotly_white",
        xaxis_rangeslider_visible=False,
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
        margin=dict(l=20, r=20, t=60, b=20),
        height=760,
    )
    if hide_market_closed_gaps:
        if _is_intraday_interval(interval):
            # Intraday rangebreak combinations can produce axis rendering artifacts
            # with some feeds/timezones. Categorical axis keeps chronological bars
            # contiguous and removes closed-session gaps reliably.
            tickvals, ticktext = _intraday_day_ticks(pd.DatetimeIndex(df.index))
            fig.update_xaxes(
                type="category",
                categoryorder="array",
                categoryarray=list(df.index),
                tickmode="array",
                tickvals=tickvals,
                ticktext=ticktext,
                tickangle=0,
            )
        elif _is_daily_interval(interval):
            rangebreaks: list[dict[str, object]] = [dict(bounds=["sat", "mon"])]
            missing_days = _missing_calendar_day_values(df)
            if missing_days:
                rangebreaks.append(dict(values=missing_days))
            fig.update_xaxes(rangebreaks=rangebreaks)

    fig.update_yaxes(title_text="Price", row=1, col=1)
    fig.update_yaxes(title_text="Volume", row=2, col=1)
    return fig
