// prose-pod-api
//
// Copyright: 2025, Rémi Bardon <remi@remibardon.name>
// License: Mozilla Public License v2.0 (MPL v2.0)

use std::fs::{self, File};
use std::io::Write;

use anyhow::Context;
use tracing::{debug, info, instrument, warn};

use crate::app_config::CONFIG_FILE_PATH;
use crate::auth::errors::InvalidCredentials;
use crate::auth::{AuthService, AuthToken, Credentials};
use crate::factory_reset::FactoryResetConfirmationCode;
use crate::models::DatabaseRwConnectionPools;
use crate::prose_pod_server_service::ProsePodServerService;
use crate::util::either::Either;
use crate::AppConfig;

use super::{FactoryResetService, InvalidConfirmationCode};

#[instrument(
    name = "factory_reset_controller::get_confirmation_code",
    level = "trace",
    skip_all, fields(jid = credentials.jid.to_string()),
)]
pub async fn get_confirmation_code(
    factory_reset_service: &FactoryResetService,
    credentials: &Credentials,
    auth_service: &AuthService,
) -> Result<FactoryResetConfirmationCode, InvalidCredentials> {
    // Test password.
    // NOTE: This cannot be used to brute-force a user’s password since
    //   the request is already authenticated using a valid OAuth 2.0
    //   token for that user.
    // TODO: Use a method that does not create a new token.
    if (auth_service
        .log_in(&credentials.jid, &credentials.password)
        .await)
        .is_err()
    {
        return Err(InvalidCredentials);
    }

    let confirmation = factory_reset_service.get_confirmation_code();

    Ok(confirmation)
}

#[instrument(
    name = "factory_reset_controller::perform_factory_reset",
    level = "trace",
    skip_all
)]
pub async fn perform_factory_reset(
    factory_reset_service: &FactoryResetService,
    confirmation: FactoryResetConfirmationCode,
    db: DatabaseRwConnectionPools,
    server: &ProsePodServerService,
    app_config: &AppConfig,
    auth: &AuthToken,
) -> Result<(), Either<InvalidConfirmationCode, anyhow::Error>> {
    if !factory_reset_service.is_confirmation_code_valid(&confirmation) {
        return Err(Either::E1(InvalidConfirmationCode));
    }

    warn!("Performing a factory reset…");

    debug!("Erasing user data from the server…");
    server
        .delete_all_data(auth)
        .await
        .context("Could not erase all server data")?;

    debug!("Resetting the API’s database…");
    // Close the database connection to make sure SeaORM
    // doesn’t write to it after we empty the file.
    (db.read.close())
        .await
        .context("Could not close the database connection (read)")?;
    // Close the database connection to make sure SeaORM
    // doesn’t write to it after we empty the file.
    (db.write.close())
        .await
        .context("Could not close the database connection (write)")?;
    // Then empty the database file.
    // NOTE: We don’t just revert database migrations to ensure nothing remains.
    let database_url = (app_config.api.databases.main_url())
        .strip_prefix("sqlite://")
        .context("Database URL should start with `sqlite://`")?;
    // NOTE: `File::create` truncates the file if it exists.
    File::create(database_url)
        .context(format!("Could not reset API database at <{database_url}>"))?;

    debug!("Resetting the API’s configuration file…");
    let config_file_path = CONFIG_FILE_PATH.as_path();
    let mut file = fs::OpenOptions::new()
        .write(true)
        .truncate(true)
        .open(config_file_path)
        .context(format!(
            "Could not reset API config file at <{path}>: Cannot open",
            path = config_file_path.display(),
        ))?;
    let bootstrap_config = r#"# Prose Pod API configuration file
# Template: https://github.com/prose-im/prose-pod-system/blob/master/templates/prose.toml
# All keys: https://github.com/prose-im/prose-pod-api/blob/master/src/service/src/features/app_config/mod.rs
"#;
    file.write_all(bootstrap_config.as_bytes())
        .context(format!(
            "Could not reset API config file at <{path}>: Cannot write",
            path = config_file_path.display(),
        ))?;

    info!("Factory reset done.");
    Ok(())
}
