//! Login flow components

// use anyhow::Error;
use gloo::console;
use kanidm_proto::v1::{
    AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
    AuthStep,
};
use kanidm_proto::webauthn::PublicKeyCredential;
use kanidmd_web_ui_shared::utils::{autofocus, do_footer, window};
use kanidmd_web_ui_shared::{
    add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus,
};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::CredentialRequestOptions;
use yew::prelude::*;
use yew::virtual_dom::VNode;

use kanidmd_web_ui_shared::constants::{
    CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD, CSS_ALERT_DANGER,
    URL_USER_HOME,
};
use kanidmd_web_ui_shared::models::{
    self, clear_bearer_token, get_bearer_token, get_login_hint, pop_login_hint,
    pop_login_remember_me, pop_return_location, push_login_remember_me, set_bearer_token,
};
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
use serde::Serialize;
use yew_router::BrowserRouter;

#[derive(Clone)]
pub struct LoginApp {
    state: LoginState,
}

impl Default for LoginApp {
    fn default() -> Self {
        Self {
            state: LoginState::InitLogin {
                enable: true,
                remember_me: false,
                username: String::new(),
            },
        }
    }
}

#[derive(PartialEq, Clone, Copy)]
pub enum LoginWorkflow {
    Login,
    Reauth,
}

impl std::fmt::Display for LoginWorkflow {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            LoginWorkflow::Login => "LoginWorkflow::Login",
            LoginWorkflow::Reauth => "LoginWorkflow::Reauth",
        })
    }
}

impl Default for LoginWorkflow {
    fn default() -> Self {
        Self::Login
    }
}

#[derive(PartialEq, Properties, Default)]
pub struct LoginAppProps {
    pub workflow: LoginWorkflow,
}

#[derive(PartialEq, Clone, Copy)]
enum TotpState {
    Enabled,
    Disabled,
    Invalid,
}

#[derive(Clone)]
enum LoginState {
    InitLogin {
        enable: bool,
        remember_me: bool,
        username: String,
    },
    InitReauth {
        enable: bool,
        spn: String,
    },
    // Select between different cred types, either password (and MFA) or Passkey
    Select(Vec<AuthMech>),
    // The choices of authentication mechanism.
    Continue(Vec<AuthAllowed>),
    // The different methods
    Password(bool),
    BackupCode(bool),
    Totp(TotpState),
    Passkey(CredentialRequestOptions),
    SecurityKey(CredentialRequestOptions),
    // Error, state handling.
    Error {
        emsg: String,
        kopid: Option<String>,
    },
    UnknownUser,
    Denied(String),
    Authenticated,
}

pub enum LoginAppMsg {
    Restart,
    Begin,
    PasswordSubmit,
    BackupCodeSubmit,
    TotpSubmit,
    PasskeySubmit(PublicKeyCredential),
    SecurityKeySubmit(PublicKeyCredential),
    Start(AuthResponse),
    Next(AuthResponse),
    Continue(usize),
    Select(usize),
    // DoNothing,
    UnknownUser,
    AlreadyAuthenticated,
    Error { emsg: String, kopid: Option<String> },
}

impl From<FetchError> for LoginAppMsg {
    fn from(fe: FetchError) -> Self {
        LoginAppMsg::Error {
            emsg: fe.as_string(),
            kopid: None,
        }
    }
}

impl From<SessionStatus> for LoginAppMsg {
    fn from(s: SessionStatus) -> Self {
        match s {
            SessionStatus::TokenValid => LoginAppMsg::AlreadyAuthenticated,
            SessionStatus::LoginRequired => LoginAppMsg::Begin,
            SessionStatus::Error { emsg, kopid } => LoginAppMsg::Error { emsg, kopid },
        }
    }
}

impl LoginApp {
    /// Validate that the current auth token's OK
    async fn fetch_session_valid() -> Result<LoginAppMsg, FetchError> {
        fetch_session_valid().await.map(|v| v.into())
    }

