//! Direct, additive KeePass KDBX imports into a pass-compatible repository.

use std::{
    collections::BTreeSet,
    error::Error,
    fmt, fs,
    path::{Path, PathBuf},
};

use keepass::{
    Database, DatabaseKey,
    db::{Entry, EntryRef, GroupRef, Icon, fields},
};

use crate::{
    command::{EditRequest, InsertInput, InsertRequest},
    crypto::{KeyStore, SecretProvider},
    git::{AutomaticEntryCommitter, GitError, GitIdentity},
    repository::{EntryPath, Repository, RepositoryError, SecretBytes},
    write::{InsertContent, OverwriteDecision, VaultWriter, WriteError},
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KdbxImportMode {
    AddAndUpdate,
    QuickAdd,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KdbxImportRequest {
    source: PathBuf,
    key_file: Option<PathBuf>,
    mode: KdbxImportMode,
}

impl KdbxImportRequest {
    pub fn new(
        source: impl Into<PathBuf>,
        key_file: Option<PathBuf>,
        mode: KdbxImportMode,
    ) -> Self {
        Self {
            source: source.into(),
            key_file,
            mode,
        }
    }

    pub fn source(&self) -> &Path {
        &self.source
    }

    pub fn key_file(&self) -> Option<&Path> {
        self.key_file.as_deref()
    }

    pub fn mode(&self) -> KdbxImportMode {
        self.mode
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KdbxImportOutcome {
    added: usize,
    updated: usize,
    unchanged: usize,
    skipped: usize,
    changed_paths: Vec<EntryPath>,
}

impl KdbxImportOutcome {
    pub fn added(&self) -> usize {
        self.added
    }

    pub fn updated(&self) -> usize {
        self.updated
    }

    pub fn unchanged(&self) -> usize {
        self.unchanged
    }

    pub fn skipped(&self) -> usize {
        self.skipped
    }

    pub fn changed_paths(&self) -> &[EntryPath] {
        &self.changed_paths
    }
}

pub struct KdbxImporter<'a> {
    repository: &'a Repository,
    keys: &'a KeyStore,
}

impl<'a> KdbxImporter<'a> {
    pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
        Self { repository, keys }
    }

    pub fn import(
        &self,
        request: &KdbxImportRequest,
        password: SecretBytes,
        secrets: &mut impl SecretProvider,
        identity: GitIdentity,
    ) -> Result<KdbxImportOutcome, KdbxImportError> {
        let mut source =
            fs::File::open(request.source()).map_err(|source| KdbxImportError::Io {
                operation: "open KDBX database",
                path: request.source.clone(),
                source,
            })?;
        let password = std::str::from_utf8(password.expose())
            .map_err(|_| KdbxImportError::PasswordEncoding)?;
        let mut key = DatabaseKey::new().with_password(password);
        if let Some(path) = request.key_file() {
            let mut file = fs::File::open(path).map_err(|source| KdbxImportError::Io {
                operation: "open KDBX key file",
                path: path.to_owned(),
                source,
            })?;
            key = key
                .with_keyfile(&mut file)
                .map_err(|source| KdbxImportError::Io {
                    operation: "read KDBX key file",
                    path: path.to_owned(),
                    source,
                })?;
        }
        let database = Database::open(&mut source, key).map_err(KdbxImportError::Open)?;
        let items = collect_items(&database)?;
        let writer = VaultWriter::new(self.repository, self.keys);
        let mut outcome = KdbxImportOutcome::default();

        for item in items {
            let path = item.path.to_string();
            let exists = writer.entry_exists(&path)?;
            if exists && request.mode() == KdbxImportMode::QuickAdd {
                outcome.skipped += 1;
                continue;
            }
            let mut committer =
                AutomaticEntryCommitter::for_entry(self.repository, &path, identity.clone())?;
            if exists {
                let session = writer.begin_edit(&EditRequest { entry: path }, secrets)?;
                if session.plaintext().expose() == item.contents.expose() {
                    outcome.unchanged += 1;
                    continue;
                }
                writer.finish_edit(
                    session,
                    item.contents,
                    "IronStorage KDBX importer",
                    None,
                    &mut committer,
                )?;
                outcome.updated += 1;
            } else {
                writer.insert(
                    &InsertRequest {
                        entry: path,
                        input: InsertInput::Multiline,
                        force: false,
                    },
                    InsertContent::multiline(item.contents.expose().to_vec()),
                    OverwriteDecision::Decline,
                    None,
                    &mut committer,
                )?;
                outcome.added += 1;
            }
            outcome.changed_paths.push(item.path);
        }
        Ok(outcome)
    }
}

struct ImportItem {
    path: EntryPath,
    contents: SecretBytes,
}

fn collect_items(database: &Database) -> Result<Vec<ImportItem>, KdbxImportError> {
    let mut items = Vec::new();
    let mut used = BTreeSet::new();
    let recycle_bin = database.recycle_bin().map(|group| group.id());
    collect_group(database.root(), &[], recycle_bin, &mut used, &mut items)?;
    Ok(items)
}

fn collect_group(
    group: GroupRef<'_>,
    components: &[String],
    recycle_bin: Option<keepass::db::GroupId>,
    used: &mut BTreeSet<PathBuf>,
    items: &mut Vec<ImportItem>,
) -> Result<(), KdbxImportError> {
    for entry in group.entries() {
        collect_entry(entry, components, used, items)?;
    }
    for child in group.groups() {
        if Some(child.id()) == recycle_bin {
            continue;
        }
        let mut child_components = components.to_vec();
        let name = sanitize_component(&child.name);
        if !name.is_empty() {
            child_components.push(name);
        }
        collect_group(child, &child_components, recycle_bin, used, items)?;
    }
    Ok(())
}

fn collect_entry(
    entry: EntryRef<'_>,
    components: &[String],
    used: &mut BTreeSet<PathBuf>,
    items: &mut Vec<ImportItem>,
) -> Result<(), KdbxImportError> {
    let title = entry_title(&entry);
    let attachments = entry
        .attachments_named()
        .map(|(name, attachment)| (name.to_owned(), attachment.data.get().to_vec()))
        .collect::<Vec<_>>();
    let attachment_names = attachments
        .iter()
        .map(|(name, _)| name.as_str())
        .collect::<Vec<_>>();
    items.push(ImportItem {
        path: reserve_path(components, &title, used)?,
        contents: render_entry(&entry, &attachment_names),
    });

    for (name, data) in attachments {
        items.push(ImportItem {
            path: reserve_path(components, &sanitize_component(&name), used)?,
            contents: SecretBytes::new(data),
        });
    }

    let history_len = entry
        .history
        .as_ref()
        .map_or(0, |history| history.get_entries().len());
    if history_len != 0 {
        let mut history_components = vec!["History".to_owned()];
        history_components.extend_from_slice(components);
        for index in 0..history_len {
            if let Some(historical) = entry.historical(index) {
                items.push(ImportItem {
                    path: reserve_path(&history_components, &entry_title(&historical), used)?,
                    contents: render_entry(&historical, &[]),
                });
            }
        }
    }
    Ok(())
}

fn reserve_path(
    components: &[String],
    title: &str,
    used: &mut BTreeSet<PathBuf>,
) -> Result<EntryPath, RepositoryError> {
    let mut path = components.iter().collect::<PathBuf>();
    let title = if title.is_empty() { "notitle" } else { title };
    path.push(title);
    let original = path.clone();
    let mut suffix = 1_u64;
    while !used.insert(path.clone()) {
        path = original.clone();
        path.set_file_name(format!("{title}-{suffix}"));
        suffix = suffix.saturating_add(1);
    }
    EntryPath::parse(path)
}

fn entry_title(entry: &Entry) -> String {
    let title = entry.get_title().filter(|title| !title.trim().is_empty());
    let candidate = title
        .or_else(|| entry.get_url().filter(|url| !url.trim().is_empty()))
        .or_else(|| {
            entry
                .get_username()
                .filter(|login| !login.trim().is_empty())
        })
        .unwrap_or("notitle");
    let candidate = if title.is_none() {
        url::Url::parse(candidate)
            .ok()
            .and_then(|url| url.host_str().map(str::to_owned))
            .unwrap_or_else(|| candidate.to_owned())
    } else {
        candidate.to_owned()
    };
    sanitize_component(&candidate)
}

fn sanitize_component(value: &str) -> String {
    let cleaned = value
        .chars()
        .map(|character| {
            if character.is_control() || "<>:\"/\\|?*".contains(character) {
                '-'
            } else {
                character
            }
        })
        .collect::<String>();
    match cleaned.trim() {
        "." | ".." => "-".to_owned(),
        cleaned => cleaned.to_owned(),
    }
}

fn render_entry(entry: &Entry, attachment_names: &[&str]) -> SecretBytes {
    let mut output = Vec::new();
    let password = normalize_newlines(entry.get_password().unwrap_or_default());
    let first_line = password
        .split_once('\n')
        .map_or(password.as_str(), |(first, _)| first);
    output.extend_from_slice(first_line.as_bytes());
    output.push(b'\n');

    if password.contains('\n') {
        append_field(&mut output, "password_multiline", &password);
    }
    append_field(
        &mut output,
        "login",
        entry.get_username().unwrap_or_default(),
    );
    append_field(&mut output, "url", entry.get_url().unwrap_or_default());

    let otp = otp_uri(entry);
    if let Some(otp) = &otp {
        output.extend_from_slice(otp.as_bytes());
        output.push(b'\n');
    }
    append_field(
        &mut output,
        "comments",
        entry.get(fields::NOTES).unwrap_or_default(),
    );
    if !attachment_names.is_empty() {
        append_field(&mut output, "attachments", &attachment_names.join(", "));
    }
    if !entry.tags.is_empty() {
        append_field(&mut output, "tags", &entry.tags.join(", "));
    }
    if let Some(autotype) = &entry.autotype {
        append_field(
            &mut output,
            "autotype_enabled",
            if autotype.enabled { "true" } else { "false" },
        );
        append_field(
            &mut output,
            "autotype_sequence",
            autotype.default_sequence.as_deref().unwrap_or_default(),
        );
        if !autotype.associations.is_empty() {
            let associations = autotype
                .associations
                .iter()
                .map(|association| format!("{} => {}", association.window, association.sequence))
                .collect::<Vec<_>>()
                .join("\n");
            append_field(&mut output, "autotype_associations", &associations);
        }
    }
    if let Some(icon) = entry.icon() {
        let icon = match icon {
            Icon::BuiltIn(index) => index.to_string(),
            Icon::Custom(id) => id.to_string(),
        };
        append_field(&mut output, "icon", &icon);
    }

    let mut custom = entry
        .fields
        .iter()
        .filter(|(name, value)| {
            !fields::KNOWN_FIELDS.contains(&name.as_str())
                && !(name.eq_ignore_ascii_case(fields::OTP) && otp.is_some())
                && !value.get().is_empty()
        })
        .map(|(name, value)| (field_name(name), value.get().as_str()))
        .collect::<Vec<_>>();
    custom.sort_by_cached_key(|(name, _)| name.to_lowercase());
    let mut names = BTreeSet::from([
        "attachments".to_owned(),
        "autotype_associations".to_owned(),
        "autotype_enabled".to_owned(),
        "autotype_sequence".to_owned(),
        "comments".to_owned(),
        "icon".to_owned(),
        "login".to_owned(),
        "password_multiline".to_owned(),
        "tags".to_owned(),
        "url".to_owned(),
    ]);
    for (mut name, value) in custom {
        let original = name.clone();
        let mut suffix = 2_u64;
        while !names.insert(name.to_lowercase()) {
            name = format!("{original}_{suffix}");
            suffix = suffix.saturating_add(1);
        }
        append_field(&mut output, &name, value);
    }
    SecretBytes::new(output)
}

fn field_name(value: &str) -> String {
    let name = value
        .chars()
        .map(|character| {
            if character == ':' || character.is_whitespace() || character.is_control() {
                '_'
            } else {
                character
            }
        })
        .collect::<String>();
    if name.is_empty() {
        "field".to_owned()
    } else {
        name
    }
}

fn append_field(output: &mut Vec<u8>, name: &str, value: &str) {
    if value.is_empty() {
        return;
    }
    output.extend_from_slice(name.as_bytes());
    output.extend_from_slice(b": ");
    let value = normalize_newlines(value);
    for byte in value.bytes() {
        output.push(byte);
        if byte == b'\n' {
            output.push(b' ');
        }
    }
    output.push(b'\n');
}

fn normalize_newlines(value: &str) -> String {
    value.replace("\r\n", "\n").replace('\r', "\n")
}

fn otp_uri(entry: &Entry) -> Option<String> {
    let raw = entry
        .get(fields::OTP)
        .filter(|value| !value.trim().is_empty());
    if let Some(raw) = raw
        && raw.starts_with("otpauth://")
        && crate::otp::OtpUri::parse_str(raw).is_ok()
    {
        return Some(raw.to_owned());
    }
    let secret = raw
        .filter(|value| !value.starts_with("otpauth://"))
        .or_else(|| entry.get("TimeOtp-Secret-Base32"))
        .or_else(|| entry.get("TOTP Seed"))?;
    let secret = secret
        .chars()
        .filter(|character| !character.is_whitespace())
        .collect::<String>();
    let title = entry
        .get_title()
        .filter(|title| !title.is_empty())
        .unwrap_or("Imported");
    let mut uri = url::Url::parse("otpauth://totp/Imported").expect("static OTP URI is valid");
    uri.set_path(title);
    {
        let mut query = uri.query_pairs_mut();
        query.append_pair("secret", &secret);
        query.append_pair("issuer", "Imported");
        query.append_pair(
            "digits",
            entry
                .get("TimeOtp-Length")
                .or_else(|| (entry.get("TOTP Settings") == Some("30;S")).then_some("6"))
                .unwrap_or("6"),
        );
        query.append_pair("period", entry.get("TimeOtp-Period").unwrap_or("30"));
        if let Some(algorithm) = entry.get("TimeOtp-Algorithm") {
            let algorithm = algorithm.replace("HMAC-", "").replace('-', "");
            query.append_pair("algorithm", &algorithm);
        }
    }
    let uri = uri.to_string();
    crate::otp::OtpUri::parse_str(&uri).ok().map(|_| uri)
}

#[derive(Debug)]
pub enum KdbxImportError {
    Io {
        operation: &'static str,
        path: PathBuf,
        source: std::io::Error,
    },
    PasswordEncoding,
    Open(keepass::db::DatabaseOpenError),
    Repository(RepositoryError),
    Write(WriteError),
    Git(GitError),
}

impl fmt::Display for KdbxImportError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io {
                operation, path, ..
            } => write!(formatter, "cannot {operation}: {}", path.display()),
            Self::PasswordEncoding => formatter.write_str("KDBX password is not valid UTF-8"),
            Self::Open(_) => formatter.write_str("cannot decrypt or parse the KDBX database"),
            Self::Repository(error) => error.fmt(formatter),
            Self::Write(error) => error.fmt(formatter),
            Self::Git(error) => error.fmt(formatter),
        }
    }
}

impl Error for KdbxImportError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Io { source, .. } => Some(source),
            Self::Open(error) => Some(error),
            Self::Repository(error) => Some(error),
            Self::Write(error) => Some(error),
            Self::Git(error) => Some(error),
            Self::PasswordEncoding => None,
        }
    }
}

impl From<RepositoryError> for KdbxImportError {
    fn from(error: RepositoryError) -> Self {
        Self::Repository(error)
    }
}

impl From<WriteError> for KdbxImportError {
    fn from(error: WriteError) -> Self {
        Self::Write(error)
    }
}

impl From<GitError> for KdbxImportError {
    fn from(error: GitError) -> Self {
        Self::Git(error)
    }
}
