use crate::{
    ChecksumMetadata, DecryptOptions, DecryptionMode, Metadata, OperationKind, OperationResult,
    SecretMetadata, decrypt_into_memory, is_encrypted_path, project::append_to_gitignore_if_absent,
    to_decrypted_path, to_encrypted_path, to_metadata_path,
};
use crate::{RecipientData, generate_preview, is_metadata_path, make_checksum, verify_checksum};
use age::armor::ArmoredWriter;
use age::secrecy::SecretString;
use anyhow::{Context, Result, anyhow};
use chrono::{DateTime, Utc};
use filetime::{FileTime, set_file_mtime};
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub enum EncryptionMode {
    Passphrase(String),
    Recipients(Vec<RecipientData>),
}

#[derive(Clone)]
pub struct EncryptOptions {
    pub mode: EncryptionMode,
    pub identity_path: PathBuf,
    pub decryption_mode: Option<DecryptionMode>,
    pub armor: bool,
    pub skip_gitignore: bool,
    pub skip_timestamps: bool,
    pub skip_preview: bool,
    pub force: bool,
}

pub fn encrypt_file(path: &Path, options: &EncryptOptions) -> Result<Option<OperationResult>> {
    log::debug!("{}: encrypting file", path.display());
    let is_identity = (|| {
        let p = path.canonicalize().ok()?;
        let i = options.identity_path.canonicalize().ok()?;
        Some(p.starts_with(i))
    })()
    .unwrap_or(false);

    if is_encrypted_path(path) || is_metadata_path(path) || is_identity {
        log::warn!("skipped: {}: invalid path for encryption", path.display());
        return Ok(None);
    }

    // Just read operations for now ------------------------
    let (encryptor, recipients_data) = match &options.mode {
        EncryptionMode::Passphrase(pass) => (
            age::Encryptor::with_user_passphrase(SecretString::from(pass.as_str())),
            pass.as_bytes().to_vec(),
        ),
        EncryptionMode::Recipients(recipients) => (
            age::Encryptor::with_recipients(recipients.iter().map(|(r, _)| r.as_ref()))
                .map_err(|_| anyhow!("at least one recipient must be provided"))?,
            recipients
                .iter()
                .flat_map(|(_, data)| data)
                .copied()
                .collect::<Vec<u8>>(),
        ),
    };

    let format = if options.armor {
        age::armor::Format::AsciiArmor
    } else {
        age::armor::Format::Binary
    };

    let input_file = File::open(path)
        .with_context(|| format!("{}: could not open input file", path.display()))?;
    log::debug!("{}: reading input file", path.display());

    let input = {
        let mut reader = BufReader::new(&input_file);
        let mut buffer = vec![];
        std::io::copy(&mut reader, &mut buffer)?;
        buffer
    };

    let output_path = to_encrypted_path(path);
    let metadata_path = to_metadata_path(path);
    let filemtime = input_file.metadata()?.modified()?;

    if !options.force && metadata_path.exists() {
        let metadata = Metadata::read_from_path(&metadata_path)
            .with_context(|| format!("{}: could not read metadata", metadata_path.display()))?;

        if verify_checksum(&recipients_data, &metadata.checksum.recipients, path).is_ok()
            && verify_checksum(input.as_slice(), &metadata.checksum.decrypted, path).is_ok()
        {
            log::debug!("{}: skipping encryption: checksums match", path.display());
            if !options.skip_timestamps {
                set_file_mtime(&output_path, FileTime::from_system_time(filemtime))?;
            }
            return Ok(None);
        }
    }

    let output = {
        let mut reader = BufReader::new(input.as_slice());
        let mut buffer = vec![];
        let mut enc_writer =
            encryptor.wrap_output(ArmoredWriter::wrap_output(&mut buffer, format)?)?;
        std::io::copy(&mut reader, &mut enc_writer)?;
        enc_writer.finish().and_then(|armor| armor.finish())?;
        buffer.flush()?;
        buffer
    };

    let timestamp = DateTime::<Utc>::from(filemtime).to_rfc3339();
    let secret = SecretMetadata { timestamp };
    let checksum = {
        let encrypted = make_checksum(output.as_slice());
        let decrypted = make_checksum(input.as_slice());
        ChecksumMetadata {
            encrypted,
            decrypted,
            recipients: make_checksum(&recipients_data),
        }
    };

    let preview = if !options.skip_preview {
        let (old_content, old_preview) = if let Some(dec_mode) = &options.decryption_mode
            && output_path.exists()
            && metadata_path.exists()
        {
            let old_metadata = Metadata::read_from_path(&metadata_path).ok();
            let old_preview = old_metadata
                .as_ref()
                .and_then(|m| m.preview.as_ref())
                .map(|p| p.preview.clone());

            let decrypt_options = DecryptOptions {
                mode: dec_mode.clone(),
                skip_gitignore: true,
                skip_timestamps: true,
                skip_verify_encrypted: true,
                skip_verify_decrypted: true,
            };

            let old_content = File::open(&output_path)
                .ok()
                .and_then(|f| decrypt_into_memory(f, &decrypt_options).ok());

            (old_content, old_preview)
        } else {
            (None, None)
        };

        generate_preview(
            path,
            &input,
            old_content.as_deref(),
            old_preview.as_deref(),
            &secret.timestamp,
        )
    } else {
        None
    };

    let metadata = Metadata {
        secret,
        checksum,
        preview,
    };

    // Write starts here ------------------------

    // First add to .gitignore before creating the encrypted file, because, why not!
    let gitignore = if !options.skip_gitignore {
        append_to_gitignore_if_absent(path)?
    } else {
        None
    };

    log::debug!("{}: writing encrypted file", output_path.display());
    std::fs::write(&output_path, output)
        .with_context(|| format!("{}: could not write encrypted file", output_path.display()))?;

    if !options.skip_timestamps {
        set_file_mtime(&output_path, FileTime::from_system_time(filemtime))?;
    }
    log::debug!("{}: writing metadata file", metadata_path.display());
    std::fs::write(&metadata_path, toml::to_string(&metadata)?)
        .with_context(|| format!("{}: could not write metadata file", metadata_path.display()))?;

    Ok(Some(OperationResult {
        kind: OperationKind::Encrypt,
        input: path.to_path_buf(),
        output: output_path,
        gitignore,
        metadata: Some(metadata_path),
    }))
}

pub fn encrypt_dir(
    path: &Path,
    options: &EncryptOptions,
) -> impl Iterator<Item = Result<OperationResult>> {
    walkdir::WalkDir::new(path)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.path().is_file())
        .filter(|e| is_encrypted_path(e.path()))
        .filter_map(|e| to_decrypted_path(e.path()))
        .filter_map(|path| encrypt_file(&path, options).transpose())
}

pub fn encrypt_path<'a>(
    path: &'a Path,
    options: &'a EncryptOptions,
) -> Box<dyn Iterator<Item = Result<OperationResult>> + 'a> {
    if path.is_dir() {
        Box::new(encrypt_dir(path, options))
    } else if path.is_file() {
        Box::new(encrypt_file(path, options).transpose().into_iter())
    } else {
        Box::new(std::iter::once(Err(anyhow!(
            "{}: path does not exist",
            path.display()
        ))))
    }
}
