//! `/admin/pages/profile` — self-service profile page for the
//! currently-signed-in user. Anyone with a valid dashboard cookie can
//! reach this; no admin gate (the user-management page elsewhere is
//! the admin-side equivalent for editing OTHER users).
//!
//! Flow for each section:
//!   - Profile info  → POST update-info       (display_name, email)
//!   - Password      → POST change-password   (current_pw, new_pw, confirm)
//!   - TOTP enroll   → POST totp/start        (generate secret + QR)
//!                  → POST totp/confirm       (verify 6-digit code)
//!   - TOTP remove   → POST totp/remove       (current_pw)
//!
//! TOTP enrollment is two-step: a freshly-generated secret is shown to
//! the user as a QR code AND echoed in a hidden form field. Until the
//! user submits a valid 6-digit code derived from that secret, nothing
//! is written to `user_totp_secrets`. This means a half-finished enroll
//! (user closes the tab) leaves no garbage state.

use crate::api::error::ApiError;
use crate::api::middleware::AuthedUser;
use crate::api::state::AppState;
use crate::api::users::{hash_password, verify_password};
use axum::extract::{Extension, Form};
use axum::response::Html;
use serde::Deserialize;
use std::fmt::Write as _;
use std::sync::Arc;
use totp_rs::Secret;

// ---------- index ----------

pub async fn index(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
) -> Result<Html<String>, ApiError> {
    Ok(Html(render_full_page(&state, &user, None).await?))
}

// ---------- update profile info ----------

#[derive(Debug, Deserialize)]
pub struct InfoForm {
    #[serde(default)]
    pub display_name: String,
    #[serde(default)]
    pub email: String,
}

pub async fn update_info(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Form(form): Form<InfoForm>,
) -> Result<Html<String>, ApiError> {
    state
        .db
        .user_set_display_name(user.user_id, form.display_name.trim())
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    state
        .db
        .raw_update_user_email(user.user_id, form.email.trim())
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_full_page(&state, &user, Some(("ok", "Profile updated."))).await?,
    ))
}

// ---------- change password ----------

#[derive(Debug, Deserialize)]
pub struct PasswordForm {
    pub current_password: String,
    pub new_password: String,
    pub confirm_password: String,
}

pub async fn change_password(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Form(form): Form<PasswordForm>,
) -> Result<Html<String>, ApiError> {
    let row = state
        .db
        .user_find_by_id(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?
        .ok_or(ApiError::NotFound)?;
    if row.is_oidc_linked() {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some((
                    "error",
                    "Your account signs in via the identity provider — change the password there.",
                )),
            )
            .await?,
        ));
    }
    if form.new_password.len() < 4 {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "New password must be at least 4 characters.")),
            )
            .await?,
        ));
    }
    if form.new_password != form.confirm_password {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "New password and confirmation don't match.")),
            )
            .await?,
        ));
    }
    let pw_ok = verify_password(row.password_hash.clone(), form.current_password)
        .await
        .unwrap_or(false);
    if !pw_ok {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "Current password is incorrect.")),
            )
            .await?,
        ));
    }
    let hash = hash_password(form.new_password)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    state
        .db
        .user_set_password(user.user_id, &hash)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_full_page(&state, &user, Some(("ok", "Password updated."))).await?,
    ))
}

// ---------- TOTP: start ----------

/// `POST /admin/pages/profile/totp/start` — generate a fresh secret
/// and render the QR + confirm form. Nothing is written to the DB
/// yet; the secret rides in a hidden form field until confirm.
pub async fn totp_start(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
) -> Result<Html<String>, ApiError> {
    // Reject if the user already has TOTP — they should remove it first.
    let already = state
        .db
        .user_has_totp(user.user_id)
        .await
        .unwrap_or(false);
    if already {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "You already have TOTP enrolled. Disable it first if you want to re-enroll.")),
            )
            .await?,
        ));
    }

    let raw = sodiumoxide::randombytes::randombytes(20);
    let secret_b32 = Secret::Raw(raw).to_encoded().to_string();
    let issuer = "RustDesk";
    let label = format!("{}:{}", issuer, user.name);
    let otpauth = format!(
        "otpauth://totp/{label}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30",
        label = url_encode(&label),
        secret = url_encode(&secret_b32),
        issuer = url_encode(issuer),
    );
    let qr_svg = render_qr_svg(&otpauth);

    Ok(Html(render_totp_enroll_panel(
        &state,
        &user,
        &secret_b32,
        &qr_svg,
        None,
    )
    .await?))
}

