/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use std::{
    path::{Path, PathBuf},
    sync::Arc,
};

use futures::{StreamExt, stream::FuturesUnordered};
use jmap_client::{
    email::{self, Email},
    identity::{self, Identity},
    mailbox::{self, Mailbox},
    sieve::{self, SieveScript},
    vacation_response::{self, VacationResponse},
};
use serde::Serialize;
use tokio::io::AsyncWriteExt;

use crate::modules::RETRY_ATTEMPTS;

use super::{
    UnwrapResult,
    cli::{Client, ExportCommands},
    name_to_id,
};

impl ExportCommands {
    pub async fn exec(self, client: Client) {
        let mut client = client.into_jmap_client().await;
        match self {
            ExportCommands::Account {
                num_concurrent,
                account,
                path,
            } => {
                client.set_default_account_id(name_to_id(&client, &account).await);
                let max_objects_in_get = client
                    .session()
                    .core_capabilities()
                    .map(|c| c.max_objects_in_get())
                    .unwrap_or(500);

                // Create directory
                let mut path = PathBuf::from(path);
                if !path.is_dir() {
                    eprintln!("Directory {} does not exist.", path.display());
                    std::process::exit(1);
                }
                path.push(&account);
                if !path.is_dir() {
                    std::fs::create_dir(&path).unwrap_or_else(|_| {
                        eprintln!("Failed to create directory: {}", path.display());
                        std::process::exit(1);
                    });
                }

                // Export metadata
                let mut blobs = Vec::new();
                export_mailboxes(&client, max_objects_in_get, &path).await;
                export_emails(&client, max_objects_in_get, &mut blobs, &path).await;
                export_sieve_scripts(&client, max_objects_in_get, &mut blobs, &path).await;
                export_identities(&client, &path).await;
                export_vacation_responses(&client, &path).await;

                // Export blobs
                path.push("blobs");
                if !path.exists() {
                    std::fs::create_dir(&path).unwrap_or_else(|_| {
                        eprintln!("Failed to create directory: {}", path.display());
                        std::process::exit(1);
                    });
                }
                let client = Arc::new(client);
                let num_concurrent = num_concurrent.unwrap_or_else(num_cpus::get);
                let mut futures = FuturesUnordered::new();
                eprintln!("Exporting {} blobs...", blobs.len());
                for blob_id in blobs {
                    let client = client.clone();
                    let mut blob_path = path.clone();
                    blob_path.push(&blob_id);

                    if tokio::fs::metadata(&blob_path).await.is_err() {
                        futures.push(async move {
                            let mut retry_count = 0;

                            let bytes = loop {
                                match client.download(&blob_id).await {
                                    Ok(bytes) => break bytes,
                                    Err(_) if retry_count < RETRY_ATTEMPTS => {
                                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                                        retry_count += 1;
                                    }
                                    result => {
                                        result.unwrap_result("download blob");
                                        return;
                                    }
                                }
                            };

                            tokio::fs::OpenOptions::new()
                                .create(true)
                                .write(true)
                                .truncate(true)
                                .open(&blob_path)
                                .await
                                .unwrap_result(&format!("open {}", blob_path.display()))
                                .write_all(&bytes)
                                .await
                                .unwrap_result(&format!("write {}", blob_path.display()));
                        });
                    }

                    if futures.len() == num_concurrent {
                        futures.next().await.unwrap();
                    }
                }

                // Wait for remaining futures
                while futures.next().await.is_some() {}
            }
        }
    }
}

pub async fn fetch_mailboxes(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
) -> Vec<Mailbox> {
    let mut position = 0;
    let mut results = Vec::new();
    loop {
        let mut request = client.build();
        let query_result = request
            .query_mailbox()
            .calculate_total(true)
            .position(position)
            .limit(max_objects_in_get)
            .result_reference();
        request.get_mailbox().ids_ref(query_result).properties([
            mailbox::Property::Id,
            mailbox::Property::Name,
            mailbox::Property::IsSubscribed,
            mailbox::Property::ParentId,
            mailbox::Property::Role,
            mailbox::Property::SortOrder,
            mailbox::Property::ShareWith,
        ]);

        let mut response = request
            .send()
            .await
            .unwrap_result("send JMAP request")
            .unwrap_method_responses();
        if response.len() != 2 {
            eprintln!("Invalid response while fetching mailboxes");
            std::process::exit(1);
        }
        let mut get_response = response
            .pop()
            .unwrap()
            .unwrap_get_mailbox()
            .unwrap_result("fetch mailboxes");
        let mailboxes_part = get_response.take_list();
        let total_mailboxes = response
            .pop()
            .unwrap()
            .unwrap_query_mailbox()
            .unwrap_result("query mailboxes")
            .total()
            .unwrap_or(0);

        let mailboxes_part_len = mailboxes_part.len();
        if mailboxes_part_len > 0 {
            results.extend(mailboxes_part);
            if results.len() < total_mailboxes {
                position += mailboxes_part_len as i32;
                continue;
            }
        }
        break;
    }
    results
}

async fn export_mailboxes(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
    path: &Path,
) {
    eprintln!(
        "Exported {} mailboxes.",
        write_file(
            path,
            "mailboxes.json",
            fetch_mailboxes(client, max_objects_in_get).await,
        )
        .await
    );
}

