"""
Metrics Dashboard Template

A comprehensive metrics dashboard demonstrating:
- Time series visualization with Altair (line, area, bar, point charts)
- Metric cards with chart/table toggle and popover filters
- Time range filtering (1M, 6M, 1Y, QTD, YTD, All)
- Line options (Daily, 7-day MA)

This template uses synthetic data. Replace the generate_*_data() functions
with your own data sources (e.g., Snowflake queries, APIs, etc.)
"""

from datetime import date, timedelta
import numpy as np
import pandas as pd
import streamlit as st
import altair as alt

st.set_page_config(
    page_title="Metrics Dashboard",
    page_icon=":material/monitoring:",
    layout="wide",
)


# =============================================================================
# Constants
# =============================================================================

TIME_RANGES = ["1M", "6M", "1Y", "QTD", "YTD", "All"]
CHART_HEIGHT = 300


# =============================================================================
# Synthetic Data Generation (Replace with your data source)
# =============================================================================


def generate_metric_data(
    metric_name: str,
    start_date: date,
    end_date: date,
    base_value: float = 1000,
    growth_rate: float = 0.001,
    noise_factor: float = 0.1,
) -> pd.DataFrame:
    """Generate synthetic time series data for a metric.
    
    Replace this function with your actual data source, e.g.:
    - Snowflake query
    - API call
    - Database query
    """
    np.random.seed(hash(metric_name) % 2**32)
    
    dates = pd.date_range(start=start_date, end=end_date, freq="D")
    n_days = len(dates)
    
    # Base trend with growth
    trend = base_value * (1 + growth_rate) ** np.arange(n_days)
    
    # Add weekly seasonality (lower on weekends)
    day_of_week = dates.dayofweek
    seasonality = np.where(day_of_week >= 5, 0.7, 1.0)
    trend = trend * seasonality
    
    # Add noise
    noise = np.random.normal(1, noise_factor, n_days)
    values = trend * noise
    
    # Calculate rolling averages
    df = pd.DataFrame({
        "ds": dates,
        "daily_value": values,
    })
    df["value_7d_ma"] = df["daily_value"].rolling(7, min_periods=1).mean()
    
    return df


@st.cache_data(ttl=3600)
def load_all_metrics() -> dict[str, pd.DataFrame]:
    """Load all metrics data. Replace with your data loading logic."""
    end_date = date.today() - timedelta(days=1)
    start_date = end_date - timedelta(days=730)  # 2 years of data
    
    return {
        "users": generate_metric_data("users", start_date, end_date, base_value=5000, growth_rate=0.002),
        "sessions": generate_metric_data("sessions", start_date, end_date, base_value=15000, growth_rate=0.003),
        "revenue": generate_metric_data("revenue", start_date, end_date, base_value=50000, growth_rate=0.001),
        "conversions": generate_metric_data("conversions", start_date, end_date, base_value=500, growth_rate=0.0015),
    }


# =============================================================================
# Chart Utilities
# =============================================================================


