use crate::app_config::AppConfig;
use crate::constants::{STATE_KEY, USER_SESSION_KEY};
use crate::server::{HttpFailure, HttpResult};
use crate::user::{APIClient, APIClientID, User, UserConfig, UserID};
use crate::utils;
use actix_session::Session;
use actix_web::{web, HttpResponse};
use askama::Template;
use ipnet::IpNet;
use light_openid::primitives::OpenIDConfig;
use std::str::FromStr;

/// Static assets
#[derive(rust_embed::Embed)]
#[folder = "assets/"]
struct Assets;

/// Serve static file
pub async fn static_file(path: web::Path<String>) -> HttpResult {
    match Assets::get(path.as_ref()) {
        Some(content) => Ok(HttpResponse::Ok()
            .content_type(
                mime_guess::from_path(path.as_str())
                    .first_or_octet_stream()
                    .as_ref(),
            )
            .body(content.data.into_owned())),
        None => Ok(HttpResponse::NotFound().body("404 Not Found")),
    }
}

#[derive(askama::Template)]
#[template(path = "index.html")]
struct HomeTemplate {
    name: String,
    user_id: UserID,
    matrix_token: String,
    clients: Vec<APIClient>,
    success_message: Option<String>,
    error_message: Option<String>,
}

/// HTTP form request
#[derive(serde::Deserialize)]
pub struct FormRequest {
    /// Update matrix token
    new_matrix_token: Option<String>,

    /// Create a new client
    new_client_desc: Option<String>,

    /// Restrict new client to a given network
    ip_network: Option<String>,

    /// Grant read only access to client
    readonly_client: Option<String>,

    /// Delete a specified client id
    delete_client_id: Option<APIClientID>,
}

/// Main route
pub async fn home(session: Session, form_req: Option<web::Form<FormRequest>>) -> HttpResult {
    // Get user information, requesting authentication if information is missing
    let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else {
        // Generate auth state
        let state = utils::rand_str(50);
        session.insert(STATE_KEY, &state)?;

        let oidc = AppConfig::get().openid_provider();
        let config = OpenIDConfig::load_from_url(oidc.configuration_url)
            .await
            .map_err(HttpFailure::OpenID)?;

        let auth_url = config.gen_authorization_url(oidc.client_id, &state, &oidc.redirect_url);

        return Ok(HttpResponse::Found()
            .append_header(("location", auth_url))
            .finish());
    };

    let mut success_message = None;
    let mut error_message = None;

    // Retrieve user configuration
    let mut config = UserConfig::load(&user.id, true)
        .await
        .map_err(HttpFailure::FetchUserConfig)?;

    if let Some(form_req) = form_req {
        // Update matrix token, if requested
        if let Some(t) = form_req.0.new_matrix_token {
            if t.len() < 3 {
                error_message = Some("Specified Matrix token is too short!".to_string());
            } else {
                // TODO : invalidate all existing connections
                config.matrix_token = t;
                config.save().await?;
                success_message = Some("Matrix token was successfully updated!".to_string());
            }
        }

        // Create a new client, if requested
        if let Some(new_token_desc) = form_req.0.new_client_desc {
            let ip_net = match form_req.0.ip_network.as_deref() {
                None | Some("") => None,
                Some(e) => match IpNet::from_str(e) {
                    Ok(n) => Some(n),
                    Err(e) => {
                        log::error!("Failed to parse IP network provided by user: {e}");
                        error_message = Some(format!("Failed to parse restricted IP network: {e}"));
                        None
                    }
                },
            };

            if error_message.is_none() {
                let mut token = APIClient::generate(new_token_desc, ip_net);
                token.readonly_client = form_req.0.readonly_client.is_some();
                success_message = Some(format!("The secret of your new token is '{}'. Be sure to write it somewhere as you will not be able to recover it later!", token.secret));
                config.clients.push(token);
                config.save().await?;
            }
        }

        // Delete a client
        if let Some(delete_client_id) = form_req.0.delete_client_id {
            config.clients.retain(|c| c.id != delete_client_id);
            config.save().await?;
            success_message = Some("The client was successfully deleted!".to_string());
            // TODO : close connections with given id
        }
    }

    // Render page
    Ok(HttpResponse::Ok()
        .insert_header(("content-type", "text/html"))
        .body(
            HomeTemplate {
                name: user.name,
                user_id: user.id,
                matrix_token: config.obfuscated_matrix_token(),
                clients: config.clients,
                success_message,
                error_message,
            }
            .render()
            .unwrap(),
        ))
}

#[derive(serde::Deserialize)]
pub struct AuthCallbackQuery {
    code: String,
    state: String,
}

/// Authenticate user callback
pub async fn oidc_cb(session: Session, query: web::Query<AuthCallbackQuery>) -> HttpResult {
    if session.get(STATE_KEY)? != Some(query.state.to_string()) {
        return Ok(HttpResponse::BadRequest()
            .append_header(("content-type", "text/html"))
            .body("State mismatch! <a href='/'>Try again</a>"));
    }

    let oidc = AppConfig::get().openid_provider();
    let config = OpenIDConfig::load_from_url(oidc.configuration_url)
        .await
        .map_err(HttpFailure::OpenID)?;

    let (token, _) = match config
        .request_token(
            oidc.client_id,
            oidc.client_secret,
            &query.code,
            &oidc.redirect_url,
        )
        .await
    {
        Ok(t) => t,
        Err(e) => {
            log::error!("Failed to request user token! {e}");

            return Ok(HttpResponse::BadRequest()
                .append_header(("content-type", "text/html"))
                .body("Authentication failed! <a href='/'>Try again</a>"));
        }
    };

    let (user, _) = config
        .request_user_info(&token)
        .await
        .map_err(HttpFailure::OpenID)?;

    let user = User {
        id: UserID(user.sub),
        name: user.name.unwrap_or("no_name".to_string()),
        email: user.email.unwrap_or("no@mail.com".to_string()),
    };
    log::info!("Successful authentication as {:?}", user);
    session.insert(USER_SESSION_KEY, user)?;

    Ok(HttpResponse::Found()
        .insert_header(("location", "/"))
        .finish())
}

/// De-authenticate user
pub async fn sign_out(session: Session) -> HttpResult {
    session.remove(USER_SESSION_KEY);

    Ok(HttpResponse::Found()
        .insert_header(("location", "/"))
        .finish())
}