# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Metrics dashboard template.

Demonstrates:
- 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.).
"""

import hashlib
from datetime import date, timedelta

import altair as alt
import numpy as np
import pandas as pd

import streamlit as st

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
    """
    seed = int(hashlib.sha256(metric_name.encode()).hexdigest(), 16) % 2**32
    rng = np.random.default_rng(seed)

    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 *= seasonality

    # Add noise
    noise = rng.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

    filtered: pd.DataFrame = df[df[x_col] >= min_date]
    return filtered


def _melt_chart_data(
    df: pd.DataFrame, x_col: str, y_cols: list[str], labels: list[str]
) -> pd.DataFrame:
    """Melt wide data into long form and re-label the series column."""
    melted = df.melt(
        id_vars=[x_col],
        value_vars=y_cols,
        var_name="series",
        value_name="value",
    )
    label_map = dict(zip(y_cols, labels, strict=True))
    melted["series"] = melted["series"].map(label_map)
    return melted


def _chart_tooltip(x_col: str, x_title: str = "Date") -> list[alt.Tooltip]:
    """Standard tooltip for melted chart data."""
    return [
        alt.Tooltip(f"{x_col}:T", title=x_title, format="%Y-%m-%d"),
        alt.Tooltip("series:N", title="Series"),
        alt.Tooltip("value:Q", title="Value", format=",.0f"),
    ]


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."""
    melted = _melt_chart_data(df, x_col, y_cols, labels)

    chart: alt.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=_chart_tooltip(x_col),
        )
        .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 = _melt_chart_data(df, x_col, y_cols, labels)

    chart: alt.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=_chart_tooltip(x_col),
        )
        .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

    agg_df = df.groupby("week")[y_cols].mean().reset_index()
    melted = _melt_chart_data(agg_df, "week", y_cols, labels)

    chart: alt.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=_chart_tooltip("week", x_title="Week"),
        )
        .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 a dashed trend line for the 7-day MA."""
    melted = _melt_chart_data(df, x_col, y_cols, labels)

    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=_chart_tooltip(x_col),
        )
    )

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

    chart: alt.Chart = (points + trend).properties(height=height)
    return chart


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


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

    Parameters
    ----------
    title : str
        Card title.
    df : pd.DataFrame
        DataFrame with ``ds``, ``daily_value``, ``value_7d_ma`` columns.
    key_prefix : str
        Unique prefix for widget keys.
    chart_type : str
        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 or "All")

        # 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,
                height=CHART_HEIGHT,
                hide_index=True,
            )
        elif y_cols:
            st.altair_chart(
                render_chart(filtered_df, "ds", y_cols, labels),
            )
        else:
            st.info("Select at least one line option.")


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


def render_page_header(title: str) -> None:
    """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"
    )
