use axum::{
    extract::{Path as AxumPath, Query, State},
    http::StatusCode,
    response::{IntoResponse, Redirect, Response},
    routing::get,
    Router,
};
use serde::Deserialize;
use tower_cookies::Cookies;
use uuid::Uuid;

use crate::render::{render, render_to_string};
use crate::routes::auth::is_authenticated;
use crate::AppState;

// Milliseconds per day. Used to convert between the date-range query
// parameter (in days) and the millisecond timestamps stored in events.
const DAY_MS: i64 = 24 * 60 * 60 * 1000;
// Default look-back when the request omits ?date_range= and gives a custom
// start/end. Matches what the dashboard's date selector picks by default.
const DEFAULT_DATE_RANGE_DAYS: i64 = 28;

pub fn router() -> Router<AppState> {
    // The dashboard's UUID path segment is a catch-all; merging this module
    // last in app::router keeps named routes (e.g. /login, /properties)
    // winning the match (axum prefers literal segments over path parameters
    // at the same depth).
    Router::new().route("/{property_id}", get(property))
}

#[derive(Debug, Deserialize)]
pub struct DashboardQuery {
    pub date_start: Option<String>,
    pub date_end: Option<String>,
    pub date_range: Option<String>,
    pub filter_url: Option<String>,
    pub report: Option<String>,
}

