pub mod common;

use crate::common::set_roles::normal::get_server_member_bulk;
use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serenity::client::Context;
use serenity::model::guild;
use serenity::model::id::{ChannelId, GuildId, RoleId, UserId};
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use serenity::prelude::TypeMapKey;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow};
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
use std::str::FromStr;
use std::{env, sync::Arc};
use tokio::sync::RwLock;

#[derive(Debug)]
pub struct Config {
  // manages where teh database is stored
  pub home: String,
  pub database: String,

  // tokens for discord and other API's
  pub discord_token: String,
  pub discord_token_minecraft: String,
  pub minecraft_mcprofile: String,

  // email settings
  pub mail_smtp: String,
  pub mail_user: String,
  pub mail_pass: String,

  // wolves API base for clubs/socs
  pub wolves_url: String,
  // API key for accessing more general resources
  pub wolves_api: String,

  // discord server for committee
  pub committee_server: GuildId,
  pub committee_role: RoleId,
  pub committee_category: ChannelId,
}
impl TypeMapKey for Config {
  type Value = Arc<RwLock<Config>>;
}

pub fn get_config() -> Config {
  dotenv().ok();

  // reasonable defaults
  let mut config = Config {
    discord_token: "".to_string(),
    discord_token_minecraft: "".to_string(),
    minecraft_mcprofile: "".to_string(),

    home: ".".to_string(),
    database: "database.db".to_string(),

    mail_smtp: "".to_string(),
    mail_user: "".to_string(),
    mail_pass: "".to_string(),
    wolves_url: "".to_string(),
    wolves_api: "".to_string(),
    committee_server: GuildId(0),
    committee_role: RoleId(0),
    committee_category: ChannelId(0),
  };

  if let Ok(x) = env::var("DATABASE_HOME") {
    config.home = x.trim().to_string();
  }
  if let Ok(x) = env::var("DATABASE") {
    config.database = x.trim().to_string();
  }

  if let Ok(x) = env::var("DISCORD_TOKEN") {
    config.discord_token = x.trim().to_string();
  }
  if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") {
    config.discord_token_minecraft = x.trim().to_string();
  }
  if let Ok(x) = env::var("MINECRAFT_MCPROFILE_KEY") {
    config.minecraft_mcprofile = x.trim().to_string();
  }

  if let Ok(x) = env::var("EMAIL_SMTP") {
    config.mail_smtp = x.trim().to_string();
  }
  if let Ok(x) = env::var("EMAIL_USER") {
    config.mail_user = x.trim().to_string();
  }
  if let Ok(x) = env::var("EMAIL_PASS") {
    config.mail_pass = x.trim().to_string();
  }

  if let Ok(x) = env::var("WOLVES_URL_BASE") {
    config.wolves_url = x.trim().to_string();
  }
  if let Ok(x) = env::var("WOLVES_API") {
    config.wolves_api = x.trim().to_string();
  }

  if let Ok(x) = env::var("COMMITTEE_DISCORD") {
    if let Ok(x) = x.trim().parse::<u64>() {
      config.committee_server = GuildId(x);
    }
  }
  if let Ok(x) = env::var("COMMITTEE_DISCORD") {
    if let Ok(x) = x.trim().parse::<u64>() {
      config.committee_role = RoleId(x);
    }
  }
  if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
    if let Ok(x) = x.trim().parse::<u64>() {
      config.committee_category = ChannelId(x);
    }
  }

  config
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
  pub server: GuildId,
  pub id_wolves: i64,
  pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let server_tmp: i64 = row.try_get("server")?;
    let server = GuildId::from(server_tmp as u64);

    Ok(Self {
      server,
      id_wolves: row.try_get("id_wolves")?,
      expiry: row.try_get("expiry")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
  pub server: GuildId,
  pub id_wolves: i64,
  pub expiry: String,
  pub email: String,
  pub discord: Option<UserId>,
  pub minecraft: Option<String>,
  pub minecraft_uid: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let server_tmp: i64 = row.try_get("server")?;
    let server = GuildId::from(server_tmp as u64);
    let discord = match row.try_get("discord") {
      Ok(x) => {
        let tmp: i64 = x;
        if tmp == 0 {
          None
        } else {
          Some(UserId::from(tmp as u64))
        }
      }
      _ => None,
    };

    Ok(Self {
      server,
      id_wolves: row.try_get("id_wolves")?,
      expiry: row.try_get("expiry")?,
      email: row.try_get("email")?,
      discord,
      minecraft: row.try_get("minecraft")?,
      minecraft_uid: row.try_get("minecraft_uid")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
  pub id_wolves: i64,
  pub email: String,
  pub discord: Option<UserId>,
  pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let discord = match row.try_get("discord") {
      Ok(x) => {
        let tmp: i64 = x;
        if tmp == 0 {
          None
        } else {
          Some(UserId::from(tmp as u64))
        }
      }
      _ => None,
    };

    Ok(Self {
      id_wolves: row.try_get("id_wolves")?,
      email: row.try_get("email")?,
      discord,
      minecraft: row.try_get("minecraft")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
  pub email: String,
  pub discord: UserId,
  pub auth_code: String,
  pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let user_tmp: i64 = row.try_get("discord")?;
    let discord = UserId::from(user_tmp as u64);

    Ok(Self {
      email: row.try_get("email")?,
      discord,
      auth_code: row.try_get("auth_code")?,
      date_expiry: row.try_get("date_expiry")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Committee {
  pub email: String,
  pub discord: UserId,
  pub auth_code: String,
  pub committee: i64,
}
impl<'r> FromRow<'r, SqliteRow> for Committee {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let user_tmp: i64 = row.try_get("discord")?;
    let discord = UserId::from(user_tmp as u64);

    Ok(Self {
      email: row.try_get("email")?,
      discord,
      auth_code: row.try_get("auth_code")?,
      committee: row.try_get("committee")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
  pub server: GuildId,
  pub wolves_api: String,
  pub role_past: Option<RoleId>,
  pub role_current: RoleId,
  pub member_past: i64,
  pub member_current: i64,
  pub bot_channel_id: ChannelId,
  // these can be removed in teh future with an API update
  pub server_name: String,
  pub wolves_link: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let server_tmp: i64 = row.try_get("server")?;
    let server = GuildId::from(server_tmp as u64);
    let role_past = match row.try_get("role_past") {
      Ok(x) => {
        let tmp: i64 = x;
        if tmp == 0 {
          None
        } else {
          Some(RoleId::from(tmp as u64))
        }
      }
      _ => None,
    };
    let role_current = match row.try_get("role_current") {
      Ok(x) => {
        let tmp: i64 = x;
        RoleId::from(tmp as u64)
      }
      _ => RoleId::from(0u64),
    };

    let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
    let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);

    Ok(Self {
      server,
      wolves_api: row.try_get("wolves_api")?,
      role_past,
      role_current,
      member_past: row.try_get("member_past")?,
      member_current: row.try_get("member_current")?,
      bot_channel_id,
      server_name: row.try_get("server_name")?,
      wolves_link: row.try_get("wolves_link")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft {
  pub discord: GuildId,
  pub minecraft: String,
}
impl<'r> FromRow<'r, SqliteRow> for Minecraft {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let server_tmp: i64 = row.try_get("server_discord")?;
    let discord = GuildId::from(server_tmp as u64);

    Ok(Self {
      discord,
      minecraft: row.try_get("server_minecraft")?,
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
  pub server: GuildId,
  pub role_a: RoleId,
  pub role_b: RoleId,
  pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
    let server_tmp: i64 = row.try_get("server")?;
    let server = GuildId::from(server_tmp as u64);

    Ok(Self {
      server,
      role_a: get_role_from_row(row, "role_a"),
      role_b: get_role_from_row(row, "role_b"),
      role_c: get_role_from_row(row, "role_c"),
    })
  }
}

fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
  match row.try_get(col) {
    Ok(x) => {
      let tmp: i64 = x;
      RoleId(tmp as u64)
    }
    _ => RoleId::from(0u64),
  }
}

pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
  let database = format!("{}/{}", &config.home, &config.database);

  let pool = SqlitePoolOptions::new()
    .max_connections(5)
    .connect_with(
      SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
        .foreign_keys(true)
        .create_if_missing(true),
    )
    .await?;

  // migrations are amazing!
  sqlx::migrate!("./db/migrations").run(&pool).await?;

  Ok(pool)
}

pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
  sqlx::query_as::<_, Servers>(
    r#"
    SELECT *
    FROM servers
    WHERE server = ?
    "#,
  )
  .bind(*server.as_u64() as i64)
  .fetch_one(db)
  .await
  .ok()
}

pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
  sqlx::query_as::<_, ServerMembersWolves>(
    r#"
    SELECT *
    FROM server_members
    JOIN wolves USING (id_wolves)
    WHERE server = ? AND discord = ?
    "#,
  )
  .bind(*server.as_u64() as i64)
  .bind(*member.user.id.as_u64() as i64)
  .fetch_one(db)
  .await
}

pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
  sqlx::query_as::<_, Servers>(
    r#"
        SELECT *
        FROM servers
        "#,
  )
  .fetch_all(db)
  .await
  .unwrap_or_default()
}

pub fn get_now_iso(short: bool) -> String {
  let now = Utc::now();
  if short {
    format!("{}-{:02}-{:02}", now.year(), now.month(), now.day())
  } else {
    now.to_rfc3339_opts(SecondsFormat::Millis, true)
  }
}

pub fn random_string(len: usize) -> String {
  thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}

pub mod set_roles {
  use super::*;
  use crate::common::database::DataBase;
  pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
    let db_lock = {
      let data_read = ctx.data.read().await;
      data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
    };

    let db = db_lock.read().await;

    let Servers {
      server,
      role_past,
      role_current,
      ..
    } = server;

    let mut roles_set = [0, 0, 0];
    let mut members = vec![];

    for member in get_server_member_bulk(&db, server).await {
      if let Some(x) = member.discord {
        members.push(x);
      }
    }
    let mut members_all = members.len();

    if let Ok(x) = server.members(ctx, None, None).await {
      for mut member in x {
        // members_changed acts as an override to only deal with teh users in it
        if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
          continue;
        }

        if members.contains(&member.user.id) {
          let mut roles = vec![];

          if let Some(role) = &role_past {
            if !member.roles.contains(role) {
              roles_set[0] += 1;
              roles.push(role.to_owned());
            }
          }

          if !member.roles.contains(role_current) {
            roles_set[1] += 1;
            roles.push(role_current.to_owned());
          }

          if let Err(e) = member.add_roles(ctx, &roles).await {
            println!("{:?}", e);
          }
        } else {
          // old and never

          if let Some(role) = &role_past {
            if member.roles.contains(role) {
              members_all += 1;
            }
          }

          if member.roles.contains(role_current) {
            roles_set[2] += 1;
            // if theya re not a current member and have the role then remove it
            if let Err(e) = member.remove_role(ctx, role_current).await {
              println!("{:?}", e);
            }
          }
        }
        for role in remove_roles.iter().flatten() {
          if let Err(e) = member.remove_role(ctx, role).await {
            println!("{:?}", e);
          }
        }
      }
    }

    set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;

    // small bit of logging to note changes over time
    println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]);
  }

  pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
    sqlx::query_as::<_, ServerMembersWolves>(
      r#"
      SELECT * 
      FROM server_members
      JOIN wolves USING (id_wolves)
      WHERE (
        server = ? 
        AND discord IS NOT NULL
        AND expiry > ?
      )
      "#,
    )
    .bind(*server.as_u64() as i64)
    .bind(get_now_iso(true))
    .fetch_all(db)
    .await
    .unwrap_or_default()
  }

  async fn set_server_numbers(db: &Pool<Sqlite>, server: &GuildId, past: i64, current: i64) {
    match sqlx::query_as::<_, Wolves>(
      "
      UPDATE servers
      SET member_past = ?, member_current = ?
      WHERE server = ?
    ",
    )
    .bind(past)
    .bind(current)
    .bind(*server.as_u64() as i64)
    .fetch_optional(db)
    .await
    {
      Ok(_) => {}
      Err(e) => {
        println!("Failure to insert into {}", server.as_u64());
        println!("{:?}", e);
      }
    }
  }
}

pub mod get_data {
  use super::*;
  use crate::common::database::DataBase;
  use crate::set_roles::update_server;
  use std::collections::BTreeMap;
  use wolves_oxidised::WolvesUser;

  pub async fn get_wolves(ctx: &Context) {
    let db_lock = {
      let data_read = ctx.data.read().await;
      data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
    };
    let db = db_lock.read().await;

    let config_lock = {
      let data_read = ctx.data.read().await;
      data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
    };
    let config = config_lock.read().await;

    // set up teh client
    let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));

    for server_config in get_server_config_bulk(&db).await {
      let Servers {
        server,
        wolves_api,
        ..
      } = &server_config;

      let existing_tmp = get_server_member(&db, server).await;
      let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();

      // list of users that need to be updated for this server
      let mut user_to_update = vec![];
      for user in wolves.get_members(wolves_api).await {
        let id = user.member_id.parse::<u64>().unwrap_or_default();
        match existing.get(&(id as i64)) {
          None => {
            // user does not exist already, add everything
            add_users_wolves(&db, &user).await;
            add_users_server_members(&db, server, &user).await;
          }
          Some(old) => {
            // always update wolves table, in case data has changed
            add_users_wolves(&db, &user).await;
            if old.expiry != user.expiry {
              add_users_server_members(&db, server, &user).await;

              if let Some(discord_id) = old.discord {
                user_to_update.push(discord_id);
              }
            }
          }
        }
      }

      if !user_to_update.is_empty() {
        update_server(ctx, &server_config, &[], &user_to_update).await;
      }
    }
  }

  pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
    sqlx::query_as::<_, ServerMembersWolves>(
      r#"
      SELECT *
      FROM server_members
      JOIN wolves USING (id_wolves)
      WHERE (
        server = ?
        AND discord IS NOT NULL
      )
      "#,
    )
    .bind(*server.as_u64() as i64)
    .fetch_all(db)
    .await
    .unwrap_or_default()
  }

  async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesUser) {
    // expiry
    match sqlx::query_as::<_, Wolves>(
      "
      INSERT INTO wolves (id_wolves, email)
      VALUES ($1, $2)
      ON CONFLICT(id_wolves) DO UPDATE SET email = $2
    ",
    )
    .bind(&user.member_id)
    .bind(&user.contact_email)
    .fetch_optional(db)
    .await
    {
      Ok(_) => {}
      Err(e) => {
        println!("Failure to insert into Wolves {:?}", user);
        println!("{:?}", e);
      }
    }
  }
  async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesUser) {
    match sqlx::query_as::<_, ServerMembers>(
      "
      INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
      VALUES (?1, ?2, ?3)
    ",
    )
    .bind(*server.as_u64() as i64)
    .bind(&user.member_id)
    .bind(&user.expiry)
    .fetch_optional(db)
    .await
    {
      Ok(_) => {}
      Err(e) => {
        println!("Failure to insert into ServerMembers {}  {:?}", server.as_u64(), user);
        println!("{:?}", e);
      }
    }
  }
}

