use askama::Template;
use askama_web::WebTemplate;
use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::response::{AppendHeaders, IntoResponse, Redirect, Response};
use axum::Form;
use k8s_openapi::api::core::v1::Secret;
use kube::api::{DeleteParams, ListParams, PostParams};
use kube::Api;
use serde::Deserialize;

use crate::auth_guard::AuthUser;
use crate::flash;
use crate::htmx;
use crate::k8s::{SojuBouncer, sanitize_bouncer_name, default_bouncer_spec};
use crate::page::PageContext;
use crate::state::AppState;

#[derive(Template, WebTemplate)]
#[template(path = "bouncers.html")]
pub struct BouncersTemplate {
    pub page: PageContext,
    pub bouncers: Vec<BouncerInfo>,
}

pub struct BouncerInfo {
    pub name: String,
    pub hostname: String,
    pub ready: bool,
}

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

    let bouncers = api
        .list(&lp)
        .await
        .map(|list| {
            list.items
                .into_iter()
                .map(|b| {
                    let ready = b.status.as_ref()
                        .and_then(|s| s.conditions.first())
                        .map(|c| c.status == "True")
                        .unwrap_or(false);
                    BouncerInfo {
                        name: b.metadata.name.unwrap_or_default(),
                        hostname: b.spec.hostname,
                        ready,
                    }
                })
                .collect()
        })
        .unwrap_or_default();

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

    let template = BouncersTemplate { page, bouncers };

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

#[derive(Deserialize)]
pub struct CreateForm {
    pub name: String,
    pub migrate: Option<String>,
}

#[derive(Template)]
#[template(path = "partials/bouncer_creating.html")]
struct BouncerCreatingFragment {
    name: String,
}

#[derive(Template)]
#[template(path = "partials/flash_fragment.html")]
struct FlashFragment {
    css_class: &'static str,
    text: &'static str,
}

pub async fn create(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
    Form(form): Form<CreateForm>,
) -> Response {
    if !user.is_pro() {
        if htmx::is_htmx(&headers) {
            return axum::http::Response::builder()
                .status(403)
                .body(axum::body::Body::from(
                    FlashFragment { css_class: "flash-error", text: "upgrade to pro to create a dedicated bouncer" }
                        .render().unwrap_or_default()
                ))
                .unwrap()
                .into_response();
        }
        return axum::http::StatusCode::FORBIDDEN.into_response();
    }

    let name = sanitize_bouncer_name(&form.name);
    if name.is_empty() {
        if htmx::is_htmx(&headers) {
            return axum::http::Response::builder()
                .status(200)
                .body(axum::body::Body::from(
                    FlashFragment { css_class: "flash-error", text: "failed to create bouncer." }
                        .render().unwrap_or_default()
                ))
                .unwrap()
                .into_response();
        }
        return (AppendHeaders([flash::set_flash("bouncer_create_failed")]), Redirect::to("/bouncers")).into_response();
    }

    let user_sub = user.sub.clone();

    let bouncer = SojuBouncer::new(&name, default_bouncer_spec(&name, &user.sub, &state.oidc.issuer_url));

    let mut bouncer = bouncer;
    bouncer
        .metadata
        .labels
        .get_or_insert_with(Default::default)
        .insert(
            "irc.now/owner".to_string(),
            user.sub,
        );

    let api: Api<SojuBouncer> = Api::namespaced(state.kube.clone(), &state.namespace);
    if let Err(e) = api.create(&PostParams::default(), &bouncer).await {
        let flash_code = match &e {
            kube::Error::Api(status) if status.is_already_exists() => "bouncer_name_taken",
            _ => {
                tracing::error!("failed to create bouncer: {e}");
                "bouncer_create_failed"
            }
        };

        if htmx::is_htmx(&headers) {
            let msg = flash::resolve(flash_code).unwrap();
            return axum::http::Response::builder()
                .status(200)
                .body(axum::body::Body::from(
                    FlashFragment { css_class: msg.css_class(), text: msg.text }
                        .render().unwrap_or_default()
                ))
                .unwrap()
                .into_response();
        }
        return (AppendHeaders([flash::set_flash(flash_code)]), Redirect::to("/bouncers")).into_response();
    }

    crate::events::record(
        &state.db, &user_sub, "bouncer_create",
        Some(serde_json::json!({"bouncer": name})),
    ).await;

    // TODO: if form.migrate == Some("true".into()), run soju_operator::migrate::migrate_user
    // to copy user data from soju-shared tenant DB to the new bouncer's tenant DB.
    // Requires connecting to both DBs via tokio_postgres using URIs built from
    // db::build_tenant_uri with credentials from the respective K8s secrets.

    if htmx::is_htmx(&headers) {
        let fragment = BouncerCreatingFragment { name };
        axum::http::Response::builder()
            .status(200)
            .body(axum::body::Body::from(fragment.render().unwrap_or_default()))
            .unwrap()
            .into_response()
    } else {
        (AppendHeaders([flash::set_flash("bouncer_created")]), Redirect::to("/bouncers")).into_response()
    }
}

#[derive(Template)]
#[template(path = "partials/bouncer_status_fragment.html")]
struct BouncerStatusFragment {
    ready: bool,
}

