use crate::infra::api::HostService;
use anyhow::{anyhow, bail, Context, Result};
use lldap_auth::*;
use validator_derive::Validate;
use yew::prelude::*;
use yew::services::{fetch::FetchTask, ConsoleService};
use yew_form::Form;
use yew_form_derive::Model;

pub struct LoginForm {
    link: ComponentLink<Self>,
    on_logged_in: Callback<(String, bool)>,
    error: Option<anyhow::Error>,
    form: Form<FormModel>,
    // Used to keep the request alive long enough.
    task: Option<FetchTask>,
}

/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
pub struct FormModel {
    #[validate(length(min = 1, message = "Missing username"))]
    username: String,
    #[validate(length(min = 8, message = "Invalid password. Min length: 8"))]
    password: String,
}

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub on_logged_in: Callback<(String, bool)>,
}

pub enum Msg {
    Update,
    Submit,
    AuthenticationStartResponse(
        (
            opaque::client::login::ClientLogin,
            Result<Box<login::ServerLoginStartResponse>>,
        ),
    ),
    AuthenticationFinishResponse(Result<(String, bool)>),
}

impl LoginForm {
    fn handle_message(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
        match msg {
            Msg::Update => Ok(true),
            Msg::Submit => {
                if !self.form.validate() {
                    bail!("Invalid inputs");
                }
                let FormModel { username, password } = self.form.model();
                let mut rng = rand::rngs::OsRng;
                let opaque::client::login::ClientLoginStartResult { state, message } =
                    opaque::client::login::start_login(&password, &mut rng)
                        .context("Could not initialize login")?;
                let req = login::ClientLoginStartRequest {
                    username,
                    login_start_request: message,
                };
                self.task = Some(HostService::login_start(
                    req,
                    self.link
                        .callback_once(move |r| Msg::AuthenticationStartResponse((state, r))),
                )?);
                Ok(true)
            }
            Msg::AuthenticationStartResponse((login_start, res)) => {
                let res = res.context("Could not log in (invalid response to login start)")?;
                let login_finish =
                    match opaque::client::login::finish_login(login_start, res.credential_response)
                    {
                        Err(e) => {
                            // Common error, we want to print a full error to the console but only a
                            // simple one to the user.
                            ConsoleService::error(&format!("Invalid username or password: {}", e));
                            self.error = Some(anyhow!("Invalid username or password"));
                            return Ok(true);
                        }
                        Ok(l) => l,
                    };
                let req = login::ClientLoginFinishRequest {
                    server_data: res.server_data,
                    credential_finalization: login_finish.message,
                };
                self.task = Some(HostService::login_finish(
                    req,
                    self.link.callback_once(Msg::AuthenticationFinishResponse),
                )?);
                Ok(false)
            }
            Msg::AuthenticationFinishResponse(user_info) => {
                self.task = None;
                self.on_logged_in
                    .emit(user_info.context("Could not log in")?);
                Ok(true)
            }
        }
    }
}

impl Component for LoginForm {
    type Message = Msg;
    type Properties = Props;

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
        LoginForm {
            link,
            on_logged_in: props.on_logged_in,
            error: None,
            form: Form::<FormModel>::new(FormModel::default()),
            task: None,
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        self.error = None;
        match self.handle_message(msg) {
            Err(e) => {
                ConsoleService::error(&e.to_string());
                self.error = Some(e);
                self.task = None;
                true
            }
            Ok(b) => b,
        }
    }

    fn change(&mut self, _: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        type Field = yew_form::Field<FormModel>;
        html! {
            <form
              class="form center-block col-sm-4 col-offset-4">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">
                      <i class="bi-person-fill"/>
                    </span>
                  </div>
                  <Field
                    class="form-control"
                    class_invalid="is-invalid has-error"
                    class_valid="has-success"
                    form=&self.form
                    field_name="username"
                    placeholder="Username"
                    autocomplete="username"
                    oninput=self.link.callback(|_| Msg::Update) />
                </div>
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">
                      <i class="bi-lock-fill"/>
                    </span>
                  </div>
                  <Field
                    class="form-control"
                    class_invalid="is-invalid has-error"
                    class_valid="has-success"
                    form=&self.form
                    field_name="password"
                    input_type="password"
                    placeholder="Password"
                    autocomplete="current-password" />
                </div>
                <div class="form-group">
                  <button
                    type="submit"
                    class="btn btn-primary"
                    disabled=self.task.is_some()
                    onclick=self.link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
                    {"Login"}
                  </button>
                </div>
                <div class="form-group">
                { if let Some(e) = &self.error {
                    html! { e.to_string() }
                  } else { html! {} }
                }
                </div>
            </form>
        }
    }
}