//! Page-level components and their public render entry points.
//!
//! Each `render_xxx` function constructs a Leptos view, drives it through
//! the SSR renderer, and returns a complete HTML document. The doctype is
//! prepended manually because `view!{}` only renders the tree it is given.

use crate::layout::Shell;
use chrono::{DateTime, Utc};
use leptos::prelude::*;
use leptos::reactive::owner::Owner;
use sui_id_shared::api::{AuditLogEntryDto, ClientSummary, UserSummary};

const DOCTYPE: &str = "<!DOCTYPE html>";

/// Severity of a flash banner displayed at the top of a page.
#[derive(Debug, Clone, Copy)]
pub enum FlashKind {
    Info,
    Warn,
    Error,
}

impl FlashKind {
    fn class(self) -> &'static str {
        match self {
            Self::Info => "flash info",
            Self::Warn => "flash warn",
            Self::Error => "flash error",
        }
    }
}

#[derive(Debug, Clone)]
pub struct Flash {
    pub kind: FlashKind,
    pub text: String,
}

fn flash_banner(flash: Option<Flash>) -> Option<impl IntoView> {
    flash.map(|f| view! { <div class=f.kind.class() role="status">{f.text}</div> })
}

fn fmt_time(t: DateTime<Utc>) -> String {
    t.format("%Y-%m-%d %H:%M UTC").to_string()
}

/// Run a closure inside a fresh reactive Owner and prepend the HTML doctype.
fn render<F, V>(f: F) -> String
where
    F: FnOnce() -> V,
    V: IntoView + 'static,
{
    let owner = Owner::new();
    let body = owner.with(|| f().into_view().to_html());
    let mut out = String::with_capacity(DOCTYPE.len() + body.len());
    out.push_str(DOCTYPE);
    out.push_str(&body);
    out
}

// ---------- setup ----------

pub fn render_setup(flash: Option<Flash>) -> String {
    render(move || {
        view! {
            <crate::layout::AuthShell title="Setup".to_string()>
                <h1>"sui-id へようこそ"</h1>
                <p class="muted">
                    "サービスはまだ初期化されていません。"
                    "起動時に表示されたセットアップトークンを使って、最初の管理者アカウントを作成してください。"
                </p>
                {flash_banner(flash)}
                <form method="post" action="/setup" class="stack">
                    <div class="field">
                        <label for="token" class="field__label">"セットアップトークン"</label>
                        <input id="token" name="setup_token" type="password"
                               required=true autocomplete="off" autofocus=true />
                        <span class="field__hint">"起動ログに 1 度だけ出力された値"</span>
                    </div>
                    <div class="field">
                        <label for="username" class="field__label">"管理者ユーザー名"</label>
                        <input id="username" name="username" type="text"
                               required=true autocomplete="username" />
                    </div>
                    <div class="field">
                        <label for="display" class="field__label">"表示名(任意)"</label>
                        <input id="display" name="display_name" type="text" autocomplete="name" />
                    </div>
                    <div class="field">
                        <label for="password" class="field__label">"パスワード"</label>
                        <input id="password" name="password" type="password"
                               required=true minlength="12" autocomplete="new-password" />
                        <span class="field__hint">"12 文字以上"</span>
                    </div>
                    <button type="submit">"管理者を作成"</button>
                </form>
            </crate::layout::AuthShell>
        }
    })
}

// ---------- login ----------

pub fn render_login(flash: Option<Flash>, next: Option<String>) -> String {
    render(move || {
        let next_value = next.clone().unwrap_or_default();
        view! {
            <crate::layout::AuthShell title="Sign in".to_string()>
                <h1>"sui-id にログイン"</h1>
                {flash_banner(flash)}
                <form method="post" action="/admin/login" class="stack">
                    <input type="hidden" name="next" value=next_value />
                    <div class="field">
                        <label for="username" class="field__label">"ユーザー名またはメールアドレス"</label>
                        <input id="username" name="username" type="text"
                               required=true autocomplete="username"
                               autofocus=true />
                    </div>
                    <div class="field">
                        <label for="password" class="field__label">"パスワード"</label>
                        <input id="password" name="password" type="password"
                               required=true autocomplete="current-password" />
                    </div>
                    <button type="submit">"ログイン"</button>
                </form>
            </crate::layout::AuthShell>
        }
    })
}

// ---------- MFA challenge ----------

pub fn render_mfa_challenge(
    flash: Option<Flash>,
    csrf_token: String,
    has_passkey: bool,
) -> String {
    render(move || {
        let csrf_for_totp = csrf_token.clone();
        let csrf_for_pk = csrf_token.clone();
        let passkey_block = if has_passkey {
            view! {
                <hr class="divider" />
                <p class="muted">"または、パスキーでサインイン:"</p>
                <form id="passkey-auth-form" method="post"
                      action="/admin/login/webauthn/start" class="stack">
                    <input type="hidden" name="_csrf" value=csrf_for_pk />
                    <button type="submit" class="secondary">"パスキーでサインイン"</button>
                </form>
                <script src="/static/webauthn.js"></script>
            }
            .into_any()
        } else {
            view! { <></> }.into_any()
        };
        view! {
            <crate::layout::AuthShell title="Verification required".to_string()>
                <h1>"確認コード"</h1>
                {flash_banner(flash)}
                <p class="muted">
                    "認証アプリの 6 桁コード、またはリカバリーコードを入力してください。"
                </p>
                <form method="post" action="/admin/login/mfa" class="stack">
                    <input type="hidden" name="_csrf" value=csrf_for_totp />
                    <div class="field">
                        <label for="code" class="field__label">"コード"</label>
                        <input id="code" name="code" type="text"
                               required=true autocomplete="one-time-code"
                               inputmode="text" autofocus=true />
                    </div>
                    <button type="submit">"確認"</button>
                </form>
                {passkey_block}
            </crate::layout::AuthShell>
        }
    })
}

// ---------- profile (MFA settings) ----------