/**
 For any time ye need to check if a user who calls a command has admin privlages
*/
pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) -> Option<String> {
  let mut admin = false;

  let g_id = match command.guild_id {
    None => return Some("Not in a server".to_string()),
    Some(x) => x,
  };

  let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();

  if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
    if let Some(permissions) = member.permissions {
      if permissions.administrator() {
        admin = true;
      }
    }

    for role_id in member.roles {
      if admin {
        break;
      }
      if let Some(role) = roles_server.get(&role_id) {
        if role.permissions.administrator() {
          admin = true;
        }
      }
    }
  }
  if !admin {
    Some("Administrator permission required".to_string())
  } else {
    None
  }
}

/**
loop through all members of server
get a list of folks with mc accounts that are members
and a list that arent members
 */
pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) {
  let mut usernames = vec![];
  for member in get_server_member_bulk(db, g_id).await {
    if let Some(x) = member.minecraft {
      usernames.push((x, true));
    }
    if let Some(x) = member.minecraft_uid {
      usernames.push((x, false));
    }
  }
  if !usernames.is_empty() {
    whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
  }
}

async fn post<T: Serialize>(url: &str, bearer: &str, data: &T) {
  match surf::post(url)
    .header("Authorization", bearer)
    .header("Content-Type", "application/json")
    .header("Accept", "Application/vnd.pterodactyl.v1+json")
    .body_json(&data)
  {
    Ok(req) => {
      req.await.ok();
    }
    Err(e) => {
      dbg!(e);
    }
  }
}