// ---------- TOTP: confirm ----------

#[derive(Debug, Deserialize)]
pub struct TotpConfirmForm {
    pub secret_b32: String,
    pub code: String,
}

pub async fn totp_confirm(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Form(form): Form<TotpConfirmForm>,
) -> Result<Html<String>, ApiError> {
    let code = form.code.trim();
    let secret = form.secret_b32.trim();
    if secret.is_empty() {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "Missing secret in confirm form.")),
            )
            .await?,
        ));
    }
    let valid = crate::api::auth::verify_totp(secret, code).unwrap_or(false);
    if !valid {
        // Re-render the enroll panel with the same secret so the user
        // can try again — losing the QR forces them to start over and
        // re-scan, which is annoying when the only error was a typo'd
        // code.
        let issuer = "RustDesk";
        let label = format!("{}:{}", issuer, user.name);
        let otpauth = format!(
            "otpauth://totp/{label}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30",
            label = url_encode(&label),
            secret = url_encode(secret),
            issuer = url_encode(issuer),
        );
        let qr_svg = render_qr_svg(&otpauth);
        return Ok(Html(
            render_totp_enroll_panel(
                &state,
                &user,
                secret,
                &qr_svg,
                Some(("error", "Code didn't match. Try again — make sure the time on the authenticator device is in sync.")),
            )
            .await?,
        ));
    }
    state
        .db
        .totp_enroll(user.user_id, secret)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_full_page(
            &state,
            &user,
            Some(("ok", "TOTP enrolled. Future sign-ins will require a 6-digit code.")),
        )
        .await?,
    ))
}

// ---------- TOTP: remove ----------

#[derive(Debug, Deserialize)]
pub struct TotpRemoveForm {
    pub current_password: String,
}

pub async fn totp_remove(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Form(form): Form<TotpRemoveForm>,
) -> Result<Html<String>, ApiError> {
    let row = state
        .db
        .user_find_by_id(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?
        .ok_or(ApiError::NotFound)?;
    let pw_ok = verify_password(row.password_hash.clone(), form.current_password)
        .await
        .unwrap_or(false);
    if !pw_ok {
        return Ok(Html(
            render_full_page(
                &state,
                &user,
                Some(("error", "Current password is incorrect — TOTP not removed.")),
            )
            .await?,
        ));
    }
    state
        .db
        .totp_unenroll(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_full_page(&state, &user, Some(("ok", "TOTP removed."))).await?,
    ))
}

// ---------- rendering ----------

async fn render_full_page(
    state: &Arc<AppState>,
    user: &AuthedUser,
    notice: Option<(&str, &str)>,
) -> Result<String, ApiError> {
    render_full_page_with_totp_override(state, user, notice, None).await
}