    async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> {
        let authreq = AuthRequest {
            step: AuthStep::Init2 {
                username,
                issue: AuthIssueSession::Token,
                privileged: false,
            },
        };
        let req_jsvalue = authreq
            .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
            .expect("Failed to serialise request");
        let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");

        let (kopid, status, value, _) =
            do_request("/v1/auth", RequestMethod::POST, Some(req_jsvalue)).await?;

        if status == 200 {
            let state: AuthResponse = serde_wasm_bindgen::from_value(value)
                .expect_throw("Invalid response type - auth_init::AuthResponse");
            Ok(LoginAppMsg::Start(state))
        } else if status == 404 {
            console::error!(format!(
                "User not found: {:?}. Operation ID: {:?}",
                value.as_string().unwrap_or_default(),
                kopid
            ));
            Ok(LoginAppMsg::UnknownUser)
        } else {
            let emsg = value.as_string().unwrap_or_default();
            Ok(LoginAppMsg::Error { emsg, kopid })
        }
    }

    async fn reauth_init() -> Result<LoginAppMsg, FetchError> {
        let issue = AuthIssueSession::Token;
        let req_jsvalue = issue
            .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
            .expect("Failed to serialise request");
        let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");
        let url = "/v1/reauth";
        let (kopid, status, value, _) =
            do_request(url, RequestMethod::POST, Some(req_jsvalue)).await?;

        if status == 200 {
            let state: AuthResponse = serde_wasm_bindgen::from_value(value)
                .expect_throw("Invalid response type during reauth_init::AuthResponse");
            Ok(LoginAppMsg::Next(state))
        } else if status == 404 {
            console::error!(format!(
                "User not found during reauth_init: {:?}. Operation ID: {:?}",
                value.as_string(),
                kopid
            ));
            Ok(LoginAppMsg::UnknownUser)
        } else {
            let emsg = value.as_string().unwrap_or_default();
            Ok(LoginAppMsg::Error { emsg, kopid })
        }
    }

    async fn auth_step(authreq: AuthRequest) -> Result<LoginAppMsg, FetchError> {
        let req_jsvalue = authreq
            .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
            .expect("Failed to serialise request");
        let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");

        let (kopid, status, value, _) =
            do_request("/v1/auth", RequestMethod::POST, Some(req_jsvalue)).await?;

        if status == 200 {
            let state: AuthResponse = serde_wasm_bindgen::from_value(value)
                .map_err(|e| {
                    console::error!(format!("auth_step::AuthResponse: {:?}", e));
                    e
                })
                .expect_throw("Invalid response type - auth_step::AuthResponse");
            Ok(LoginAppMsg::Next(state))
        } else {
            let emsg = value.as_string()
                .unwrap_or_else(|| "Unhandled error, please report this along with the operation ID below to your administrator. 😔".to_string());
            Ok(LoginAppMsg::Error { emsg, kopid })
        }
    }

    /// Renders the "Start again" button
    fn button_start_again(&self, ctx: &Context<Self>) -> VNode {
        html! {
            <div class="col-md-auto text-center">
                <button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >
                {" Start Again "}
                </button>
            </div>
        }
    }

    fn render_auth_allowed(&self, ctx: &Context<Self>, idx: usize, allow: &AuthAllowed) -> Html {
        html! {
            <li class="text-center mb-2">
                <button
                    type="button"
                    class={CLASS_BUTTON_DARK}
                    onclick={ ctx.link().callback(move |_| LoginAppMsg::Continue(idx)) }
                >{ allow.to_string() }</button>
            </li>
        }
    }

    fn render_mech_select(&self, ctx: &Context<Self>, idx: usize, allow: &AuthMech) -> Html {
        html! {
            <li class="text-center mb-2">
                <button
                    type="button"
                    class={CLASS_BUTTON_DARK}
                    onclick={ ctx.link().callback(move |_| LoginAppMsg::Select(idx)) }
                >{ allow.to_string() }</button>
            </li>
        }
    }

    /// shows an error-alert in a bootstrap alert container
    fn do_alert_error(
        &self,
        alert_title: &str,
        alert_message: Option<&str>,
        ctx: &Context<Self>,
    ) -> VNode {
        html! {
        <div class="container">
            <div class="row justify-content-md-center">
                <div class={CSS_ALERT_DANGER} role="alert">
                    <p><strong>{ alert_title }</strong></p>
                    if let Some(value) = alert_message {
                        <p>{ value }</p>
                    }
                </div>
                { self.button_start_again(ctx) }
            </div>
        </div>
        }
    }