pub async fn property(
    State(state): State<AppState>,
    AxumPath(property_id): AxumPath<Uuid>,
    cookies: Cookies,
    Query(q): Query<DashboardQuery>,
) -> Response {
    let row: Option<crate::models::PropertyRow> = sqlx::query_as(
        "SELECT id, name, custom_cards, is_protected, is_public, created_at, updated_at \
         FROM properties WHERE id = ?",
    )
    .bind(property_id.as_bytes().to_vec())
    .fetch_optional(&state.pool)
    .await
    .unwrap_or(None);
    let Some(row) = row else {
        return Redirect::to("/properties").into_response();
    };
    let p = row.into_property();
    let authed = is_authenticated(&cookies, &state);
    if !p.is_public && !authed {
        return Redirect::to("/login").into_response();
    }

    use chrono::{Duration, Local};

    let today = Local::now().date_naive();
    let default_start = today - Duration::days(DEFAULT_DATE_RANGE_DAYS);
    let date_start = q
        .date_start
        .clone()
        .unwrap_or_else(|| default_start.format("%Y-%m-%d").to_string());
    let date_end = q
        .date_end
        .clone()
        .unwrap_or_else(|| today.format("%Y-%m-%d").to_string());

    let start_ms = match crate::queries::parse_date_to_ms(&date_start, false) {
        Some(v) => v,
        None => return (StatusCode::BAD_REQUEST, "bad date_start").into_response(),
    };
    let end_ms = match crate::queries::parse_date_to_ms(&date_end, true) {
        Some(v) => v,
        None => return (StatusCode::BAD_REQUEST, "bad date_end").into_response(),
    };

    let date_range: i64 = match q.date_range.as_deref() {
        Some("custom") | None => {
            // Days between start and end, inclusive of the end-of-day window.
            let span = (end_ms - start_ms) / DAY_MS;
            span.max(1)
        }
        Some(other) => other.parse::<i64>().unwrap_or(DEFAULT_DATE_RANGE_DAYS),
    };

    let prev_start_ms = start_ms - date_range * DAY_MS;
    let prev_end_ms = end_ms - date_range * DAY_MS;
    let filter_url = q.filter_url.as_deref().filter(|s| !s.is_empty());

    let dash_value: serde_json::Value = {
        let pool = &state.pool;
        let pid = &p.id;

        let event_cards =
            crate::queries::standard_event_cards(pool, pid, start_ms, end_ms, prev_start_ms, prev_end_ms, filter_url).await;
        let (custom_cards, custom_events) = crate::queries::custom_event_cards(
            pool, pid, &p.custom_cards, start_ms, end_ms, prev_start_ms, prev_end_ms, filter_url,
        )
        .await;
        let mut all_cards = event_cards;
        all_cards.extend(custom_cards);

        let total_events_graph = crate::queries::events_graph(
            pool, pid, start_ms, end_ms, filter_url, today, date_range,
        )
        .await;

        let total_events_by_screen_size =
            crate::queries::events_by_screen_size(pool, pid, start_ms, end_ms, filter_url, 7).await;
        let total_events_by_device =
            crate::queries::events_by_device(pool, pid, start_ms, end_ms, filter_url, 7).await;
        let total_events_by_browser =
            crate::queries::events_by_browser(pool, pid, start_ms, end_ms, filter_url, 7).await;
        let total_events_by_platform =
            crate::queries::events_by_platform(pool, pid, start_ms, end_ms, filter_url, 7).await;
        let total_events_by_page_url =
            crate::queries::events_by_page_url(pool, pid, start_ms, end_ms, filter_url, 10).await;
        let total_page_views_by_page_url =
            crate::queries::page_views_by_page_url(pool, pid, start_ms, end_ms, filter_url, 10).await;
        let total_events_by_custom_event =
            crate::queries::events_by_custom_event(pool, pid, start_ms, end_ms, filter_url, 10).await;
        let total_session_starts_by_referrer =
            crate::queries::session_starts_by_referrer(pool, pid, start_ms, end_ms, filter_url, 10).await;
        let total_page_views_by_utm_medium =
            crate::queries::page_views_by_utm(pool, pid, start_ms, end_ms, filter_url, "medium", 10).await;
        let total_page_views_by_utm_source =
            crate::queries::page_views_by_utm(pool, pid, start_ms, end_ms, filter_url, "source", 10).await;
        let total_page_views_by_utm_campaign =
            crate::queries::page_views_by_utm(pool, pid, start_ms, end_ms, filter_url, "campaign", 10).await;
        let session_starts_by_country =
            crate::queries::session_starts_by_country(pool, pid, start_ms, end_ms, filter_url).await;
        let session_starts_by_country_region =
            crate::queries::session_starts_by_country_region(pool, pid, start_ms, end_ms, filter_url).await;
        let bot_traffic =
            crate::queries::bot_traffic(pool, pid, start_ms, end_ms, 10).await;

        serde_json::json!({
            "event_cards": all_cards,
            "custom_events": custom_events,
            "total_events_graph": total_events_graph,
            "total_events_by_screen_size": total_events_by_screen_size,
            "total_events_by_device": total_events_by_device,
            "total_events_by_browser": total_events_by_browser,
            "total_events_by_platform": total_events_by_platform,
            "total_events_by_page_url": total_events_by_page_url,
            "total_page_views_by_page_url": total_page_views_by_page_url,
            "total_events_by_custom_event": total_events_by_custom_event,
            "total_session_starts_by_referrer": total_session_starts_by_referrer,
            "total_page_views_by_utm_medium": total_page_views_by_utm_medium,
            "total_page_views_by_utm_source": total_page_views_by_utm_source,
            "total_page_views_by_utm_campaign": total_page_views_by_utm_campaign,
            "session_starts_by_country": session_starts_by_country,
            "session_starts_by_country_region": session_starts_by_country_region,
            "bot_traffic": bot_traffic,
        })
    };

    let total_live_users = crate::queries::total_live_users(&state.pool, &p.id).await;

    // Build the small chart helpers + breakdown totals the print template needs.
    let chart_polyline = build_chart_polyline(
        dash_value
            .get("total_events_graph")
            .and_then(|v| v.as_array())
            .map(|v| v.as_slice())
            .unwrap_or(&[]),
    );
    let graph_arr = dash_value
        .get("total_events_graph")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();
    let chart_label_start = graph_arr
        .first()
        .and_then(|p| p.get("label"))
        .and_then(|l| l.as_str())
        .unwrap_or("")
        .to_string();
    let chart_label_end = graph_arr
        .last()
        .and_then(|p| p.get("label"))
        .and_then(|l| l.as_str())
        .unwrap_or("")
        .to_string();
    let (chart_peak_count, chart_peak_label) = graph_arr
        .iter()
        .max_by_key(|p| p.get("count").and_then(|c| c.as_i64()).unwrap_or(0))
        .map(|p| {
            (
                p.get("count").and_then(|c| c.as_i64()).unwrap_or(0),
                p.get("label").and_then(|l| l.as_str()).unwrap_or("").to_string(),
            )
        })
        .unwrap_or((0, String::new()));

    let breakdown_total = |key: &str| -> i64 {
        dash_value
            .get(key)
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|item| item.get("count").and_then(|c| c.as_i64()))
                    .sum::<i64>()
                    .max(1)
            })
            .unwrap_or(1)
    };
    let breakdown_totals = serde_json::json!({
        "device": breakdown_total("total_events_by_device"),
        "browser": breakdown_total("total_events_by_browser"),
        "platform": breakdown_total("total_events_by_platform"),
        "screen_size": breakdown_total("total_events_by_screen_size"),
    });

    let mut top_countries: Vec<serde_json::Value> = dash_value
        .get("session_starts_by_country")
        .and_then(|v| v.as_object())
        .map(|m| {
            m.iter()
                .map(|(k, v)| {
                    serde_json::json!({
                        "label": k,
                        "count": v.as_i64().unwrap_or(0),
                    })
                })
                .collect()
        })
        .unwrap_or_default();
    top_countries.sort_by_key(|v| -v.get("count").and_then(|c| c.as_i64()).unwrap_or(0));
    top_countries.truncate(10);

    let generated_at = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();

    let extra = minijinja::context! {
        page => minijinja::context! {
            title => &p.name,
            description => format!("Analytics for {}", p.name),
        },
        property => minijinja::context! {
            id => p.id.to_string(),
            name => &p.name,
            is_protected => p.is_protected,
            is_public => p.is_public,
        },
        date_start => &date_start,
        date_end => &date_end,
        date_range => date_range,
        filter_url => filter_url,
        total_live_users => total_live_users,
        event_cards => dash_value.get("event_cards").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        custom_events => dash_value.get("custom_events").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_graph => dash_value.get("total_events_graph").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_screen_size => dash_value.get("total_events_by_screen_size").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_device => dash_value.get("total_events_by_device").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_browser => dash_value.get("total_events_by_browser").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_platform => dash_value.get("total_events_by_platform").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_page_url => dash_value.get("total_events_by_page_url").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_page_views_by_page_url => dash_value.get("total_page_views_by_page_url").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_events_by_custom_event => dash_value.get("total_events_by_custom_event").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_session_starts_by_referrer => dash_value.get("total_session_starts_by_referrer").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_page_views_by_utm_medium => dash_value.get("total_page_views_by_utm_medium").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_page_views_by_utm_source => dash_value.get("total_page_views_by_utm_source").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        total_page_views_by_utm_campaign => dash_value.get("total_page_views_by_utm_campaign").cloned().unwrap_or(serde_json::Value::Array(vec![])),
        session_starts_by_country => dash_value.get("session_starts_by_country").cloned().unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
        session_starts_by_country_region => dash_value.get("session_starts_by_country_region").cloned().unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
        bot_traffic => dash_value.get("bot_traffic").cloned().unwrap_or(serde_json::json!({"total": 0, "top_bots": [], "top_pages": []})),
        chart_polyline => &chart_polyline,
        chart_label_start => &chart_label_start,
        chart_label_end => &chart_label_end,
        chart_peak_count => chart_peak_count,
        chart_peak_label => &chart_peak_label,
        breakdown_totals => &breakdown_totals,
        top_countries => &top_countries,
        generated_at => &generated_at,
    };

    // Report exports.
    if let Some(fmt) = q.report.as_deref() {
        let fmt = if fmt.is_empty() { "pdf" } else { fmt };
        let path = format!("/{property_id}");
        if fmt == "md" {
            let body = match render_to_string(
                &state,
                "properties/property_report.md",
                &path,
                authed,
                extra,
            ) {
                Ok(b) => b,
                Err(resp) => return resp,
            };
            let mut h = axum::http::HeaderMap::new();
            h.insert(
                axum::http::header::CONTENT_TYPE,
                "text/markdown; charset=utf-8".parse().unwrap(),
            );
            h.insert(
                axum::http::header::CONTENT_DISPOSITION,
                format!("inline; filename=\"{}.md\"", p.name).parse().unwrap(),
            );
            return (StatusCode::OK, h, body).into_response();
        }
        if fmt == "pdf" {
            let typst_source = match render_to_string(
                &state,
                "properties/property_report.typ",
                &path,
                authed,
                extra,
            ) {
                Ok(b) => b,
                Err(resp) => return resp,
            };
            let renderer = state.pdf_renderer.clone();
            let pdf_res =
                tokio::task::spawn_blocking(move || renderer.render(typst_source)).await;
            match pdf_res {
                Ok(Ok(bytes)) => {
                    let mut h = axum::http::HeaderMap::new();
                    h.insert(axum::http::header::CONTENT_TYPE, "application/pdf".parse().unwrap());
                    h.insert(
                        axum::http::header::CONTENT_DISPOSITION,
                        format!("inline; filename=\"{}.pdf\"", p.name).parse().unwrap(),
                    );
                    return (StatusCode::OK, h, bytes).into_response();
                }
                Ok(Err(e)) => {
                    tracing::error!("pdf render: {e}");
                    return (StatusCode::INTERNAL_SERVER_ERROR, "pdf error").into_response();
                }
                Err(e) => {
                    tracing::error!("pdf join: {e}");
                    return (StatusCode::INTERNAL_SERVER_ERROR, "pdf error").into_response();
                }
            }
        }
    }

    render(
        &state,
        "properties/property.html",
        &format!("/{property_id}"),
        authed,
        extra,
    )
}

/// Toner-friendly SVG polyline points for the print template. `width`,
/// `height`, and `padding` match the SVG viewBox in
/// templates/properties/property_print.html — change one and change the other.
fn build_chart_polyline(points: &[serde_json::Value]) -> String {
    if points.is_empty() {
        return String::new();
    }
    let counts: Vec<i64> = points
        .iter()
        .map(|p| p.get("count").and_then(|c| c.as_i64()).unwrap_or(0))
        .collect();
    let max = *counts.iter().max().unwrap_or(&1);
    let max = if max == 0 { 1 } else { max };
    let n = counts.len();
    let width = 600.0_f64;
    let height = 100.0_f64;
    let padding = 4.0_f64;
    let usable_h = height - 2.0 * padding;
    if n == 1 {
        let x = width / 2.0;
        let y = height - padding - (counts[0] as f64 / max as f64) * usable_h;
        return format!("{x:.1},{y:.1}");
    }
    counts
        .iter()
        .enumerate()
        .map(|(i, c)| {
            let x = (i as f64 / (n - 1) as f64) * width;
            let y = height - padding - (*c as f64 / max as f64) * usable_h;
            format!("{x:.1},{y:.1}")
        })
        .collect::<Vec<_>>()
        .join(" ")
}
