use askama::Template;
use askama_web::WebTemplate;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::response::IntoResponse;
use k8s_openapi::api::apps::v1::Deployment;
use k8s_openapi::api::core::v1::Secret;
use kube::{Api, api::ListParams};

use crate::auth_guard::AuthUser;
use crate::flash;
use crate::k8s::SojuBouncer;
use crate::page::PageContext;
use crate::state::AppState;

#[derive(Template, WebTemplate)]
#[template(path = "dashboard.html")]
pub struct DashboardTemplate {
    pub page: PageContext,
    pub has_bouncer: bool,
    pub is_pro: bool,
    pub plan: String,
    pub networks: i64,
    pub channels: i64,
    pub messages_relayed: i64,
    pub bouncer_uptime: String,
    pub paste_count: i64,
    pub paste_storage: String,
    pub pic_count: i64,
    pub pic_storage: String,
}

#[derive(serde::Deserialize)]
struct TxtUsage {
    paste_count: i64,
    storage_bytes: i64,
}

#[derive(serde::Deserialize)]
struct PicsUsage {
    image_count: i64,
    storage_bytes: i64,
}

fn format_bytes(bytes: i64) -> String {
    if bytes < 1024 {
        format!("{bytes} B")
    } else if bytes < 1024 * 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else if bytes < 1024 * 1024 * 1024 {
        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
    } else {
        format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
    }
}

fn format_uptime(start: chrono::DateTime<chrono::Utc>) -> String {
    let dur = chrono::Utc::now() - start;
    let days = dur.num_days();
    let hours = dur.num_hours() % 24;
    if days > 0 {
        format!("{days}d {hours}h")
    } else {
        format!("{hours}h")
    }
}

pub async fn index(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
) -> impl IntoResponse {
    let bouncers: Api<SojuBouncer> = Api::namespaced(state.kube.clone(), &state.namespace);
    let lp = ListParams::default().labels(&format!("irc.now/owner={}", user.sub));
    let bouncer_list = bouncers.list(&lp).await.unwrap_or_default();
    let has_bouncer = !bouncer_list.items.is_empty();

    let plan = user.plan.unwrap_or_else(|| "free".to_string());
    let is_pro = plan == "pro";

    let (mut networks, mut channels, mut messages_relayed) = (0i64, 0i64, 0i64);
    let mut bouncer_uptime = String::new();

    if let Some(bouncer) = bouncer_list.items.first() {
        let bouncer_name = bouncer.metadata.name.as_deref().unwrap_or_default();

        let deploy_api: Api<Deployment> = Api::namespaced(state.kube.clone(), &state.namespace);
        if let Ok(deploy) = deploy_api.get(bouncer_name).await {
            if let Some(ts) = deploy.metadata.creation_timestamp.as_ref() {
                bouncer_uptime = format_uptime(ts.0);
            }
        }

        if let Ok(stats) = get_bouncer_stats(&state, bouncer_name).await {
            networks = stats.0;
            channels = stats.1;
            messages_relayed = stats.2;
        }
    }

    let txt_usage = async {
        let resp = state.http_client
            .get(format!("{}/api/usage/{}", state.txt_url, user.sub))
            .send().await.ok()?;
        resp.json::<TxtUsage>().await.ok()
    }.await;
    let (paste_count, paste_storage_bytes) = txt_usage
        .map(|u| (u.paste_count, u.storage_bytes))
        .unwrap_or((0, 0));

    let pics_usage = async {
        let resp = state.http_client
            .get(format!("{}/api/usage/{}", state.pics_url, user.sub))
            .send().await.ok()?;
        resp.json::<PicsUsage>().await.ok()
    }.await;
    let (pic_count, pic_storage_bytes) = pics_usage
        .map(|u| (u.image_count, u.storage_bytes))
        .unwrap_or((0, 0));

    let page = PageContext::from_request(&headers, &state.announcement);
    let clear = page.flash.is_some();

    let template = DashboardTemplate {
        page,
        has_bouncer,
        is_pro,
        plan,
        networks,
        channels,
        messages_relayed,
        bouncer_uptime,
        paste_count,
        paste_storage: format_bytes(paste_storage_bytes),
        pic_count,
        pic_storage: format_bytes(pic_storage_bytes),
    };

    if clear {
        (axum::response::AppendHeaders([flash::clear_flash()]), template).into_response()
    } else {
        template.into_response()
    }
}

async fn get_bouncer_stats(
    state: &AppState,
    bouncer_name: &str,
) -> Result<(i64, i64, i64), Box<dyn std::error::Error>> {
    let secret_api: Api<Secret> = Api::namespaced(state.kube.clone(), &state.namespace);
    let secret = secret_api.get(&format!("{bouncer_name}-db")).await?;

    let uri = secret.data.as_ref()
        .and_then(|d| d.get("uri"))
        .map(|b| String::from_utf8_lossy(&b.0).to_string())
        .ok_or("missing uri in db secret")?;

    let (client, connection) = tokio_postgres::connect(&uri, tokio_postgres::NoTls).await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            tracing::warn!("soju db connection error: {e}");
        }
    });

    let networks: i64 = client
        .query_one(r#"SELECT COUNT(*) FROM "Network""#, &[])
        .await?
        .get(0);

    let channels: i64 = client
        .query_one(r#"SELECT COUNT(*) FROM "Channel""#, &[])
        .await?
        .get(0);

    let messages: i64 = client
        .query_one(r#"SELECT COUNT(*) FROM "MessageTarget""#, &[])
        .await
        .map(|r| r.get(0))
        .unwrap_or(0);

    Ok((networks, channels, messages))
}