#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsResSub {
  pub identifier: String,
  pub name: String,
  pub description: String,
  pub is_suspended: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsRes {
  pub attributes: ServerDetailsResSub,
}

async fn get<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
  match surf::get(url)
    .header("Authorization", bearer)
    .header("Content-Type", "application/json")
    .header("Accept", "Application/vnd.pterodactyl.v1+json")
    .recv_json()
    .await
  {
    Ok(res) => Some(res),
    Err(e) => {
      dbg!(e);

      None
    }
  }
}

#[derive(Deserialize, Serialize, Debug)]
struct BodyCommand {
  command: String,
}

#[derive(Deserialize, Serialize, Debug)]
struct BodyDelete {
  root: String,
  files: Vec<String>,
}

pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) {
  println!("Update whitelist for {}", server);
  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
  let bearer = format!("Bearer {token}");

  for (name, java) in add {
    let data = if *java {
      BodyCommand {
        command: format!("whitelist add {name}"),
      }
    } else {
      BodyCommand {
        command: format!("fwhitelist add {name}"),
      }
    };
    post(&format!("{url_base}/command"), &bearer, &data).await;
  }
}

pub async fn whitelist_wipe(server: &str, token: &str) {
  println!("Wiping whitelist for {}", server);
  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
  let bearer = format!("Bearer {token}");

  // delete whitelist
  let deletion = BodyDelete {
    root: "/".to_string(),
    files: vec!["whitelist.json".to_string()],
  };
  post(&format!("{url_base}/files/delete"), &bearer, &deletion).await;

  // recreate teh file, passing in the type here so the compiler knows what type of vec it is
  post::<Vec<&str>>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await;

  // reload the whitelist
  let data = BodyCommand {
    command: "whitelist reload".to_string(),
  };
  post(&format!("{url_base}/command"), &bearer, &data).await;
}

pub async fn server_information(server: &str, token: &str) -> Option<ServerDetailsRes> {
  println!("Get server information for {}", server);
  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
  let bearer = format!("Bearer {token}");
  get::<ServerDetailsRes>(&format!("{url_base}/"), &bearer).await
}

pub async fn get_minecraft_config(db: &Pool<Sqlite>) -> Vec<Minecraft> {
  sqlx::query_as::<_, Minecraft>(
    r#"
        SELECT *
        FROM minecraft
        "#,
  )
  .fetch_all(db)
  .await
  .unwrap_or_default()
}

pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Vec<Minecraft> {
  sqlx::query_as::<_, Minecraft>(
    r#"
        SELECT *
        FROM minecraft
        WHERE server_discord = ?1
        "#,
  )
  .bind(*g_id.as_u64() as i64)
  .fetch_all(db)
  .await
  .unwrap_or_default()
}