use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use serde_json::Value;
use tokio::sync::RwLock;

use crate::{
    api::{ApiResult, EmptyResult, UpdateType},
    db::models::{Cipher, Device, Folder, Send, User},
    util::get_reqwest_client,
    CONFIG,
};

use once_cell::sync::Lazy;
use std::time::{Duration, Instant};

#[derive(Deserialize)]
struct AuthPushToken {
    access_token: String,
    expires_in: i32,
}

#[derive(Debug)]
struct LocalAuthPushToken {
    access_token: String,
    valid_until: Instant,
}

async fn get_auth_push_token() -> ApiResult<String> {
    static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
        RwLock::new(LocalAuthPushToken {
            access_token: String::new(),
            valid_until: Instant::now(),
        })
    });
    let push_token = PUSH_TOKEN.read().await;

    if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
        debug!("Auth Push token still valid, no need for a new one");
        return Ok(push_token.access_token.clone());
    }
    drop(push_token); // Drop the read lock now

    let installation_id = CONFIG.push_installation_id();
    let client_id = format!("installation.{installation_id}");
    let client_secret = CONFIG.push_installation_key();

    let params = [
        ("grant_type", "client_credentials"),
        ("scope", "api.push"),
        ("client_id", &client_id),
        ("client_secret", &client_secret),
    ];

    let res = match get_reqwest_client().post("https://identity.bitwarden.com/connect/token").form(&params).send().await
    {
        Ok(r) => r,
        Err(e) => err!(format!("Error getting push token from bitwarden server: {e}")),
    };

    let json_pushtoken = match res.json::<AuthPushToken>().await {
        Ok(r) => r,
        Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
    };

    let mut push_token = PUSH_TOKEN.write().await;
    push_token.valid_until = Instant::now()
        .checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
        .unwrap();

    push_token.access_token = json_pushtoken.access_token;

    debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
    Ok(push_token.access_token.clone())
}

pub async fn register_push_device(user_uuid: String, device: Device) -> EmptyResult {
    if !CONFIG.push_enabled() {
        return Ok(());
    }
    let auth_push_token = get_auth_push_token().await?;

    //Needed to register a device for push to bitwarden :
    let data = json!({
        "userId": user_uuid,
        "deviceId": device.push_uuid,
        "identifier": device.uuid,
        "type": device.atype,
        "pushToken": device.push_token
    });

    let auth_header = format!("Bearer {}", &auth_push_token);

    get_reqwest_client()
        .post(CONFIG.push_relay_uri() + "/push/register")
        .header(CONTENT_TYPE, "application/json")
        .header(ACCEPT, "application/json")
        .header(AUTHORIZATION, auth_header)
        .json(&data)
        .send()
        .await?
        .error_for_status()?;
    Ok(())
}

pub async fn unregister_push_device(uuid: String) -> EmptyResult {
    if !CONFIG.push_enabled() {
        return Ok(());
    }
    let auth_push_token = get_auth_push_token().await?;

    let auth_header = format!("Bearer {}", &auth_push_token);

    match get_reqwest_client()
        .delete(CONFIG.push_relay_uri() + "/push/" + &uuid)
        .header(AUTHORIZATION, auth_header)
        .send()
        .await
    {
        Ok(r) => r,
        Err(e) => err!(format!("An error occured during device unregistration: {e}")),
    };
    Ok(())
}

pub async fn push_cipher_update(
    ut: UpdateType,
    cipher: &Cipher,
    acting_device_uuid: &String,
    conn: &mut crate::db::DbConn,
) {
    // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
    if cipher.organization_uuid.is_some() {
        return;
    };
    let user_uuid = match &cipher.user_uuid {
        Some(c) => c,
        None => {
            debug!("Cipher has no uuid");
            return;
        }
    };

    if Device::check_user_has_push_device(user_uuid, conn).await {
        send_to_push_relay(json!({
            "userId": user_uuid,
            "organizationId": (),
            "deviceId": acting_device_uuid,
            "identifier": acting_device_uuid,
            "type": ut as i32,
            "payload": {
                "id": cipher.uuid,
                "userId": cipher.user_uuid,
                "organizationId": (),
                "revisionDate": cipher.updated_at
            }
        }))
        .await;
    }
}

pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
    let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null);

    tokio::task::spawn(send_to_push_relay(json!({
        "userId": user.uuid,
        "organizationId": (),
        "deviceId": acting_device_uuid,
        "identifier": acting_device_uuid,
        "type": UpdateType::LogOut as i32,
        "payload": {
            "userId": user.uuid,
            "date": user.updated_at
        }
    })));
}

pub fn push_user_update(ut: UpdateType, user: &User) {
    tokio::task::spawn(send_to_push_relay(json!({
        "userId": user.uuid,
        "organizationId": (),
        "deviceId": (),
        "identifier": (),
        "type": ut as i32,
        "payload": {
            "userId": user.uuid,
            "date": user.updated_at
        }
    })));
}

pub async fn push_folder_update(
    ut: UpdateType,
    folder: &Folder,
    acting_device_uuid: &String,
    conn: &mut crate::db::DbConn,
) {
    if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
        tokio::task::spawn(send_to_push_relay(json!({
            "userId": folder.user_uuid,
            "organizationId": (),
            "deviceId": acting_device_uuid,
            "identifier": acting_device_uuid,
            "type": ut as i32,
            "payload": {
                "id": folder.uuid,
                "userId": folder.user_uuid,
                "revisionDate": folder.updated_at
            }
        })));
    }
}

pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) {
    if let Some(s) = &send.user_uuid {
        if Device::check_user_has_push_device(s, conn).await {
            tokio::task::spawn(send_to_push_relay(json!({
                "userId": send.user_uuid,
                "organizationId": (),
                "deviceId": acting_device_uuid,
                "identifier": acting_device_uuid,
                "type": ut as i32,
                "payload": {
                    "id": send.uuid,
                    "userId": send.user_uuid,
                    "revisionDate": send.revision_date
                }
            })));
        }
    }
}

async fn send_to_push_relay(notification_data: Value) {
    if !CONFIG.push_enabled() {
        return;
    }

    let auth_push_token = match get_auth_push_token().await {
        Ok(s) => s,
        Err(e) => {
            debug!("Could not get the auth push token: {}", e);
            return;
        }
    };

    let auth_header = format!("Bearer {}", &auth_push_token);

    if let Err(e) = get_reqwest_client()
        .post(CONFIG.push_relay_uri() + "/push/send")
        .header(ACCEPT, "application/json")
        .header(CONTENT_TYPE, "application/json")
        .header(AUTHORIZATION, &auth_header)
        .json(&notification_data)
        .send()
        .await
    {
        error!("An error occured while sending a send update to the push relay: {}", e);
    };
}

pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, conn: &mut crate::db::DbConn) {
    if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
        tokio::task::spawn(send_to_push_relay(json!({
            "userId": user_uuid,
            "organizationId": (),
            "deviceId": null,
            "identifier": null,
            "type": UpdateType::AuthRequest as i32,
            "payload": {
                "id": auth_request_uuid,
                "userId": user_uuid,
            }
        })));
    }
}

pub async fn push_auth_response(
    user_uuid: String,
    auth_request_uuid: String,
    approving_device_uuid: String,
    conn: &mut crate::db::DbConn,
) {
    if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
        tokio::task::spawn(send_to_push_relay(json!({
            "userId": user_uuid,
            "organizationId": (),
            "deviceId": approving_device_uuid,
            "identifier": approving_device_uuid,
            "type": UpdateType::AuthRequestResponse as i32,
            "payload": {
                "id": auth_request_uuid,
                "userId": user_uuid,
            }
        })));
    }
}