use askama::Template;
use askama_web::WebTemplate;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::response::{AppendHeaders, IntoResponse, Redirect, Response};
use axum::Form;
use serde::Deserialize;
use sqlx::FromRow;
use tower_sessions::Session;

use crate::auth_guard::AuthUser;
use crate::flash;
use crate::htmx;
use crate::page::PageContext;
use crate::state::AppState;
use irc_now_common::auth::UserClaims;

#[derive(FromRow)]
struct ProfileRow {
    email: Option<String>,
    display_name: Option<String>,
    plan: String,
    created_at: String,
    content_expires: bool,
}

#[derive(Template, WebTemplate)]
#[template(path = "profile.html")]
pub struct ProfileTemplate {
    pub page: PageContext,
    pub display_name: String,
    pub email: String,
    pub plan: String,
    pub sub: String,
    pub created_at: String,
    pub password_url: String,
    pub content_expires: bool,
}

pub async fn index(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
) -> Result<axum::response::Response, Redirect> {
    let row = sqlx::query_as::<_, ProfileRow>(
        "SELECT email, display_name, plan, to_char(created_at, 'YYYY-MM-DD') AS created_at, content_expires FROM users WHERE keycloak_sub = $1",
    )
    .bind(&user.sub)
    .fetch_one(&state.db)
    .await
    .map_err(|e| {
        tracing::error!("profile query failed: {e}");
        Redirect::temporary("/dashboard")
    })?;

    let password_url = format!(
        "{}/account/#/security/signingin",
        state.oidc.issuer_url
    );

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

    let template = ProfileTemplate {
        page,
        display_name: row.display_name.unwrap_or_default(),
        email: row.email.unwrap_or_default(),
        plan: row.plan,
        sub: user.sub,
        created_at: row.created_at,
        password_url,
        content_expires: row.content_expires,
    };

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

#[derive(Deserialize)]
pub struct ProfileForm {
    display_name: String,
    email: String,
    #[serde(default)]
    content_expires: Option<String>,
}

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

pub async fn update(
    State(state): State<AppState>,
    headers: HeaderMap,
    AuthUser(user): AuthUser,
    session: Session,
    Form(form): Form<ProfileForm>,
) -> Response {
    let display_name = form.display_name.trim().to_string();
    let new_email = form.email.trim().to_string();
    let content_expires = form.content_expires.is_some();

    let email_changed = user.email.as_deref() != Some(&new_email);

    if email_changed && !new_email.is_empty() {
        if let Err(e) = update_keycloak_email(&state, &user.sub, &new_email).await {
            tracing::error!("keycloak email update failed: {e}");
            return flash_response(&headers, "email_update_failed", "/profile");
        }
    }

    let display_name_val = if display_name.is_empty() {
        None
    } else {
        Some(display_name.as_str())
    };

    let result = sqlx::query(
        "UPDATE users SET display_name = $1, email = $2, content_expires = $3 WHERE keycloak_sub = $4",
    )
    .bind(display_name_val)
    .bind(&new_email)
    .bind(content_expires)
    .bind(&user.sub)
    .execute(&state.db)
    .await;

    if let Err(e) = result {
        tracing::error!("profile update failed: {e}");
        return flash_response(&headers, "profile_save_failed", "/profile");
    }

    let plan_str = user.plan.as_deref().unwrap_or("free");
    if let Err(e) = update_keycloak_user_attributes(
        &state,
        &user.sub,
        plan_str,
        content_expires,
    )
    .await
    {
        tracing::warn!("keycloak attribute sync failed: {e}");
    }

    let updated_claims = UserClaims {
        sub: user.sub,
        email: if new_email.is_empty() {
            None
        } else {
            Some(new_email)
        },
        plan: user.plan,
        stripe_customer_id: user.stripe_customer_id,
        display_name: if display_name.is_empty() {
            None
        } else {
            Some(display_name)
        },
        content_expires: Some(content_expires),
    };

    if let Err(e) = session.insert("user", &updated_claims).await {
        tracing::error!("session update failed: {e}");
    }

    flash_response(&headers, "profile_saved", "/profile")
}

fn flash_response(headers: &HeaderMap, code: &str, redirect_url: &str) -> Response {
    if htmx::is_htmx(headers) {
        if let Some(msg) = flash::resolve(code) {
            let fragment = FlashFragment { css_class: msg.css_class(), text: msg.text };
            return axum::response::Html(fragment.render().unwrap_or_default()).into_response();
        }
        "".into_response()
    } else {
        (AppendHeaders([flash::set_flash(code)]), Redirect::to(redirect_url)).into_response()
    }
}

async fn get_keycloak_admin_token(state: &AppState) -> Result<String, String> {
    let token_url = format!(
        "{}/protocol/openid-connect/token",
        state.oidc.issuer_url
    );

    let resp = state
        .http_client
        .post(&token_url)
        .form(&[
            ("grant_type", "client_credentials"),
            ("client_id", &state.oidc.client_id),
            ("client_secret", &state.oidc.client_secret),
        ])
        .send()
        .await
        .map_err(|e| format!("token request failed: {e}"))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("token request returned {status}: {body}"));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("token parse failed: {e}"))?;

    body["access_token"]
        .as_str()
        .map(String::from)
        .ok_or_else(|| "no access_token in response".to_string())
}

pub(crate) async fn update_keycloak_user_attributes(
    state: &AppState,
    sub: &str,
    plan: &str,
    content_expires: bool,
) -> Result<(), String> {
    let token = get_keycloak_admin_token(state).await?;

    let base_url = state
        .oidc
        .issuer_url
        .replace("/realms/irc-now", "");

    let url = format!(
        "{}/admin/realms/irc-now/users/{}",
        base_url, sub
    );

    let resp = state
        .http_client
        .put(&url)
        .bearer_auth(&token)
        .json(&serde_json::json!({
            "attributes": {
                "plan": [plan],
                "content_expires": [content_expires.to_string()]
            }
        }))
        .send()
        .await
        .map_err(|e| format!("attribute update request failed: {e}"))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("attribute update returned {status}: {body}"));
    }

    Ok(())
}

async fn update_keycloak_email(
    state: &AppState,
    sub: &str,
    email: &str,
) -> Result<(), String> {
    let token = get_keycloak_admin_token(state).await?;

    let base_url = state
        .oidc
        .issuer_url
        .replace("/realms/irc-now", "");

    let url = format!(
        "{}/admin/realms/irc-now/users/{}",
        base_url, sub
    );

    let resp = state
        .http_client
        .put(&url)
        .bearer_auth(&token)
        .json(&serde_json::json!({ "email": email }))
        .send()
        .await
        .map_err(|e| format!("email update request failed: {e}"))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("email update returned {status}: {body}"));
    }

    Ok(())
}