def filter_by_time_range(df: pd.DataFrame, x_col: str, time_range: str) -> pd.DataFrame:
    """Filter dataframe by time range."""
    if time_range == "All" or df.empty:
        return df

    df = df.copy()
    df[x_col] = pd.to_datetime(df[x_col])
    max_date = df[x_col].max()

    if time_range == "1M":
        min_date = max_date - timedelta(days=30)
    elif time_range == "6M":
        min_date = max_date - timedelta(days=180)
    elif time_range == "1Y":
        min_date = max_date - timedelta(days=365)
    elif time_range == "QTD":
        quarter_month = ((max_date.month - 1) // 3) * 3 + 1
        min_date = pd.Timestamp(date(max_date.year, quarter_month, 1))
    elif time_range == "YTD":
        min_date = pd.Timestamp(date(max_date.year, 1, 1))
    else:
        return df

    return df[df[x_col] >= min_date]


def render_line_chart(
    df: pd.DataFrame,
    x_col: str,
    y_cols: list[str],
    labels: list[str],
    height: int = CHART_HEIGHT,
) -> alt.Chart:
    """Render a multi-line chart."""
    # Melt for Altair
    melted = df.melt(
        id_vars=[x_col],
        value_vars=y_cols,
        var_name="series",
        value_name="value",
    )
    
    # Map to labels
    label_map = dict(zip(y_cols, labels))
    melted["series"] = melted["series"].map(label_map)
    
    chart = (
        alt.Chart(melted)
        .mark_line()
        .encode(
            x=alt.X(f"{x_col}:T", title=None),
            y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
            color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
            strokeDash=alt.condition(
                alt.datum.series == "7-day MA",
                alt.value([5, 5]),
                alt.value([0]),
            ),
            tooltip=[
                alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
                alt.Tooltip("series:N", title="Series"),
                alt.Tooltip("value:Q", title="Value", format=",.0f"),
            ],
        )
        .properties(height=height)
    )
    
    return chart


def render_area_chart(
    df: pd.DataFrame,
    x_col: str,
    y_cols: list[str],
    labels: list[str],
    height: int = CHART_HEIGHT,
) -> alt.Chart:
    """Render a stacked area chart."""
    melted = df.melt(
        id_vars=[x_col],
        value_vars=y_cols,
        var_name="series",
        value_name="value",
    )
    label_map = dict(zip(y_cols, labels))
    melted["series"] = melted["series"].map(label_map)

    chart = (
        alt.Chart(melted)
        .mark_area(opacity=0.6, line=True)
        .encode(
            x=alt.X(f"{x_col}:T", title=None),
            y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
            color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
            tooltip=[
                alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
                alt.Tooltip("series:N", title="Series"),
                alt.Tooltip("value:Q", title="Value", format=",.0f"),
            ],
        )
        .properties(height=height)
    )
    return chart


def render_bar_chart(
    df: pd.DataFrame,
    x_col: str,
    y_cols: list[str],
    labels: list[str],
    height: int = CHART_HEIGHT,
) -> alt.Chart:
    """Render a bar chart (weekly aggregation for readability)."""
    df = df.copy()
    df[x_col] = pd.to_datetime(df[x_col])
    df["week"] = df[x_col].dt.to_period("W").dt.start_time

    # Aggregate by week
    agg_df = df.groupby("week")[y_cols].mean().reset_index()

    melted = agg_df.melt(
        id_vars=["week"],
        value_vars=y_cols,
        var_name="series",
        value_name="value",
    )
    label_map = dict(zip(y_cols, labels))
    melted["series"] = melted["series"].map(label_map)

    chart = (
        alt.Chart(melted)
        .mark_bar(opacity=0.8)
        .encode(
            x=alt.X("week:T", title=None),
            y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
            color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
            xOffset="series:N",
            tooltip=[
                alt.Tooltip("week:T", title="Week", format="%Y-%m-%d"),
                alt.Tooltip("series:N", title="Series"),
                alt.Tooltip("value:Q", title="Value", format=",.0f"),
            ],
        )
        .properties(height=height)
    )
    return chart


def render_point_chart(
    df: pd.DataFrame,
    x_col: str,
    y_cols: list[str],
    labels: list[str],
    height: int = CHART_HEIGHT,
) -> alt.Chart:
    """Render a scatter/point chart with trend line."""
    melted = df.melt(
        id_vars=[x_col],
        value_vars=y_cols,
        var_name="series",
        value_name="value",
    )
    label_map = dict(zip(y_cols, labels))
    melted["series"] = melted["series"].map(label_map)

    points = (
        alt.Chart(melted)
        .mark_point(opacity=0.5, size=20)
        .encode(
            x=alt.X(f"{x_col}:T", title=None),
            y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
            color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
            tooltip=[
                alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
                alt.Tooltip("series:N", title="Series"),
                alt.Tooltip("value:Q", title="Value", format=",.0f"),
            ],
        )
    )

    # Add trend line for 7-day MA only
    trend = (
        alt.Chart(melted[melted["series"] == "7-day MA"])
        .mark_line(strokeDash=[5, 5], strokeWidth=2)
        .encode(
            x=alt.X(f"{x_col}:T"),
            y=alt.Y("value:Q"),
            color=alt.Color("series:N"),
        )
    )

    return (points + trend).properties(height=height)


# =============================================================================
# Metric Card Component
# =============================================================================


def metric_card(
    title: str,
    df: pd.DataFrame,
    key_prefix: str,
    chart_type: str = "line",
):
    """Display a metric card with chart/table toggle and popover filters.

    Args:
        title: Card title
        df: DataFrame with ds, daily_value, value_7d_ma columns
        key_prefix: Unique prefix for widget keys
        chart_type: One of "line", "area", "bar", "point"
    """
    chart_renderers = {
        "line": render_line_chart,
        "area": render_area_chart,
        "bar": render_bar_chart,
        "point": render_point_chart,
    }
    render_chart = chart_renderers.get(chart_type, render_line_chart)

    with st.container(border=True):
        # Header row with title, view toggle, and filters
        with st.container(
            horizontal=True,
            horizontal_alignment="distribute",
            vertical_alignment="center",
        ):
            st.markdown(f"**{title}**")
            
            view_mode = st.segmented_control(
                "View",
                options=[":material/show_chart:", ":material/table:"],
                default=":material/show_chart:",
                key=f"{key_prefix}_view",
                label_visibility="collapsed",
            )
            
            with st.popover("Filters", type="tertiary"):
                line_options = st.pills(
                    "Lines",
                    options=["Daily", "7-day MA"],
                    default=["Daily", "7-day MA"],
                    selection_mode="multi",
                    key=f"{key_prefix}_lines",
                )
                time_range = st.segmented_control(
                    "Time range",
                    options=TIME_RANGES,
                    default="All",
                    key=f"{key_prefix}_time",
                )
        
        # Apply filters
        line_options = line_options or ["7-day MA"]
        filtered_df = filter_by_time_range(df, "ds", time_range)
        
        # Determine which columns to show
        y_cols = []
        labels = []
        if "Daily" in line_options:
            y_cols.append("daily_value")
            labels.append("Daily")
        if "7-day MA" in line_options:
            y_cols.append("value_7d_ma")
            labels.append("7-day MA")
        
        # Render view
        if "table" in (view_mode or ""):
            st.dataframe(
                filtered_df,
                use_container_width=True,
                height=CHART_HEIGHT,
                hide_index=True,
            )
        else:
            if y_cols:
                st.altair_chart(
                    render_chart(filtered_df, "ds", y_cols, labels),
                    use_container_width=True,
                )
            else:
                st.info("Select at least one line option.")


# =============================================================================
# Page Header Component
# =============================================================================


def render_page_header(title: str):
    """Render page header with title and reset button."""
    with st.container(
        horizontal=True, horizontal_alignment="distribute", vertical_alignment="center"
    ):
        st.markdown(title)
        if st.button(":material/restart_alt: Reset", type="tertiary"):
            st.session_state.clear()
            st.rerun()


# =============================================================================
# Page Layout
# =============================================================================

# Load data (cached)
metrics_data = load_all_metrics()

# Page header
render_page_header("# :material/monitoring: Metrics Dashboard")

# Row 1: Users and Sessions
row1 = st.columns(2)

with row1[0]:
    metric_card("Active Users", metrics_data["users"], "users", chart_type="line")

with row1[1]:
    metric_card("Sessions", metrics_data["sessions"], "sessions", chart_type="area")

# Row 2: Revenue and Conversions
row2 = st.columns(2)

with row2[0]:
    metric_card("Revenue", metrics_data["revenue"], "revenue", chart_type="bar")

with row2[1]:
    metric_card("Conversions", metrics_data["conversions"], "conversions", chart_type="point")