    fn view_state(&self, ctx: &Context<Self>) -> Html {
        match &self.state {
            LoginState::InitLogin {
                enable,
                remember_me,
                username,
            } => {
                let username = username.clone();

                html! {
                    <>
                    <div class="container">
                        <label for="username" class="form-label">{ "Username" }</label>
                        <form id="login"
                        onsubmit={ ctx.link().callback(|e: SubmitEvent| {
                            #[cfg(debug_assertions)]
                            console::debug!("login::view_state -> Init - prevent_default()".to_string());
                            e.prevent_default();
                            LoginAppMsg::Begin
                        } ) }
                        >
                        <div class={CLASS_DIV_LOGIN_FIELD}>
                            <input
                                autofocus=true
                                class="autofocus form-control"
                                disabled={ !enable }
                                id="username"
                                name="username"
                                type="text"
                                autocomplete="username"
                                value={ username }
                                required=true
                            />
                        </div>

                        <div class="mb-3 form-check form-switch">
                            <input
                                type="checkbox"
                                class="form-check-input"
                                role="switch"
                                id="remember_me_check"
                                disabled={ !enable }
                                checked={ *remember_me }
                            />
                            <label class="form-check-label" for="remember_me_check">{ "Remember my Username" }</label>
                        </div>

                        <div class={CLASS_DIV_LOGIN_BUTTON}>
                            <button
                                type="submit"
                                class={CLASS_BUTTON_DARK}
                                disabled={ !enable }
                            >{" Begin "}</button>
                        </div>
                        </form>
                    </div>
                    </>
                }
            }
            LoginState::InitReauth { enable, spn } => {
                let msg = format!("Reauthenticate as {} to continue", spn);
                html! {
                    <>
                    <div class="container">
                        <p>{ msg }</p>
                        <form id="login"
                        onsubmit={ ctx.link().callback(|e: SubmitEvent| {
                            #[cfg(debug_assertions)]
                            console::debug!("login::view_state -> Init - prevent_default()".to_string());
                            e.prevent_default();
                            LoginAppMsg::Begin
                        } ) }
                        >
                        <div class={CLASS_DIV_LOGIN_BUTTON}>
                            <button
                                type="submit"
                                class="autofocus form-control btn btn-dark"
                                autofocus=true
                                id="begin"
                                disabled={ !enable }
                            >{" Begin "}</button>
                        </div>
                        </form>
                    </div>
                    </>
                }
            }
            // Selecting between password (and MFA) or Passkey
            LoginState::Select(mechs) => {
                html! {
                    <>
                    <div class="container">
                        <p>
                        {" Which credential would you like to use? "}
                        </p>
                    </div>
                    <div class="container">
                        <ul class="list-unstyled">
                            { for mechs.iter()
                                .enumerate()
                                .map(|(idx, mech)| self.render_mech_select(ctx, idx, mech)) }
                        </ul>
                    </div>
                    </>
                }
            }
            LoginState::Continue(allowed) => {
                html! {
                    <>
                    <div class="container">
                        <p>
                        {"Choose how to proceed:"}
                        </p>
                    </div>
                    <div class="container">
                        <ul class="list-unstyled">
                            { for allowed.iter()
                                .enumerate()
                                .map(|(idx, allow)| self.render_auth_allowed(ctx, idx, allow)) }
                        </ul>
                    </div>
                    </>
                }
            }
            LoginState::Password(enable) => {
                html! {
                    <>
                    <div class="container">
                        <label for="password" class="form-label">{ "Password" }</label>
                        <form id="login"
                            onsubmit={ ctx.link().callback(|e: SubmitEvent| {
                                console::debug!("login::view_state -> Password - prevent_default()".to_string());
                                e.prevent_default();
                                LoginAppMsg::PasswordSubmit
                            } ) }
                        >
                        <div>
                            <input hidden=true type="text" autocomplete="username" />
                        </div>
                        <div class={CLASS_DIV_LOGIN_FIELD}>
                            <input
                                autofocus=true
                                class="autofocus form-control"
                                disabled={ !enable }
                                id="password"
                                name="password"
                                type="password"
                                autocomplete="current-password"
                                value=""
                            />
                            </div>
                            <div class={CLASS_DIV_LOGIN_BUTTON}>
                                <button type="submit" class={CLASS_BUTTON_DARK} disabled={ !enable }>{ "Submit" }</button>
                            </div>
                        </form>
                    </div>
                    </>
                }
            }
            LoginState::BackupCode(enable) => {
                html! {
                    <>

                    <div class="container">
                        <label for="backup_code" class="form-label">
                        {"Backup Code"}
                        </label>
                        <form id="login"
                            onsubmit={ ctx.link().callback(|e: SubmitEvent| {
                                console::debug!("login::view_state -> BackupCode - prevent_default()".to_string());
                                e.prevent_default();
                                LoginAppMsg::BackupCodeSubmit
                            } ) }
                        >
                        <div class={CLASS_DIV_LOGIN_FIELD}>
                            <input
                                autofocus=true
                                class="autofocus form-control"
                                disabled={ !enable }
                                id="backup_code"
                                name="backup_code"
                                type="text"
                                autocomplete="off"
                                value=""
                            />
                            </div>
                            <div class={CLASS_DIV_LOGIN_BUTTON}>
                                <button type="submit" class={CLASS_BUTTON_DARK}>{" Submit "}</button>
                            </div>
                        </form>
                    </div>
                    </>
                }
            }
            LoginState::Totp(state) => {
                html! {
                    <>
                    <div class="container">
                        <label for="totp" class="form-label">{"TOTP"}</label>
                        <form id="login"
                            onsubmit={ ctx.link().callback(|e: SubmitEvent| {
                                console::debug!("login::view_state -> Totp - prevent_default()".to_string());
                                e.prevent_default();
                                LoginAppMsg::TotpSubmit
                            } ) }
                        >
                        <div class={CLASS_DIV_LOGIN_FIELD}>
                        <input
                            autofocus=true
                            class="autofocus form-control"
                            disabled={ state==&TotpState::Disabled }
                            id="totp"
                            name="totp"
                            type="text"
                            autocomplete="off"
                            value=""
                            />
                        </div>
                            <div class={CLASS_DIV_LOGIN_BUTTON}>
                            <button type="submit" class={CLASS_BUTTON_DARK} disabled={ state==&TotpState::Disabled }>{" Submit "}</button>
                            </div>
                        </form>
                    </div>
                    </>
                }
            }
            LoginState::SecurityKey(challenge) => {
                // Start the navigator parts.
                if let Some(win) = web_sys::window() {
                    let promise = win
                        .navigator()
                        .credentials()
                        .get_with_options(challenge)
                        .expect_throw("Unable to create promise");
                    let fut = JsFuture::from(promise);
                    let linkc = ctx.link().clone();

                    spawn_local(async move {
                        match fut.await {
                            Ok(data) => {
                                let data = PublicKeyCredential::from(
                                    web_sys::PublicKeyCredential::from(data),
                                );
                                linkc.send_message(LoginAppMsg::SecurityKeySubmit(data));
                            }
                            Err(e) => {
                                linkc.send_message(LoginAppMsg::Error {
                                    emsg: format!("{:?}", e),
                                    kopid: None,
                                });
                            }
                        }
                    });
                } else {
                    ctx.link().send_message(LoginAppMsg::Error {
                        emsg: "failed to access navigator credentials".to_string(),
                        kopid: None,
                    });
                };

                html! {
                    <div class="container">
                        <p>
                        {"Security Key"}
                        </p>
                    </div>
                }
            }
            LoginState::Passkey(challenge) => {
                // Start the navigator parts.
                if let Some(win) = web_sys::window() {
                    let promise = win
                        .navigator()
                        .credentials()
                        .get_with_options(challenge)
                        .expect_throw("Unable to create promise");
                    let fut = JsFuture::from(promise);
                    let linkc = ctx.link().clone();

                    spawn_local(async move {
                        match fut.await {
                            Ok(data) => {
                                let data = PublicKeyCredential::from(
                                    web_sys::PublicKeyCredential::from(data),
                                );
                                linkc.send_message(LoginAppMsg::PasskeySubmit(data));
                            }
                            Err(e) => {
                                linkc.send_message(LoginAppMsg::Error {
                                    emsg: format!("{:?}", e),
                                    kopid: None,
                                });
                            }
                        }
                    });
                } else {
                    ctx.link().send_message(LoginAppMsg::Error {
                        emsg: "failed to access navigator credentials".to_string(),
                        kopid: None,
                    });
                };

                html! {
                    <div class="container text-center">
                        <p>
                        {"Prompting for Passkey authentication..."}
                        </p>
                    </div>
                }
            }
            LoginState::Authenticated => {
                let loc = pop_return_location();
                // redirect to the "return location"
                #[cfg(debug_assertions)]
                console::debug!(format!("authenticated, trying to go to {:?}", loc));

                let window = gloo_utils::window();
                window
                    .location()
                    .set_href(&loc)
                    .expect_throw(&format!("failed to set location to {}", loc));
                // this isn't likely to actually render but we might as well...
                html! {
                    <div class="alert alert-success">
                        <h3>{ "Login Success 🎉" }</h3>
                        <a href={loc}>{"Click here to continue if you aren't redirected..."}</a>
                    </div>
                }
            }
            LoginState::Denied(msg) => {
                self.do_alert_error("Authentication Denied", Some(msg.as_str()), ctx)
            }
            LoginState::UnknownUser => {
                self.do_alert_error("Username not found", Some("Please try again"), ctx)
            }
            LoginState::Error { emsg, kopid } => self.do_alert_error(
                "An error has occurred 😔 ",
                Some(
                    format!(
                        "{}\n\n{}",
                        emsg.as_str(),
                        if let Some(opid) = kopid.as_ref() {
                            format!("Operation ID: {}", opid.clone())
                        } else {
                            "Error occurred client-side.".to_string()
                        }
                    )
                    .as_str(),
                ),
                ctx,
            ),
        }
    }
}