pub async fn fetch_emails(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
) -> Vec<Email> {
    let mut position = 0;
    let mut results = Vec::new();

    loop {
        let mut request = client.build();
        let query_result = request
            .query_email()
            .calculate_total(true)
            .position(position)
            .limit(max_objects_in_get)
            .result_reference();
        request.get_email().ids_ref(query_result).properties([
            email::Property::Id,
            email::Property::MailboxIds,
            email::Property::Keywords,
            email::Property::ReceivedAt,
            email::Property::BlobId,
            email::Property::MessageId,
        ]);

        let mut response = request
            .send()
            .await
            .unwrap_result("send JMAP request")
            .unwrap_method_responses();
        if response.len() != 2 {
            eprintln!("Invalid response while fetching emails");
            std::process::exit(1);
        }
        let mut get_response = response
            .pop()
            .unwrap()
            .unwrap_get_email()
            .unwrap_result("fetch emails");
        let emails_part = get_response.take_list();
        let total_emails = response
            .pop()
            .unwrap()
            .unwrap_query_email()
            .unwrap_result("query emails")
            .total()
            .unwrap_or(0);

        let emails_part_len = emails_part.len();
        if emails_part_len > 0 {
            results.extend(emails_part);
            if results.len() < total_emails {
                position += emails_part_len as i32;
                continue;
            }
        }
        break;
    }

    results
}

async fn export_emails(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
    blobs: &mut Vec<String>,
    path: &Path,
) {
    let emails = fetch_emails(client, max_objects_in_get).await;

    for email in &emails {
        if let Some(blob_id) = email.blob_id() {
            blobs.push(blob_id.to_string());
        } else {
            eprintln!(
                "Warning: email {:?} has no blobId",
                email.id().unwrap_or_default()
            );
        }
    }

    eprintln!(
        "Exported {} emails.",
        write_file(path, "emails.json", emails,).await
    );
}

pub async fn fetch_sieve_scripts(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
) -> Vec<SieveScript> {
    let mut position = 0;
    let mut results = Vec::new();

    loop {
        let mut request = client.build();
        let query_result = request
            .query_sieve_script()
            .calculate_total(true)
            .position(position)
            .limit(max_objects_in_get)
            .result_reference();
        request
            .get_sieve_script()
            .ids_ref(query_result)
            .properties([
                sieve::Property::Id,
                sieve::Property::Name,
                sieve::Property::BlobId,
                sieve::Property::IsActive,
            ]);

        let mut response = request
            .send()
            .await
            .unwrap_result("send JMAP request")
            .unwrap_method_responses();
        if response.len() != 2 {
            eprintln!("Invalid response while fetching sieve_scripts");
            std::process::exit(1);
        }
        let mut get_response = response
            .pop()
            .unwrap()
            .unwrap_get_sieve_script()
            .unwrap_result("fetch sieve_scripts");
        let sieve_scripts_part = get_response.take_list();
        let total_sieve_scripts = response
            .pop()
            .unwrap()
            .unwrap_query_sieve_script()
            .unwrap_result("query sieve_scripts")
            .total()
            .unwrap_or(0);

        let sieve_scripts_part_len = sieve_scripts_part.len();
        if sieve_scripts_part_len > 0 {
            results.extend(sieve_scripts_part);

            if results.len() < total_sieve_scripts {
                position += sieve_scripts_part_len as i32;
                continue;
            }
        }
        break;
    }
    results
}

async fn export_sieve_scripts(
    client: &jmap_client::client::Client,
    max_objects_in_get: usize,
    blobs: &mut Vec<String>,
    path: &Path,
) {
    let sieves = fetch_sieve_scripts(client, max_objects_in_get).await;
    for sieve in &sieves {
        if let Some(blob_id) = sieve.blob_id() {
            blobs.push(blob_id.to_string());
        } else {
            eprintln!(
                "Warning: sieve script {:?} has no blobId",
                sieve.id().unwrap_or_default()
            );
        }
    }

    eprintln!(
        "Exported {} sieve scripts.",
        write_file(path, "sieve.json", sieves,).await
    );
}

pub async fn fetch_identities(client: &jmap_client::client::Client) -> Vec<Identity> {
    let mut request = client.build();
    request.get_identity().properties([
        identity::Property::Id,
        identity::Property::Name,
        identity::Property::Email,
        identity::Property::ReplyTo,
        identity::Property::Bcc,
        identity::Property::TextSignature,
        identity::Property::HtmlSignature,
    ]);
    request
        .send_get_identity()
        .await
        .unwrap_result("send JMAP request")
        .take_list()
}

async fn export_identities(client: &jmap_client::client::Client, path: &Path) {
    eprintln!(
        "Exported {} identities.",
        write_file(path, "identities.json", fetch_identities(client).await).await
    );
}

pub async fn fetch_vacation_responses(
    client: &jmap_client::client::Client,
) -> Vec<VacationResponse> {
    let mut request = client.build();
    request.get_vacation_response().properties([
        vacation_response::Property::Id,
        vacation_response::Property::FromDate,
        vacation_response::Property::ToDate,
        vacation_response::Property::Subject,
        vacation_response::Property::TextBody,
        vacation_response::Property::HtmlBody,
        vacation_response::Property::IsEnabled,
    ]);
    request
        .send_get_vacation_response()
        .await
        .unwrap_result("send JMAP request")
        .take_list()
}

async fn export_vacation_responses(client: &jmap_client::client::Client, path: &Path) {
    eprintln!(
        "Exported {} vacation responses.",
        write_file(
            path,
            "vacation.json",
            fetch_vacation_responses(client).await
        )
        .await
    );
}

async fn write_file<T: Serialize>(path: &Path, name: &str, contents: Vec<T>) -> usize {
    let mut path = PathBuf::from(path);
    path.push(name);
    let len = contents.len();
    tokio::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(&path)
        .await
        .unwrap_result(&format!("open {}", path.display()))
        .write_all(serde_json::to_string(&contents).unwrap().as_bytes())
        .await
        .unwrap_result(&format!("write to {}", path.display()));
    len
}