pub struct ProfileData {
    pub username: String,
    /// True if TOTP is set up.
    pub totp_enabled: bool,
    /// Set when the user has just enrolled or regenerated codes.
    /// Displayed exactly once.
    pub fresh_recovery_codes: Option<Vec<String>>,
    /// Registered WebAuthn passkeys for this user.
    pub passkeys: Vec<PasskeyDescriptor>,
}

pub struct PasskeyDescriptor {
    pub id: String,
    pub nickname: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
}

pub fn render_profile(data: ProfileData, flash: Option<Flash>, csrf_token: String) -> String {
    render(move || {
        let ProfileData {
            username,
            totp_enabled,
            fresh_recovery_codes,
            passkeys,
        } = data;
        let csrf_for_disable = csrf_token.clone();
        let csrf_for_regen = csrf_token.clone();
        let csrf_for_enroll = csrf_token.clone();
        let csrf_for_passkey_register = csrf_token.clone();
        let csrf_for_passkey_delete = csrf_token.clone();
        let recovery_block = fresh_recovery_codes.map(|codes| {
            let lis: Vec<_> = codes
                .into_iter()
                .map(|c| view! { <li><span class="code">{c}</span></li> })
                .collect();
            view! {
                <div class="flash warn" role="status">
                    <div class="stack-tight">
                        <strong>"リカバリーコードを今すぐ保存してください。再表示はされません。"</strong>
                        <p class="muted">
                            "各コードは 1 度だけ使えます。安全な場所に保管してください。"
                            "認証アプリへのアクセスを失った場合、6 桁コードの代わりにこのいずれかを入力してサインインできます。"
                        </p>
                        <ol>{lis}</ol>
                    </div>
                </div>
            }
        });
        let mfa_section = if totp_enabled {
            view! {
                <div class="card">
                    <h3 class="card__title">"認証アプリ(TOTP)"</h3>
                    <p>
                        "状態:"
                        <span class="badge badge--ok" style="margin-left:var(--space-1)">"有効"</span>
                    </p>
                    <div class="card__footer">
                        <form method="post" action="/admin/profile/mfa/recovery-codes/regenerate"
                              style="display:inline">
                            <input type="hidden" name="_csrf" value=csrf_for_regen />
                            <button type="submit" class="secondary">"リカバリーコード再生成"</button>
                        </form>
                        <form method="post" action="/admin/profile/mfa/disable"
                              style="display:inline"
                              onsubmit="return confirm('認証アプリによる 2 段階認証を無効化しますか?');">
                            <input type="hidden" name="_csrf" value=csrf_for_disable />
                            <button type="submit" class="danger">"TOTP を無効化"</button>
                        </form>
                    </div>
                </div>
            }
            .into_any()
        } else {
            view! {
                <div class="card">
                    <h3 class="card__title">"認証アプリ(TOTP)"</h3>
                    <p>
                        "状態:"
                        <span class="badge badge--warn" style="margin-left:var(--space-1)">"未設定"</span>
                    </p>
                    <p class="muted">
                        "有効化すると、サインイン時に認証アプリの 6 桁コードが必要になります。"
                        "標準準拠の TOTP アプリならどれでも利用できます(Aegis / FreeOTP / Google Authenticator / 1Password など)。"
                    </p>
                    <div class="card__footer">
                        <form method="post" action="/admin/profile/mfa/enroll/start">
                            <input type="hidden" name="_csrf" value=csrf_for_enroll />
                            <button type="submit">"TOTP を設定"</button>
                        </form>
                    </div>
                </div>
            }
            .into_any()
        };
        let passkey_rows: Vec<_> = passkeys
            .into_iter()
            .map(|p| {
                let id = p.id.clone();
                let delete_url = format!("/admin/profile/webauthn/{id}/delete");
                let csrf = csrf_for_passkey_delete.clone();
                let last = p
                    .last_used_at
                    .map(fmt_time)
                    .unwrap_or_else(|| "(未使用)".into());
                view! {
                    <tr>
                        <td>{p.nickname}</td>
                        <td class="muted">{fmt_time(p.created_at)}</td>
                        <td class="muted">{last}</td>
                        <td>
                            <form method="post" action=delete_url style="display:inline"
                                  onsubmit="return confirm('このパスキーを削除しますか? このパスキーでのサインインができなくなります。');">
                                <input type="hidden" name="_csrf" value=csrf />
                                <button type="submit" class="danger">"削除"</button>
                            </form>
                        </td>
                    </tr>
                }
            })
            .collect();
        let passkey_table = if passkey_rows.is_empty() {
            view! {
                <p class="muted">"パスキーは未登録です。"</p>
            }
            .into_any()
        } else {
            view! {
                <div class="table-wrap">
                    <table>
                        <thead>
                            <tr>
                                <th>"名前"</th>
                                <th>"登録日"</th>
                                <th>"最終使用"</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>{passkey_rows}</tbody>
                    </table>
                </div>
            }
            .into_any()
        };
        view! {
            <Shell title="Profile".to_string() show_nav=true current=Some("profile".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"プロフィール"</h1>
                        <p class="page-header__lede">
                            {format!("{username} のセキュリティ設定")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}
                {recovery_block}

                <section>
                    <h2>"2 段階認証"</h2>
                    {mfa_section}
                </section>

                <section>
                    <h2>"パスキー"</h2>
                    <p class="muted">
                        "パスキーは、スマートフォン・PC・セキュリティキー・パスワードマネージャに保存されるハードウェア裏付け資格情報です。"
                        "デバイスから外に出ません。複数登録できます — バックアップとして 2 つ以上登録しておくことを推奨します。"
                    </p>
                    {passkey_table}
                    <h3>"新しいパスキーを登録"</h3>
                    <div class="card">
                        <form id="passkey-register-form" method="post"
                              action="/admin/profile/webauthn/register/start" class="stack">
                            <input type="hidden" name="_csrf" value=csrf_for_passkey_register />
                            <div class="field">
                                <label for="pk-nickname" class="field__label">"ニックネーム"</label>
                                <input id="pk-nickname" name="nickname" type="text" required=true />
                                <span class="field__hint">"例: YubiKey 5C / MacBook Touch ID"</span>
                            </div>
                            <div>
                                <button type="submit">"パスキーを登録"</button>
                            </div>
                        </form>
                    </div>
                </section>
                <script src="/static/webauthn.js"></script>
            </Shell>
        }
    })
}

pub struct MfaSetupData {
    /// otpauth:// URI for the QR code
    pub otpauth_uri: String,
    /// Pre-rendered SVG of the QR code (full <svg>...</svg> string).
    pub qr_svg: String,
    /// Base32-encoded secret string for users who would rather type it
    /// in than scan the QR code.
    pub secret_b32: String,
}

pub fn render_mfa_setup(data: MfaSetupData, flash: Option<Flash>, csrf_token: String) -> String {
    render(move || {
        let MfaSetupData { otpauth_uri, qr_svg, secret_b32 } = data;
        view! {
            <Shell title="Set up MFA".to_string() show_nav=true current=Some("profile".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"2 段階認証の設定"</h1>
                        <p class="page-header__lede">"認証アプリと sui-id を関連付けます。"</p>
                    </div>
                </header>
                {flash_banner(flash)}

                <div class="card">
                    <h3 class="card__title">"手順"</h3>
                    <ol>
                        <li>"認証アプリを開き、下の QR コードを読み取ってください。手入力する場合は秘密鍵をペーストしてください。"</li>
                        <li>"アプリに表示される 6 桁コードを以下のフォームに入力して確認してください。"</li>
                        <li>"設定完了後、1 度だけ使えるリカバリーコードが 8 個発行されます。安全な場所に保管してください。"</li>
                    </ol>
                </div>

                <div class="card">
                    <h3 class="card__title">"QR コードと秘密鍵"</h3>
                    <div inner_html=qr_svg style="max-width:240px;margin-bottom:var(--space-3)"></div>
                    <p>"秘密鍵:"<span class="code" style="margin-left:var(--space-1)">{secret_b32}</span></p>
                    <details>
                        <summary class="muted">"otpauth URI(上級者向け)"</summary>
                        <p><span class="code">{otpauth_uri}</span></p>
                    </details>
                </div>

                <div class="card">
                    <h3 class="card__title">"確認"</h3>
                    <form method="post" action="/admin/profile/mfa/enroll/confirm" class="stack">
                        <input type="hidden" name="_csrf" value=csrf_token />
                        <div class="field">
                            <label for="code" class="field__label">"確認コード"</label>
                            <input id="code" name="code" type="text" required=true
                                   autocomplete="one-time-code" inputmode="text" autofocus=true />
                            <span class="field__hint">"アプリに表示されている 6 桁コード"</span>
                        </div>
                        <div>
                            <button type="submit">"確認して有効化"</button>
                        </div>
                    </form>
                </div>
            </Shell>
        }
    })
}

// ---------- dashboard ----------

pub struct DashboardData {
    pub admin_username: String,
    pub user_count: usize,
    pub client_count: usize,
    pub issuer: String,
}

pub fn render_dashboard(data: DashboardData, flash: Option<Flash>) -> String {
    render(move || {
        let DashboardData { admin_username, user_count, client_count, issuer } = data;
        view! {
            <Shell title="Dashboard".to_string() show_nav=true current=Some("dashboard".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"ダッシュボード"</h1>
                        <p class="page-header__lede">
                            {format!("Hello, {admin_username}. システムの概要と統計情報。")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}

                <section class="grid-cards" aria-label="統計サマリ">
                    <div class="card">
                        <div class="stat">
                            <span class="stat__value">{user_count.to_string()}</span>
                            <span class="stat__label">"ユーザー"</span>
                        </div>
                    </div>
                    <div class="card">
                        <div class="stat">
                            <span class="stat__value">{client_count.to_string()}</span>
                            <span class="stat__label">"クライアント"</span>
                        </div>
                    </div>
                    <div class="card">
                        <div class="stat">
                            <span class="stat__value">
                                <span class="badge badge--ok">"稼働中"</span>
                            </span>
                            <span class="stat__label">"サービス状態"</span>
                        </div>
                    </div>
                </section>

                <section>
                    <h2>"OIDC エンドポイント"</h2>
                    <div class="table-wrap">
                        <table>
                            <tbody>
                                <tr>
                                    <th scope="row">"Issuer"</th>
                                    <td><span class="code">{issuer}</span></td>
                                </tr>
                                <tr>
                                    <th scope="row">"Discovery"</th>
                                    <td><a href="/.well-known/openid-configuration"><span class="code">"/.well-known/openid-configuration"</span></a></td>
                                </tr>
                                <tr>
                                    <th scope="row">"JWKS"</th>
                                    <td><a href="/.well-known/jwks.json"><span class="code">"/.well-known/jwks.json"</span></a></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </section>
            </Shell>
        }
    })
}

// ---------- users ----------

fn user_row_view(u: UserSummary, current_user: String, csrf: String) -> impl IntoView {
    let display = u.display_name.clone().unwrap_or_default();
    let id_str = u.id.to_string();
    let is_self = u.username == current_user;
    let is_disabled = u.is_disabled;
    let is_deleted = u.is_deleted;
    let is_admin = u.is_admin;
    let mfa_enabled = u.mfa_enabled;
    let action_label = if is_disabled { "Enable" } else { "Disable" };
    let action_target = if is_disabled { "false" } else { "true" };
    let disabled_url = format!("/admin/users/{id_str}/disabled");
    let delete_url = format!("/admin/users/{id_str}/delete");
    let reset_mfa_url = format!("/admin/users/{id_str}/mfa-reset");
    let csrf_disable = csrf.clone();
    let csrf_delete = csrf.clone();
    let csrf_reset = csrf.clone();

    let status_badge = if is_deleted {
        view! { <span class="badge badge--danger">"deleted"</span> }.into_any()
    } else if is_disabled {
        view! { <span class="badge badge--warn">"disabled"</span> }.into_any()
    } else if is_admin {
        view! { <span class="badge badge--accent">"admin"</span> }.into_any()
    } else {
        view! { <span class="badge badge--ok">"active"</span> }.into_any()
    };

    let mfa_cell = if mfa_enabled {
        view! { <td><span class="badge badge--ok">"on"</span></td> }.into_any()
    } else {
        view! { <td><span class="muted">"off"</span></td> }.into_any()
    };

    let actions = if is_self {
        view! { <td><span class="muted">"(you)"</span></td> }.into_any()
    } else if is_deleted {
        view! { <td><span class="muted">"-"</span></td> }.into_any()
    } else {
        let reset_form = if mfa_enabled {
            view! {
                <form method="post" action=reset_mfa_url style="display:inline"
                      onsubmit="return confirm('Forcibly remove every MFA factor for this user (TOTP and all passkeys)? Use only when the user has lost access to their second factor.');">
                    <input type="hidden" name="_csrf" value=csrf_reset />
                    <button type="submit" class="secondary">"Reset MFA"</button>
                </form>
                " "
            }
            .into_any()
        } else {
            view! { <></> }.into_any()
        };
        view! {
            <td>
                <div class="row" style="gap:var(--space-1)">
                    {reset_form}
                    <form method="post" action=disabled_url style="display:inline">
                        <input type="hidden" name="_csrf" value=csrf_disable />
                        <input type="hidden" name="disabled" value=action_target />
                        <button type="submit" class="secondary">{action_label}</button>
                    </form>
                    <form method="post" action=delete_url style="display:inline"
                          onsubmit="return confirm('Permanently delete this user?');">
                        <input type="hidden" name="_csrf" value=csrf_delete />
                        <button type="submit" class="danger">"Delete"</button>
                    </form>
                </div>
            </td>
        }
        .into_any()
    };

    view! {
        <tr>
            <td><span class="code">{u.username}</span></td>
            <td>{display}</td>
            <td>{status_badge}</td>
            {mfa_cell}
            <td class="muted">{fmt_time(u.created_at)}</td>
            {actions}
        </tr>
    }
}

pub fn render_users(
    users: Vec<UserSummary>,
    flash: Option<Flash>,
    current_user: String,
    csrf_token: String,
) -> String {
    render(move || {
        let csrf_for_rows = csrf_token.clone();
        let csrf_for_form = csrf_token.clone();
        let user_count = users.len();
        let rows: Vec<_> = users
            .into_iter()
            .map(|u| user_row_view(u, current_user.clone(), csrf_for_rows.clone()))
            .collect();
        view! {
            <Shell title="Users".to_string() show_nav=true current=Some("users".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"ユーザー管理"</h1>
                        <p class="page-header__lede">
                            "ユーザーの作成・編集・管理。"
                            {format!(" 現在 {user_count} 名。")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}

                <section>
                    <h2>"新しいユーザーを追加"</h2>
                    <div class="card">
                        <form method="post" action="/admin/users" class="stack">
                            <input type="hidden" name="_csrf" value=csrf_for_form />
                            <div class="field">
                                <label for="u-name" class="field__label">"ユーザー名"</label>
                                <input id="u-name" name="username" type="text"
                                       required=true autocomplete="off" />
                            </div>
                            <div class="field">
                                <label for="u-disp" class="field__label">"表示名(任意)"</label>
                                <input id="u-disp" name="display_name" type="text" autocomplete="off" />
                            </div>
                            <div class="field">
                                <label for="u-pw" class="field__label">"パスワード(12 文字以上)"</label>
                                <input id="u-pw" name="password" type="password"
                                       required=true minlength="12" autocomplete="new-password" />
                            </div>
                            <label class="row" style="gap:var(--space-2)">
                                <input name="is_admin" type="checkbox" value="true" />
                                <span>"管理者権限を付与する"</span>
                            </label>
                            <div>
                                <button type="submit">"ユーザーを作成"</button>
                            </div>
                        </form>
                    </div>
                </section>

                <section>
                    <h2>"ユーザー一覧"</h2>
                    <div class="table-wrap">
                        <table>
                            <thead>
                                <tr>
                                    <th>"ユーザー名"</th>
                                    <th>"表示名"</th>
                                    <th>"状態"</th>
                                    <th>"MFA"</th>
                                    <th>"作成日"</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>{rows}</tbody>
                        </table>
                    </div>
                </section>
            </Shell>
        }
    })
}

// ---------- clients ----------

fn client_row_view(c: ClientSummary, csrf: String) -> impl IntoView {
    let is_disabled = c.is_disabled;
    let is_deleted = c.is_deleted;
    let kind = if c.confidential { "confidential" } else { "public" };
    let id_str = c.id.to_string();
    let action_label = if is_disabled { "Enable" } else { "Disable" };
    let action_target = if is_disabled { "false" } else { "true" };
    let disabled_url = format!("/admin/clients/{id_str}/disabled");
    let delete_url = format!("/admin/clients/{id_str}/delete");
    let csrf_disable = csrf.clone();
    let csrf_delete = csrf.clone();
    let scopes_display = if c.allowed_scopes.trim().is_empty() {
        "(any)".to_string()
    } else {
        c.allowed_scopes.clone()
    };
    let logout_count = c.post_logout_redirect_uris.len();
    let logout_display = if logout_count == 0 {
        "(falls back to redirect_uris)".to_string()
    } else {
        format!("{logout_count} URI(s)")
    };

    let status_badge = if is_deleted {
        view! { <span class="badge badge--danger">"deleted"</span> }.into_any()
    } else if is_disabled {
        view! { <span class="badge badge--warn">"disabled"</span> }.into_any()
    } else {
        view! { <span class="badge badge--ok">"active"</span> }.into_any()
    };

    let edit_url = format!("/admin/clients/{id_str}/edit");
    let actions = if is_deleted {
        view! { <td><span class="muted">"-"</span></td> }.into_any()
    } else {
        view! {
            <td>
                <div class="row" style="gap:var(--space-1)">
                    <a href=edit_url class="button secondary">"Edit"</a>
                    <form method="post" action=disabled_url style="display:inline">
                        <input type="hidden" name="_csrf" value=csrf_disable />
                        <input type="hidden" name="disabled" value=action_target />
                        <button type="submit" class="secondary">{action_label}</button>
                    </form>
                    <form method="post" action=delete_url style="display:inline"
                          onsubmit="return confirm('Permanently delete this client and revoke its tokens?');">
                        <input type="hidden" name="_csrf" value=csrf_delete />
                        <button type="submit" class="danger">"Delete"</button>
                    </form>
                </div>
            </td>
        }
        .into_any()
    };

    view! {
        <tr>
            <td>{c.name}</td>
            <td><span class="code">{c.id.to_string()}</span></td>
            <td>{kind}</td>
            <td><span class="code">{scopes_display}</span></td>
            <td class="muted">{logout_display}</td>
            <td>{status_badge}</td>
            {actions}
        </tr>
    }
}

pub fn render_clients(
    clients: Vec<ClientSummary>,
    flash: Option<Flash>,
    new_secret: Option<(String, String)>,
    csrf_token: String,
) -> String {
    render(move || {
        let csrf_for_rows = csrf_token.clone();
        let csrf_for_form = csrf_token.clone();
        let client_count = clients.len();
        let secret_block = new_secret.map(|(cid, sec)| {
            view! {
                <div class="flash warn" role="status">
                    <div class="stack-tight">
                        <strong>"クライアント Secret は今だけ表示されます。安全な場所に保存してください。"</strong>
                        <div>"Client ID: "<span class="code">{cid}</span></div>
                        <div>"Client Secret: "<span class="code">{sec}</span></div>
                    </div>
                </div>
            }
        });
        let rows: Vec<_> = clients
            .into_iter()
            .map(|c| client_row_view(c, csrf_for_rows.clone()))
            .collect();
        view! {
            <Shell title="Clients".to_string() show_nav=true current=Some("clients".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"クライアント管理"</h1>
                        <p class="page-header__lede">
                            "OIDC クライアントの登録と管理。"
                            {format!(" 現在 {client_count} 件。")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}
                {secret_block}

                <section>
                    <h2>"新しいクライアントを登録"</h2>
                    <div class="card">
                        <form method="post" action="/admin/clients" class="stack">
                            <input type="hidden" name="_csrf" value=csrf_for_form />
                            <div class="field">
                                <label for="c-name" class="field__label">"アプリケーション名"</label>
                                <input id="c-name" name="name" type="text" required=true />
                            </div>
                            <div class="field">
                                <label for="c-uris" class="field__label">"Redirect URIs"</label>
                                <textarea id="c-uris" name="redirect_uris" required=true rows="3"></textarea>
                                <span class="field__hint">"1 行に 1 つ。https またはループバックの http のみ。"</span>
                            </div>
                            <div class="field">
                                <label for="c-scopes" class="field__label">"許可スコープ"</label>
                                <input id="c-scopes" name="allowed_scopes" type="text" value="openid profile" />
                                <span class="field__hint">"スペース区切り。デフォルトは openid profile。"</span>
                            </div>
                            <div class="field">
                                <label for="c-logout" class="field__label">"Post-logout redirect URIs(任意)"</label>
                                <textarea id="c-logout" name="post_logout_redirect_uris" rows="2"></textarea>
                                <span class="field__hint">"1 行に 1 つ。"</span>
                            </div>
                            <label class="row" style="gap:var(--space-2)">
                                <input name="confidential" type="checkbox" value="true" checked=true />
                                <span>"Confidential client(client secret を発行する)"</span>
                            </label>
                            <div>
                                <button type="submit">"登録"</button>
                            </div>
                        </form>
                    </div>
                </section>

                <section>
                    <h2>"登録済みクライアント"</h2>
                    <div class="table-wrap">
                        <table>
                            <thead>
                                <tr>
                                    <th>"名前"</th>
                                    <th>"Client ID"</th>
                                    <th>"種別"</th>
                                    <th>"スコープ"</th>
                                    <th>"Logout URIs"</th>
                                    <th>"状態"</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>{rows}</tbody>
                        </table>
                    </div>
                </section>
            </Shell>
        }
    })
}

// ---------- client edit ----------

pub struct ClientEditData {
    pub id: String,
    pub name: String,
    /// Newline-separated for textarea editing.
    pub redirect_uris: Vec<String>,
    /// Space-separated.
    pub allowed_scopes: String,
    pub post_logout_redirect_uris: Vec<String>,
    pub confidential: bool,
    pub is_disabled: bool,
}

pub fn render_client_edit(
    data: ClientEditData,
    flash: Option<Flash>,
    csrf_token: String,
) -> String {
    render(move || {
        let ClientEditData {
            id,
            name,
            redirect_uris,
            allowed_scopes,
            post_logout_redirect_uris,
            confidential,
            is_disabled,
        } = data;
        let post_url = format!("/admin/clients/{id}/edit");
        let kind = if confidential { "confidential" } else { "public" };
        let redirect_uris_value = redirect_uris.join("\n");
        let post_logout_value = post_logout_redirect_uris.join("\n");

        let status_badge = if is_disabled {
            view! { <span class="badge badge--warn">"disabled"</span> }.into_any()
        } else {
            view! { <span class="badge badge--ok">"active"</span> }.into_any()
        };

        view! {
            <Shell title="Edit client".to_string() show_nav=true current=Some("clients".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"クライアントを編集"</h1>
                        <p class="page-header__lede">{name.clone()}</p>
                    </div>
                </header>
                {flash_banner(flash)}

                <div class="card">
                    <h3 class="card__title">"基本情報"</h3>
                    <div class="stack-tight muted">
                        <div>"Client ID: "<span class="code">{id.clone()}</span></div>
                        <div class="row" style="gap:var(--space-2)">
                            <span>"種別:"</span>
                            <span class="badge badge--accent">{kind}</span>
                            <span>"状態:"</span>
                            {status_badge}
                        </div>
                    </div>
                    <p class="muted" style="margin-top:var(--space-3)">
                        "Client ID・種別(confidential/public)・client secret は作成時に固定されます。"
                        "これらを変更したい場合は削除して登録し直してください。"
                    </p>
                </div>

                <div class="card">
                    <h3 class="card__title">"設定"</h3>
                    <form method="post" action=post_url class="stack">
                        <input type="hidden" name="_csrf" value=csrf_token />
                        <div class="field">
                            <label for="e-name" class="field__label">"アプリケーション名"</label>
                            <input id="e-name" name="name" type="text" required=true value=name />
                        </div>
                        <div class="field">
                            <label for="e-uris" class="field__label">"Redirect URIs"</label>
                            <textarea id="e-uris" name="redirect_uris" required=true rows="3">
                                {redirect_uris_value}
                            </textarea>
                            <span class="field__hint">"1 行に 1 つ。https またはループバックの http のみ。"</span>
                        </div>
                        <div class="field">
                            <label for="e-scopes" class="field__label">"許可スコープ"</label>
                            <input id="e-scopes" name="allowed_scopes" type="text" value=allowed_scopes />
                            <span class="field__hint">"スペース区切り。空欄=制限なし。"</span>
                        </div>
                        <div class="field">
                            <label for="e-logout" class="field__label">"Post-logout redirect URIs"</label>
                            <textarea id="e-logout" name="post_logout_redirect_uris" rows="2">
                                {post_logout_value}
                            </textarea>
                            <span class="field__hint">"1 行に 1 つ。空欄=Redirect URIs を流用。"</span>
                        </div>
                        <div class="row">
                            <button type="submit">"保存"</button>
                            <a href="/admin/clients" class="button secondary">"キャンセル"</a>
                        </div>
                    </form>
                </div>
            </Shell>
        }
    })
}

// ---------- audit ----------

fn audit_row_view(e: AuditLogEntryDto) -> impl IntoView {
    let result_badge = match e.result.as_str() {
        "ok" => view! { <span class="badge badge--ok">"ok"</span> }.into_any(),
        "fail" | "error" | "denied" => {
            view! { <span class="badge badge--danger">{e.result.clone()}</span> }.into_any()
        }
        _ => view! { <span class="badge">{e.result.clone()}</span> }.into_any(),
    };
    view! {
        <tr>
            <td class="muted">{fmt_time(e.at)}</td>
            <td><span class="code">{e.actor.map(|a| a.to_string()).unwrap_or_else(|| "-".into())}</span></td>
            <td>{e.action}</td>
            <td><span class="code">{e.target.unwrap_or_default()}</span></td>
            <td>{result_badge}</td>
        </tr>
    }
}

pub fn render_audit(entries: Vec<AuditLogEntryDto>, flash: Option<Flash>) -> String {
    render(move || {
        let entry_count = entries.len();
        let rows: Vec<_> = entries.into_iter().map(audit_row_view).collect();
        view! {
            <Shell title="Audit".to_string() show_nav=true current=Some("audit".to_string())>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"監査ログ"</h1>
                        <p class="page-header__lede">
                            "管理操作の履歴(新しい順)。"
                            {format!(" 直近 {entry_count} 件。")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}
                <div class="table-wrap">
                    <table>
                        <thead>
                            <tr>
                                <th>"日時"</th>
                                <th>"実行者"</th>
                                <th>"操作"</th>
                                <th>"対象"</th>
                                <th>"結果"</th>
                            </tr>
                        </thead>
                        <tbody>{rows}</tbody>
                    </table>
                </div>
            </Shell>
        }
    })
}

// ---------- signing keys ----------

fn signing_key_row_view(
    k: sui_id_shared::api::SigningKeySummary,
    csrf: String,
) -> impl IntoView {
    let id_str = k.id.to_string();
    let id_for_url = id_str.clone();
    let id_for_display = id_str.clone();
    let status_badge = if k.is_active {
        view! { <span class="badge badge--ok">"active"</span> }.into_any()
    } else {
        view! { <span class="badge">"retired"</span> }.into_any()
    };
    let rotated = k
        .rotated_at
        .map(fmt_time)
        .unwrap_or_else(|| "-".to_string());
    let delete_url = format!("/admin/signing-keys/{id_for_url}/delete");
    let actions = if k.is_active {
        view! { <td><span class="muted">"(使用中)"</span></td> }.into_any()
    } else {
        view! {
            <td>
                <form method="post" action=delete_url style="display:inline"
                      onsubmit="return confirm('この退役キーを完全削除しますか? まだ有効期限内の発行済みトークンは検証に失敗します。');">
                    <input type="hidden" name="_csrf" value=csrf />
                    <button type="submit" class="danger">"削除"</button>
                </form>
            </td>
        }
        .into_any()
    };
    view! {
        <tr>
            <td><span class="code">{id_for_display}</span></td>
            <td>{k.algorithm}</td>
            <td>{status_badge}</td>
            <td class="muted">{fmt_time(k.created_at)}</td>
            <td class="muted">{rotated}</td>
            {actions}
        </tr>
    }
}

pub fn render_signing_keys(
    keys: Vec<sui_id_shared::api::SigningKeySummary>,
    flash: Option<Flash>,
    csrf_token: String,
) -> String {
    render(move || {
        let csrf_for_rows = csrf_token.clone();
        let csrf_for_form = csrf_token.clone();
        let key_count = keys.len();
        let rows: Vec<_> = keys
            .into_iter()
            .map(|k| signing_key_row_view(k, csrf_for_rows.clone()))
            .collect();
        view! {
            <Shell
                title="Signing keys".to_string()
                show_nav=true
                current=Some("signing-keys".to_string())
            >
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"署名キー"</h1>
                        <p class="page-header__lede">
                            "JWT 署名用 Ed25519 キーの管理。"
                            {format!(" {key_count} 件登録。")}
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}

                <div class="card">
                    <h3 class="card__title">"キーローテーション"</h3>
                    <p class="muted">
                        "ローテーションを実行すると、新しい署名キーが発行され、現行キーは「退役」状態に遷移します。"
                        "退役キーは JWKS に残るため、有効期限内の既発行トークンは検証可能です。"
                        "それらが期限切れになった後、このページから安全に削除できます。"
                    </p>
                    <div class="card__footer">
                        <form method="post" action="/admin/signing-keys/rotate">
                            <input type="hidden" name="_csrf" value=csrf_for_form />
                            <button type="submit">"署名キーをローテーション"</button>
                        </form>
                    </div>
                </div>

                <section>
                    <h2>"全キー"</h2>
                    <div class="table-wrap">
                        <table>
                            <thead>
                                <tr>
                                    <th>"Key ID"</th>
                                    <th>"アルゴリズム"</th>
                                    <th>"状態"</th>
                                    <th>"作成日"</th>
                                    <th>"退役日"</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>{rows}</tbody>
                        </table>
                    </div>
                </section>
            </Shell>
        }
    })
}

// ---------- error ----------

pub fn render_error(title: String, message: String, request_id: String) -> String {
    render(move || {
        let title2 = title.clone();
        view! {
            <crate::layout::AuthShell title=title.clone()>
                <h1>{title2}</h1>
                <div class="flash error" role="alert">{message}</div>
                <p class="muted">
                    "管理者に連絡する場合、以下の ID をお伝えください: "
                    <span class="code">{request_id}</span>
                </p>
                <p>
                    <a href="/" class="button secondary">"ホームへ戻る"</a>
                </p>
            </crate::layout::AuthShell>
        }
    })
}

// ---------- /me/security ----------
//
// Self-service security overview for the signed-in user. Shows where
// they are signed in, lets them revoke individual sessions or sign out
// everywhere else, and surfaces a user-scoped activity timeline so
// they have a chance to notice unusual events on their own account
// without an operator having to escalate.
//
// MFA management itself stays on `/admin/profile` (which is
// misleadingly named — it's "user profile", and a non-admin user can
// reach it the same way; the page does not require admin). We link
// to it from here rather than re-implement.

pub struct MeSecurityData {
    pub username: String,
    pub is_admin: bool,
    /// Whether the user has TOTP enrolled.
    pub totp_enabled: bool,
    /// Number of active WebAuthn passkeys.
    pub passkey_count: usize,
    /// Identifier of the session that issued the current request.
    /// Used to mark "this is you" in the session list and to keep it
    /// alive when the user clicks "sign out everywhere else".
    pub current_session_id: String,
    pub sessions: Vec<MeSessionDescriptor>,
    pub recent_events: Vec<MeAuditEntry>,
}

pub struct MeSessionDescriptor {
    pub id: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub expires_at: chrono::DateTime<chrono::Utc>,
    /// Comma-separated human display: "password", "password + TOTP", etc.
    pub auth_methods: String,
    pub is_current: bool,
}

pub struct MeAuditEntry {
    pub at: chrono::DateTime<chrono::Utc>,
    pub action: String,
    pub result: String,
    pub note: Option<String>,
}

pub fn render_me_security(
    data: MeSecurityData,
    flash: Option<Flash>,
    csrf_token: String,
) -> String {
    render(move || {
        let MeSecurityData {
            username,
            is_admin,
            totp_enabled,
            passkey_count,
            current_session_id,
            sessions,
            recent_events,
        } = data;

        let csrf_for_revoke_others = csrf_token.clone();

        // Session table rows. Each non-current row gets its own
        // mini-form so a user can revoke that specific entry.
        let session_rows: Vec<_> = sessions
            .into_iter()
            .map(|s| {
                let MeSessionDescriptor {
                    id,
                    created_at,
                    expires_at,
                    auth_methods,
                    is_current,
                } = s;
                let when = created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string();
                let until = expires_at.format("%Y-%m-%d %H:%M:%S UTC").to_string();
                let action_cell = if is_current {
                    view! {
                        <td>
                            <span class="badge badge--accent">"current session"</span>
                        </td>
                    }
                    .into_any()
                } else {
                    let csrf_for_row = csrf_token.clone();
                    let post_url = format!("/me/security/sessions/{id}/revoke");
                    view! {
                        <td>
                            <form method="post" action=post_url style="display:inline"
                                  onsubmit="return confirm('このセッションをサインアウトしますか?');">
                                <input type="hidden" name="_csrf" value=csrf_for_row />
                                <button type="submit" class="secondary">"Revoke"</button>
                            </form>
                        </td>
                    }
                    .into_any()
                };
                view! {
                    <tr>
                        <td class="muted">{when}</td>
                        <td class="muted">{until}</td>
                        <td>{auth_methods}</td>
                        {action_cell}
                    </tr>
                }
            })
            .collect();

        // Activity timeline.
        let event_rows: Vec<_> = recent_events
            .into_iter()
            .map(|e| {
                let MeAuditEntry {
                    at,
                    action,
                    result,
                    note,
                } = e;
                let when = at.format("%Y-%m-%d %H:%M:%S UTC").to_string();
                let note_str = note.unwrap_or_default();
                let result_badge = match result.as_str() {
                    "ok" => view! { <span class="badge badge--ok">"ok"</span> }.into_any(),
                    "fail" | "error" | "denied" => {
                        view! { <span class="badge badge--danger">{result.clone()}</span> }
                            .into_any()
                    }
                    _ => view! { <span class="badge">{result.clone()}</span> }.into_any(),
                };
                view! {
                    <tr>
                        <td class="muted">{when}</td>
                        <td><span class="code">{action}</span></td>
                        <td>{result_badge}</td>
                        <td class="muted">{note_str}</td>
                    </tr>
                }
            })
            .collect();

        let admin_link = is_admin.then(|| {
            view! {
                <p class="muted">
                    <a href="/admin">"管理画面を開く →"</a>
                </p>
            }
        });

        let mfa_summary = if totp_enabled || passkey_count > 0 {
            let parts = {
                let mut v = Vec::<String>::new();
                if totp_enabled {
                    v.push("認証アプリ".into());
                }
                if passkey_count > 0 {
                    v.push(format!("パスキー {passkey_count} 件"));
                }
                v.join(" / ")
            };
            view! {
                <p>
                    "状態:"
                    <span class="badge badge--ok" style="margin-left:var(--space-1)">"有効"</span>
                    <span class="muted" style="margin-left:var(--space-2)">{parts}</span>
                </p>
            }
            .into_any()
        } else {
            view! {
                <div class="flash warn" role="status">
                    <div class="stack-tight">
                        <strong>"2 段階認証が無効です。"</strong>
                        <p class="muted" style="margin:0">
                            "現在このアカウントはパスワードのみで保護されています。"
                            "パスキーまたは認証アプリの登録を強く推奨します。"
                        </p>
                    </div>
                </div>
            }
            .into_any()
        };

        view! {
            <Shell title="Security".to_owned() show_nav=false current=None>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"アカウントセキュリティ"</h1>
                        <p class="page-header__lede">
                            <strong>{username}</strong>" としてサインイン中。"
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}
                {admin_link}

                <section>
                    <h2>"2 段階認証"</h2>
                    <div class="card">
                        {mfa_summary}
                        <div class="card__footer">
                            <a href="/admin/profile" class="button secondary">
                                "認証手段を管理"
                            </a>
                            <a href="/me/security/password" class="button secondary">
                                "パスワードを変更"
                            </a>
                        </div>
                    </div>
                </section>

                <section>
                    <h2>"サインイン中の場所"</h2>
                    <p class="muted">
                        "1 行が 1 つのブラウザセッションです。"
                        "Revoke を押すとそのブラウザは即座にサインアウトされます。"
                        "「current session」とマークされているのは現在使用中のセッションです。"
                    </p>
                    <div class="table-wrap">
                        <table>
                            <thead>
                                <tr>
                                    <th>"開始日時"</th>
                                    <th>"期限"</th>
                                    <th>"要素"</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>{session_rows}</tbody>
                        </table>
                    </div>
                    <form method="post" action="/me/security/sessions/revoke-all-others"
                          style="margin-top:var(--space-3)"
                          onsubmit="return confirm('現在のセッション以外をすべてサインアウトしますか?');">
                        <input type="hidden" name="_csrf" value=csrf_for_revoke_others />
                        <input type="hidden" name="current_session" value=current_session_id />
                        <button type="submit" class="secondary">
                            "他のすべてのセッションをサインアウト"
                        </button>
                    </form>
                </section>

                <section>
                    <h2>"最近のアクティビティ"</h2>
                    <p class="muted">
                        "あなたのアカウントに関わる認証および管理イベントです。"
                        "心当たりのない操作がある場合は、すぐにパスワードを変更し、他のセッションをサインアウトしてください。"
                    </p>
                    <div class="table-wrap">
                        <table>
                            <thead>
                                <tr>
                                    <th>"日時"</th>
                                    <th>"イベント"</th>
                                    <th>"結果"</th>
                                    <th>"備考"</th>
                                </tr>
                            </thead>
                            <tbody>{event_rows}</tbody>
                        </table>
                    </div>
                </section>
            </Shell>
        }
    })
}

// ---------- /me/security/password ----------

pub struct PasswordChangeData {
    pub username: String,
    /// Pre-filled checked value of "sign out other sessions". The
    /// caller hands it in so a re-render after a validation error
    /// keeps the user's previous choice.
    pub revoke_others_default: bool,
}

pub fn render_password_change(
    data: PasswordChangeData,
    flash: Option<Flash>,
    csrf_token: String,
) -> String {
    render(move || {
        let PasswordChangeData {
            username,
            revoke_others_default,
        } = data;
        let revoke_attr = if revoke_others_default { Some("") } else { None };
        view! {
            <Shell title="Change password".to_owned() show_nav=false current=None>
                <header class="page-header">
                    <div>
                        <h1 class="page-header__title">"パスワードを変更"</h1>
                        <p class="page-header__lede">
                            <strong>{username}</strong>" としてサインイン中。"
                        </p>
                    </div>
                </header>
                {flash_banner(flash)}

                <div class="card" style="max-width:var(--content-narrow-width)">
                    <form method="post" action="/me/security/password"
                          autocomplete="off" class="stack">
                        <input type="hidden" name="_csrf" value=csrf_token />

                        <div class="field">
                            <label for="current_password" class="field__label">"現在のパスワード"</label>
                            <input type="password" id="current_password" name="current_password"
                                   required autocomplete="current-password" />
                        </div>

                        <div class="field">
                            <label for="new_password" class="field__label">"新しいパスワード"</label>
                            <input type="password" id="new_password" name="new_password"
                                   required autocomplete="new-password" minlength="12" />
                            <span class="field__hint">
                                "12 文字以上。短く複雑なパスワードよりも、長くランダムなフレーズの方が安全です。"
                            </span>
                        </div>

                        <div class="field">
                            <label for="confirm_password" class="field__label">"新しいパスワード(確認)"</label>
                            <input type="password" id="confirm_password" name="confirm_password"
                                   required autocomplete="new-password" minlength="12" />
                        </div>

                        <div class="field">
                            <label class="row" style="gap:var(--space-2)">
                                <input type="checkbox" name="revoke_others" value="1"
                                       checked=revoke_attr />
                                <span>"パスワード変更後、他のブラウザ/アプリをサインアウトする"</span>
                            </label>
                            <span class="field__hint">
                                "推奨。既存のセッションやリフレッシュトークンが無効化され、新しいパスワードでの再サインインが必要になります。"
                            </span>
                        </div>

                        <div class="row">
                            <button type="submit">"パスワードを変更"</button>
                            <a href="/me/security" class="button secondary">"キャンセル"</a>
                        </div>
                    </form>
                </div>
            </Shell>
        }
    })
}