pub async fn status(
    State(state): State<AppState>,
    AuthUser(user): AuthUser,
    Path(name): Path<String>,
) -> Response {
    let api: Api<SojuBouncer> = Api::namespaced(state.kube.clone(), &state.namespace);

    let bouncer = match api.get(&name).await {
        Ok(b) => b,
        Err(_) => return axum::http::StatusCode::NOT_FOUND.into_response(),
    };

    let owner = bouncer.metadata.labels.as_ref()
        .and_then(|l| l.get("irc.now/owner"))
        .map(|s| s.as_str());
    if owner != Some(&user.sub) {
        return axum::http::StatusCode::NOT_FOUND.into_response();
    }

    let ready = bouncer.status.as_ref()
        .and_then(|s| s.conditions.first())
        .map(|c| c.status == "True")
        .unwrap_or(false);

    let fragment = BouncerStatusFragment { ready };
    let html = fragment.render().unwrap_or_default();

    if ready {
        ([("hx-trigger", "bouncer-ready")], axum::response::Html(html)).into_response()
    } else {
        axum::response::Html(html).into_response()
    }
}

pub struct NetworkInfo {
    pub name: String,
    pub addr: String,
    pub nick: String,
    pub enabled: bool,
}

#[derive(Template, WebTemplate)]
#[template(path = "bouncer_detail.html")]
pub struct BouncerDetailTemplate {
    pub page: PageContext,
    pub name: String,
    pub hostname: String,
    pub ready: bool,
    pub status_message: String,
    pub username: String,
    pub networks: Vec<NetworkInfo>,
}

pub async fn detail(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
    Path(name): Path<String>,
) -> Result<impl IntoResponse, impl IntoResponse> {
    let api: Api<SojuBouncer> = Api::namespaced(state.kube.clone(), &state.namespace);

    let bouncer = api.get(&name).await.map_err(|e| {
        tracing::error!("failed to get bouncer {name}: {e}");
        (AppendHeaders([flash::set_flash("bouncer_not_found")]), Redirect::to("/bouncers"))
    })?;

    let owner = bouncer.metadata.labels.as_ref()
        .and_then(|l| l.get("irc.now/owner"))
        .map(|s| s.as_str());
    if owner != Some(&user.sub) {
        return Err((AppendHeaders([flash::set_flash("bouncer_not_found")]), Redirect::to("/bouncers")));
    }

    let (ready, status_message) = bouncer.status.as_ref()
        .and_then(|s| s.conditions.first())
        .map(|c| (
            c.status == "True",
            c.message.clone().unwrap_or_default(),
        ))
        .unwrap_or((false, "pending".to_string()));

    let (username, networks) = match get_bouncer_details(&state, &name).await {
        Ok((u, n)) => (u, n),
        Err(e) => {
            tracing::warn!("failed to read soju db for {name}: {e}");
            (String::new(), vec![])
        }
    };

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

    let template = BouncerDetailTemplate {
        page,
        name,
        hostname: bouncer.spec.hostname,
        ready,
        status_message,
        username,
        networks,
    };

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

async fn get_bouncer_details(
    state: &AppState,
    bouncer_name: &str,
) -> Result<(String, Vec<NetworkInfo>), 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 username: String = client
        .query_one(r#"SELECT username FROM "User" LIMIT 1"#, &[])
        .await?
        .get(0);

    let rows = client
        .query(
            r#"SELECT COALESCE(name, '') as name, addr, COALESCE(nick, '') as nick, enabled FROM "Network" ORDER BY id"#,
            &[],
        )
        .await?;

    let networks = rows.iter().map(|r| NetworkInfo {
        name: r.get(0),
        addr: r.get(1),
        nick: r.get(2),
        enabled: r.get(3),
    }).collect();

    Ok((username, networks))
}

pub async fn delete(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
    Path(name): Path<String>,
) -> Response {
    let api: Api<SojuBouncer> = Api::namespaced(state.kube.clone(), &state.namespace);

    match api.get(&name).await {
        Ok(bouncer) => {
            let owner = bouncer.metadata.labels.as_ref()
                .and_then(|l| l.get("irc.now/owner"))
                .map(|s| s.as_str());
            if owner != Some(&user.sub) {
                if htmx::is_htmx(&headers) {
                    return ([htmx::hx_redirect("/bouncers")], "").into_response();
                }
                return (AppendHeaders([flash::set_flash("bouncer_not_found")]), Redirect::to("/bouncers")).into_response();
            }
        }
        Err(_) => {
            if htmx::is_htmx(&headers) {
                return ([htmx::hx_redirect("/bouncers")], "").into_response();
            }
            return (AppendHeaders([flash::set_flash("bouncer_not_found")]), Redirect::to("/bouncers")).into_response();
        }
    }

    match api.delete(&name, &DeleteParams::default()).await {
        Ok(_) => {
            crate::events::record(
                &state.db, &user.sub, "bouncer_delete",
                Some(serde_json::json!({"bouncer": name})),
            ).await;
            if htmx::is_htmx(&headers) {
                ([htmx::hx_redirect("/bouncers")], "").into_response()
            } else {
                (AppendHeaders([flash::set_flash("bouncer_deleted")]), Redirect::to("/bouncers")).into_response()
            }
        }
        Err(e) => {
            tracing::error!("failed to delete bouncer {name}: {e}");
            if htmx::is_htmx(&headers) {
                ([htmx::hx_redirect(&format!("/bouncers/{name}"))], "").into_response()
            } else {
                (AppendHeaders([flash::set_flash("bouncer_delete_failed")]), Redirect::to(&format!("/bouncers/{name}"))).into_response()
            }
        }
    }
}