impl Component for LoginApp {
    type Message = LoginAppMsg;
    type Properties = LoginAppProps;

    fn create(ctx: &Context<Self>) -> Self {
        let workflow = ctx.props().workflow.to_owned();

        #[cfg(debug_assertions)]
        console::debug!(&format!("login::create -> workflow: {}", workflow));

        let state = match workflow {
            LoginWorkflow::Login => {
                // let's check if they're already authenticated!
                if get_bearer_token().is_some() {
                    ctx.link().send_future(async {
                        match Self::fetch_session_valid().await {
                            Ok(_) => {
                                console::info!(
                                    "Already logged in, redirecting to user home page"
                                );
                                let window = gloo_utils::window();
                                window
                                    .location()
                                    .set_href(URL_USER_HOME)
                                    .expect_throw(&["failed to set location to ", URL_USER_HOME].concat());

                                LoginAppMsg::AlreadyAuthenticated
                                }
                            Err(v) => {
                                console::error!(
                                    "Error checking session validity, clearing token and returning to login page: {:?}",
                                    v.as_string()
                                );
                                clear_bearer_token();
                                LoginAppMsg::Restart
                            }
                        }
                    });
                }

                if get_bearer_token().is_some() {
                    // We're already logged in, so we're going to redirect to the apps page.
                    return Self::default();
                }

                // Do we have a login hint?
                let (username, remember_me) = get_login_hint()
                    .map(|user| (user, false))
                    .or_else(|| models::get_login_remember_me().map(|user| (user, true)))
                    .unwrap_or_default();

                LoginState::InitLogin {
                    enable: true,
                    remember_me,
                    username,
                }
            }
            LoginWorkflow::Reauth => match get_login_hint() {
                Some(spn) => LoginState::InitReauth { enable: true, spn },
                None => LoginState::Error {
                    emsg: "Client Error - No login hint available".to_string(),
                    kopid: None,
                },
            },
        };

        add_body_form_classes!();

        LoginApp { state }
    }

    fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
        false
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            LoginAppMsg::AlreadyAuthenticated => {
                #[cfg(debug_assertions)]
                console::info!("User is already authenticated, redirecting to user home");
                window().location().set_href(URL_USER_HOME).unwrap_throw();
                // no need to render it, we're going away now
                false
            }
            LoginAppMsg::Restart => {
                // Clear any leftover input. Reset to the remembered username if any.
                match &ctx.props().workflow {
                    LoginWorkflow::Login => {
                        let (username, remember_me) = get_login_hint()
                            .map(|user| (user, false))
                            .or_else(|| models::get_login_remember_me().map(|user| (user, true)))
                            .unwrap_or_default();

                        self.state = LoginState::InitLogin {
                            enable: true,
                            remember_me,
                            username,
                        };
                    }
                    LoginWorkflow::Reauth => {
                        match get_login_hint() {
                            Some(spn) => {
                                self.state = LoginState::InitReauth {
                                    enable: true,
                                    spn: spn.clone(),
                                };
                                LoginState::InitReauth { enable: true, spn }
                            }
                            None => LoginState::Error {
                                emsg: "Client Error - No login hint available".to_string(),
                                kopid: None,
                            },
                        };
                    }
                }
                true
            }
            LoginAppMsg::Begin => {
                match &ctx.props().workflow {
                    LoginWorkflow::Login => {
                        let username = utils::get_value_from_element_id("username")
                            .unwrap_or_default()
                            .trim()
                            .to_lowercase();

                        #[cfg(debug_assertions)]
                        console::debug!(format!("begin for username -> {:?}", username));

                        // If the remember-me was checked, stash it here.
                        // If it was false, clear existing data.

                        let remember_me = if utils::get_inputelement_by_id("remember_me_check")
                            .map(|element| element.checked())
                            .unwrap_or(false)
                        {
                            push_login_remember_me(username.clone());
                            true
                        } else {
                            pop_login_remember_me();
                            false
                        };

                        #[cfg(debug_assertions)]
                        console::debug!(format!("begin remember_me -> {:?}", remember_me));

                        let username_clone = username.clone();

                        ctx.link().send_future(async {
                            match Self::auth_init(username_clone).await {
                                Ok(v) => v,
                                Err(v) => v.into(),
                            }
                        });

                        self.state = LoginState::InitLogin {
                            enable: false,
                            remember_me,
                            username,
                        };
                    }
                    LoginWorkflow::Reauth => {
                        ctx.link().send_future(async {
                            match Self::reauth_init().await {
                                Ok(v) => v,
                                Err(v) => v.into(),
                            }
                        });

                        self.state = match get_login_hint() {
                            Some(spn) => LoginState::InitReauth { enable: false, spn },
                            None => LoginState::Error {
                                emsg: "Client Error - No login hint available".to_string(),
                                kopid: None,
                            },
                        };
                    }
                }
                true
            }
            LoginAppMsg::PasswordSubmit => {
                let password = utils::get_value_from_element_id("password").unwrap_or_default();

                #[cfg(debug_assertions)]
                console::debug!("password step".to_string());
                // Disable the button?
                self.state = LoginState::Password(false);
                let authreq = AuthRequest {
                    step: AuthStep::Cred(AuthCredential::Password(password)),
                };
                ctx.link().send_future(async {
                    match Self::auth_step(authreq).await {
                        Ok(v) => v,
                        Err(v) => v.into(),
                    }
                });
                true
            }
            LoginAppMsg::BackupCodeSubmit => {
                let backup_code =
                    utils::get_value_from_element_id("backup_code").unwrap_or_default();

                #[cfg(debug_assertions)]
                console::debug!("backup_code".to_string());
                // Disable the button?
                self.state = LoginState::BackupCode(false);
                let authreq = AuthRequest {
                    step: AuthStep::Cred(AuthCredential::BackupCode(backup_code)),
                };
                ctx.link().send_future(async {
                    match Self::auth_step(authreq).await {
                        Ok(v) => v,
                        Err(v) => v.into(),
                    }
                });
                true
            }
            LoginAppMsg::TotpSubmit => {
                let totp_str = utils::get_value_from_element_id("totp").unwrap_or_default();

                #[cfg(debug_assertions)]
                console::debug!("totp".to_string());
                // Disable the button?
                match totp_str.parse::<u32>() {
                    Ok(totp) => {
                        self.state = LoginState::Totp(TotpState::Disabled);
                        let authreq = AuthRequest {
                            step: AuthStep::Cred(AuthCredential::Totp(totp)),
                        };
                        ctx.link().send_future(async {
                            match Self::auth_step(authreq).await {
                                Ok(v) => v,
                                Err(v) => v.into(),
                            }
                        });
                    }
                    Err(_) => {
                        self.state = LoginState::Totp(TotpState::Invalid);
                    }
                }

                true
            }
            LoginAppMsg::SecurityKeySubmit(resp) => {
                #[cfg(debug_assertions)]
                console::debug!("At securitykey step".to_string());
                let authreq = AuthRequest {
                    step: AuthStep::Cred(AuthCredential::SecurityKey(Box::new(resp))),
                };
                ctx.link().send_future(async {
                    match Self::auth_step(authreq).await {
                        Ok(v) => v,
                        Err(v) => v.into(),
                    }
                });
                // Do not submit here, we need to wait for the next ui transition.
                false
            }
            LoginAppMsg::PasskeySubmit(resp) => {
                #[cfg(debug_assertions)]
                console::debug!("At passkey step".to_string());
                let authreq = AuthRequest {
                    step: AuthStep::Cred(AuthCredential::Passkey(Box::new(resp))),
                };
                ctx.link().send_future(async {
                    match Self::auth_step(authreq).await {
                        Ok(v) => v,
                        Err(v) => v.into(),
                    }
                });
                // Do not submit here, we need to wait for the next ui transition.
                false
            }
            LoginAppMsg::Start(resp) => {
                // Clear any leftover input
                #[cfg(debug_assertions)]
                console::debug!(format!("start -> {:?}", resp));
                match resp.state {
                    AuthState::Choose(mut mechs) => {
                        if mechs.len() == 1 {
                            // If it's only one mech, just submit that.
                            let mech = mechs.pop().expect_throw("Memory corruption occurred");
                            let authreq = AuthRequest {
                                step: AuthStep::Begin(mech),
                            };
                            ctx.link().send_future(async {
                                match Self::auth_step(authreq).await {
                                    Ok(v) => v,
                                    Err(v) => v.into(),
                                }
                            });
                            // We do NOT need to change state or redraw
                            false
                        } else {
                            #[cfg(debug_assertions)]
                            console::debug!("multiple mechs exist".to_string());
                            self.state = LoginState::Select(mechs);
                            true
                        }
                    }
                    AuthState::Denied(reason) => {
                        #[cfg(debug_assertions)]
                        console::debug!(format!("denied -> {:?}", reason));
                        self.state = LoginState::Denied(reason);
                        true
                    }
                    _ => {
                        console::error!("invalid state transition".to_string());
                        self.state = LoginState::Error {
                            emsg: "Invalid UI State Transition".to_string(),
                            kopid: None,
                        };
                        true
                    }
                }
            }
            LoginAppMsg::Select(idx) => {
                #[cfg(debug_assertions)]
                console::debug!(format!("chose -> {:?}", idx));
                match &self.state {
                    LoginState::Select(allowed) => match allowed.get(idx) {
                        Some(mech) => {
                            let authreq = AuthRequest {
                                step: AuthStep::Begin(mech.clone()),
                            };
                            ctx.link().send_future(async {
                                match Self::auth_step(authreq).await {
                                    Ok(v) => v,
                                    Err(v) => v.into(),
                                }
                            });
                        }
                        None => {
                            console::error!("invalid allowed mech idx".to_string());
                            self.state = LoginState::Error {
                                emsg: "Invalid Continue Index".to_string(),
                                kopid: None,
                            };
                        }
                    },
                    _ => {
                        console::error!("invalid state transition".to_string());
                        self.state = LoginState::Error {
                            emsg: "Invalid UI State Transition".to_string(),
                            kopid: None,
                        };
                    }
                };
                true
            }
            LoginAppMsg::Next(resp) => {
                // Clear any leftover input
                #[cfg(debug_assertions)]
                console::debug!(format!("next -> {:?}", resp));

                // Based on the state we have, we need to chose our steps.
                match resp.state {
                    AuthState::Choose(_mechs) => {
                        console::error!("invalid state transition".to_string());
                        self.state = LoginState::Error {
                            emsg: "Invalid UI State Transition".to_string(),
                            kopid: None,
                        };
                        true
                    }
                    AuthState::Continue(mut allowed) => {
                        if allowed.len() == 1 {
                            // If there is only one, change our state for that input type.
                            match allowed.pop().expect_throw("Memory corruption occurred") {
                                AuthAllowed::Anonymous => {
                                    // Just submit this.
                                }
                                AuthAllowed::Password => {
                                    // Go to the password view.
                                    self.state = LoginState::Password(true);
                                }
                                AuthAllowed::BackupCode => {
                                    self.state = LoginState::BackupCode(true);
                                }
                                AuthAllowed::Totp => {
                                    self.state = LoginState::Totp(TotpState::Enabled);
                                }
                                AuthAllowed::SecurityKey(challenge) => {
                                    self.state = LoginState::SecurityKey(challenge.into())
                                }
                                AuthAllowed::Passkey(challenge) => {
                                    self.state = LoginState::Passkey(challenge.into())
                                }
                            }
                        } else {
                            // Else, present the options in a choice.
                            #[cfg(debug_assertions)]
                            console::debug!("multiple auth method choices exist!");
                            self.state = LoginState::Continue(allowed);
                        }
                        true
                    }
                    AuthState::Denied(reason) => {
                        console::error!(format!("Authentication denied -> {:?}", reason));
                        self.state = LoginState::Denied(reason);
                        true
                    }
                    AuthState::Success(bearer_token) => {
                        // Store the bearer here!
                        // We need to format the bearer onto it.
                        #[cfg(debug_assertions)]
                        console::info!(
                            "User has successfully authenticated, setting the bearer token"
                        );
                        let bearer_token = format!("Bearer {}", bearer_token);
                        set_bearer_token(bearer_token);
                        self.state = LoginState::Authenticated;
                        true
                    }
                }
            }
            LoginAppMsg::Continue(idx) => {
                // Are we in the correct internal state?
                #[cfg(debug_assertions)]
                console::debug!(format!("chose -> {:?}", idx));
                match &self.state {
                    LoginState::Continue(allowed) => {
                        match allowed.get(idx) {
                            Some(AuthAllowed::Anonymous) => {
                                // Just submit this.
                            }
                            Some(AuthAllowed::Password) => {
                                // Go to the password view.
                                self.state = LoginState::Password(true);
                            }
                            Some(AuthAllowed::BackupCode) => {
                                self.state = LoginState::BackupCode(true);
                            }
                            Some(AuthAllowed::Totp) => {
                                self.state = LoginState::Totp(TotpState::Enabled);
                            }
                            Some(AuthAllowed::SecurityKey(challenge)) => {
                                self.state = LoginState::SecurityKey(challenge.clone().into())
                            }
                            Some(AuthAllowed::Passkey(challenge)) => {
                                self.state = LoginState::Passkey(challenge.clone().into())
                            }
                            None => {
                                console::error!("invalid allowed mech idx".to_string());
                                self.state = LoginState::Error {
                                    emsg: "Invalid Continue Index".to_string(),
                                    kopid: None,
                                };
                            }
                        }
                    }
                    _ => {
                        console::error!("invalid state transition".to_string());
                        self.state = LoginState::Error {
                            emsg: "Invalid UI State Transition".to_string(),
                            kopid: None,
                        };
                    }
                }
                true
            }
            LoginAppMsg::UnknownUser => {
                // Clear any leftover input
                console::warn!("Unknown user".to_string());
                self.state = LoginState::UnknownUser;
                true
            }
            LoginAppMsg::Error { emsg, kopid } => {
                // Clear any leftover input
                console::error!(format!("error -> {:?}, {:?}", emsg, kopid));
                self.state = LoginState::Error { emsg, kopid };
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        #[cfg(debug_assertions)]
        console::debug!("login::view".to_string());
        html! {
            <BrowserRouter>
                <main class="flex-shrink-0 form-signin">
                    <center>
                    {logo_img()}
                        // TODO: make a call to domain info to show the domain name
                        // or more likely we should have this passed in from the props when we start.
                        <h3>{ "Kanidm" }</h3>
                    </center>
                    // <Switch<LoginRoute> render={switch} />
                    { self.view_state(ctx) }
                </main>
                { do_footer() }
            </BrowserRouter>
        }
    }

    fn destroy(&mut self, _ctx: &Context<Self>) {
        #[cfg(debug_assertions)]
        console::debug!("login::destroy".to_string());

        // Done with this, clear it.
        let _ = pop_login_hint();

        remove_body_form_classes!();
    }

    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
        #[cfg(debug_assertions)]
        console::debug!("login::rendered".to_string());
        // Force autofocus on elements that need it if present.
        autofocus("username");
        autofocus("password");
        autofocus("backup_code");
        autofocus("otp");
        autofocus("begin");
    }
}