/// `totp_panel_override = Some(html)` swaps in a custom TOTP block —
/// used during enrollment confirm so the QR code panel sits where the
/// status badge would normally be, and the password / profile sections
/// stay visible above it.
async fn render_full_page_with_totp_override(
    state: &Arc<AppState>,
    user: &AuthedUser,
    notice: Option<(&str, &str)>,
    totp_panel_override: Option<String>,
) -> Result<String, ApiError> {
    let row = state
        .db
        .user_find_by_id(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?
        .ok_or(ApiError::NotFound)?;
    let has_totp = state
        .db
        .user_has_totp(user.user_id)
        .await
        .unwrap_or(false);
    let notice_block = notice
        .map(|(k, m)| notice_html(k, m))
        .unwrap_or_default();

    // OIDC-linked accounts sign in via the IdP — local password and
    // local TOTP both moot. Replace those sections with a short note.
    let oidc_linked = row.is_oidc_linked();
    let password_section = if oidc_linked {
        r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4">
  <h3 class="text-sm font-semibold text-slate-300 mb-2">Password</h3>
  <p class="text-sm text-slate-400">
    Your account signs in via your organisation's identity provider — there's no local password to change here.
  </p>
</section>"##
            .to_string()
    } else {
        r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4">
  <h3 class="text-sm font-semibold text-slate-300 mb-3">Change password</h3>
  <form
    class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm"
    hx-post="/admin/pages/profile/change-password"
    hx-target="#main"
    hx-swap="innerHTML"
    hx-on::after-request="if (event.detail.successful) this.reset()"
  >
    <input name="current_password" type="password" required placeholder="current password" class="bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
    <input name="new_password" type="password" required minlength="4" placeholder="new password" class="bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
    <input name="confirm_password" type="password" required minlength="4" placeholder="confirm new" class="bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
    <button type="submit" class="sm:col-span-3 justify-self-start bg-sky-600 hover:bg-sky-500 rounded px-3 py-1.5 text-white text-sm">Update password</button>
  </form>
</section>"##.to_string()
    };

    let totp_section = if let Some(panel) = totp_panel_override {
        panel
    } else if oidc_linked {
        r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4">
  <h3 class="text-sm font-semibold text-slate-300 mb-2">Two-factor authentication</h3>
  <p class="text-sm text-slate-400">
    MFA is managed by your identity provider — local TOTP isn't used for OIDC sign-ins.
  </p>
</section>"##
            .to_string()
    } else if has_totp {
        format!(
            r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4">
  <h3 class="text-sm font-semibold text-slate-300 mb-2">Two-factor authentication</h3>
  <div class="flex items-center gap-3 mb-3">
    <span class="inline-flex px-1.5 py-0.5 rounded bg-violet-900/50 border border-violet-700/50 text-violet-300 text-xs">enrolled</span>
    <span class="text-xs text-slate-400">Sign-ins require a 6-digit code from your authenticator.</span>
  </div>
  <form
    class="flex gap-2 items-center text-sm"
    hx-post="/admin/pages/profile/totp/remove"
    hx-target="#main"
    hx-swap="innerHTML"
    hx-confirm="Remove TOTP from your account?"
  >
    <input name="current_password" type="password" required placeholder="current password" class="bg-slate-800 border border-slate-700 rounded px-2 py-1.5 flex-1 max-w-xs"/>
    <button class="bg-rose-700 hover:bg-rose-600 rounded px-3 py-1.5 text-white text-xs">Disable TOTP</button>
  </form>
</section>"##
        )
    } else {
        r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4">
  <h3 class="text-sm font-semibold text-slate-300 mb-2">Two-factor authentication</h3>
  <p class="text-sm text-slate-400 mb-3">
    Add a TOTP authenticator (1Password, Authy, Google Authenticator, etc.) so sign-ins also require a 6-digit code.
  </p>
  <button
    class="bg-sky-600 hover:bg-sky-500 rounded px-3 py-1.5 text-white text-sm"
    hx-post="/admin/pages/profile/totp/start"
    hx-target="#main"
    hx-swap="innerHTML"
  >Enroll TOTP</button>
</section>"##.to_string()
    };

    Ok(format!(
        r##"<div class="space-y-6 max-w-3xl">
  <header>
    <h2 class="text-lg font-semibold">Profile</h2>
    <p class="text-xs text-slate-500 mt-0.5">Signed in as <span class="text-slate-300">{username}</span></p>
  </header>
  {notice}

  <section class="rounded-md border border-slate-800 bg-slate-900 p-4">
    <h3 class="text-sm font-semibold text-slate-300 mb-3">Profile info</h3>
    <form
      class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm"
      hx-post="/admin/pages/profile/update-info"
      hx-target="#main"
      hx-swap="innerHTML"
    >
      <label class="block">
        <span class="text-xs text-slate-400">Display name</span>
        <input name="display_name" value="{display_name}" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
      </label>
      <label class="block">
        <span class="text-xs text-slate-400">Email</span>
        <input name="email" type="email" value="{email}" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
      </label>
      <button type="submit" class="sm:col-span-2 justify-self-start bg-sky-600 hover:bg-sky-500 rounded px-3 py-1.5 text-white text-sm">Save</button>
    </form>
  </section>

  {password_section}

  {totp_section}
</div>"##,
        username = html_escape(&user.name),
        display_name = html_escape(&row.display_name),
        email = html_escape(&row.email),
        notice = notice_block,
        password_section = password_section,
        totp_section = totp_section,
    ))
}

async fn render_totp_enroll_panel(
    state: &Arc<AppState>,
    user: &AuthedUser,
    secret_b32: &str,
    qr_svg: &str,
    notice: Option<(&str, &str)>,
) -> Result<String, ApiError> {
    let panel = format!(
        r##"<section class="rounded-md border border-sky-700/60 bg-sky-900/20 p-4">
  <h3 class="text-sm font-semibold text-sky-200 mb-2">Confirm TOTP enrollment</h3>
  <p class="text-xs text-sky-200/80 mb-3">
    Scan the QR code with your authenticator app, then enter the 6-digit code it shows to confirm.
    Nothing is enrolled until you submit a valid code.
  </p>
  <div class="flex flex-col sm:flex-row gap-4 items-start">
    <div class="bg-white p-2 rounded inline-block">{qr}</div>
    <div class="flex-1 space-y-2 text-sm">
      <div>
        <span class="text-xs text-slate-400">Secret (manual entry):</span>
        <code class="block mt-1 break-all text-emerald-200 bg-slate-950 px-2 py-1.5 rounded text-xs">{secret}</code>
      </div>
      <form
        class="flex gap-2 items-stretch"
        hx-post="/admin/pages/profile/totp/confirm"
        hx-target="#main"
        hx-swap="innerHTML"
      >
        <input type="hidden" name="secret_b32" value="{secret}"/>
        <input
          name="code"
          type="text"
          inputmode="numeric"
          pattern="[0-9]{{6}}"
          required
          maxlength="6"
          autocomplete="one-time-code"
          placeholder="123456"
          class="bg-slate-800 border border-slate-700 rounded px-2 py-1.5 w-32 font-mono tracking-widest"
        />
        <button type="submit" class="bg-sky-600 hover:bg-sky-500 rounded px-3 py-1.5 text-white text-sm">Confirm</button>
      </form>
    </div>
  </div>
</section>"##,
        qr = qr_svg,
        secret = html_escape(secret_b32),
    );
    render_full_page_with_totp_override(state, user, notice, Some(panel)).await
}

fn render_qr_svg(payload: &str) -> String {
    // qrcode 0.14: build the QR, then render with the SVG renderer.
    // .min_dimensions caps how big the SVG-pixel grid is; the actual
    // CSS size is handled by inline width/height, but the underlying
    // module size needs to be reasonable so it stays crisp on retina.
    use qrcode::render::svg;
    use qrcode::QrCode;
    match QrCode::new(payload.as_bytes()) {
        Ok(code) => code
            .render::<svg::Color<'_>>()
            .min_dimensions(180, 180)
            .dark_color(svg::Color("#000"))
            .light_color(svg::Color("#fff"))
            .build(),
        Err(_) => "<div class=\"text-rose-300 text-xs\">QR encode failed</div>".to_string(),
    }
}

fn notice_html(kind: &str, msg: &str) -> String {
    let (border, bg, text) = match kind {
        "ok" => ("emerald-700/50", "emerald-900/30", "emerald-300"),
        _ => ("rose-700/50", "rose-900/30", "rose-300"),
    };
    format!(
        r##"<div class="rounded border border-{border} bg-{bg} p-3 mb-4 text-sm text-{text}">{msg}</div>"##,
        border = border,
        bg = bg,
        text = text,
        msg = html_escape(msg),
    )
}

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

fn url_encode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.as_bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(*b as char);
            }
            _ => {
                let _ = write!(out, "%{:02X}", b);
            }
        }
    }
    out
}
