use std::env::temp_dir;
use std::fs::File;
use std::io::{BufReader, Read};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::{bail, Result};
use clap::Parser;
use dfir_windows_types::Guid;
use libesedb::EseDb;
use libntdsextract2::cli::{
    AclCommand, Args, Commands, EntrySearchField, OutputOptions, SdDisplayOption,
};
use libntdsextract2::{
    use_member_of_attribute, CDatabase, CsvSerialization, EntryId, EsedbInfo, JsonSerialization,
};
use sddl::{AccessMaskFlag, Contains};
use sha1::{Digest, Sha1};
use simplelog::{Config, TermLogger};

mod progress_bar;
mod walk;

use cap::Cap;
use std::{alloc, fs};

use crate::walk::run_tui;

macro_rules! do_with_serialization {
    ($cmd: expr, $db: expr, $function: ident, $options: expr) => {
        if $cmd.flat_serialization() {
            $db.$function::<CsvSerialization>($options)
        } else {
            $db.$function::<JsonSerialization>($options)
        }
    };
}

#[global_allocator]
static ALLOCATOR: Cap<alloc::System> = Cap::new(alloc::System, usize::MAX);

fn main() -> Result<()> {
    ALLOCATOR.set_limit(4096 * 1024 * 1024).unwrap();

    let cli = Args::parse();
    if !matches!(cli.command(), Commands::Walk { .. }) {
        let _ = TermLogger::init(
            cli.verbose().log_level_filter(),
            Config::default(),
            simplelog::TerminalMode::Stderr,
            simplelog::ColorChoice::Auto,
        );
    }

    let ntds_path = Path::new(cli.ntds_file());
    if !(ntds_path.exists() && ntds_path.is_file()) {
        eprintln!("unable to open '{}'", cli.ntds_file());
        std::process::exit(-1);
    }

    let esedb = EseDb::open(cli.ntds_file())?;

    if matches!(cli.command(), Commands::Info {}) {
        EsedbInfo::print_metainfo(&esedb)?;
        return Ok(());
    }

    let info = EsedbInfo::try_from(&esedb)?;

    let cache_file = if *cli.use_cache() {
        if let Some(cache_file) = cli.cache_file() {
            let cache_file = PathBuf::from(cache_file).canonicalize()?;

            if !cache_file.exists() {
                // the cache file does not exist, so we need to create one.
                // Therefore, the parent directory must be writable
                if let Some(parent) = cache_file.parent() {
                    let permissions = fs::metadata(parent)?.permissions();
                    if permissions.readonly() {
                        bail!(
                            "directory '{}' must be writable in order to store a cache file there",
                            parent.to_string_lossy()
                        );
                    }

                    let permissions = fs::metadata(&cache_file)?.permissions();
                    if permissions.mode() & 0o177 != 0 {
                        bail!(
                            "cache file '{}' has too lose permissions set",
                            cache_file.to_string_lossy()
                        );
                    }
                    Some(cache_file)
                } else {
                    bail!("invalid path: '{}'", cache_file.to_string_lossy());
                }
            } else {
                if !cache_file.is_file() {
                    bail!(
                        "Path '{}' does not point to a file",
                        cache_file.to_string_lossy()
                    );
                }
                Some(cache_file)
            }
        } else {
            // read the first 4k bytes from the database to create a simple fingerprint
            let buffer = {
                let mut reader = BufReader::new(File::open(cli.ntds_file()).unwrap());
                let mut buffer = [0; 4096];
                let _ = reader.read(&mut buffer)?;
                buffer
            };
            let hash = {
                let mut hasher = Sha1::new();
                hasher.update(buffer);
                let result = hasher.finalize();
                hex::encode(&result[0..8])
            };

            let mut cache_file = temp_dir();
            cache_file.push(format!("ntdsextract2_cache_{}", hash));
            if cache_file.exists() {
                let permissions = fs::metadata(&cache_file)?.permissions();
                if permissions.mode() & 0o177 != 0 {
                    bail!(
                        "cache file '{}' has too loose permissions set: {:o}",
                        cache_file.to_string_lossy(),
                        permissions.mode()
                    );
                }
            }
            Some(cache_file)
        }
    } else {
        None
    };

    // always load sd_table if the user wants to show security desriptors
    // or if any acl subcommand is being used
    let database = CDatabase::new(
        &info,
        cli.command().show_security_descriptor() != SdDisplayOption::Hide
            || matches!(cli.command(), Commands::Acl { .. })
            || matches!(cli.command(), Commands::Walk { .. }),
        cache_file,
    )?;

    let mut options = OutputOptions::default();
    options.set_display_all_attributes(cli.command().display_all_attributes());
    options.set_flat_serialization(cli.command().flat_serialization());
    options.set_format(cli.command().format());
    options.set_include_dn(cli.command().include_dn());
    options.set_show_security_descriptor(cli.command().show_security_descriptor());

    use_member_of_attribute(cli.command().member_of_attribute());

    match cli.command() {
        Commands::Group { .. } => {
            do_with_serialization!(cli.command(), database, show_groups, &options)
        }
        Commands::User { .. } => {
            do_with_serialization!(cli.command(), database, show_users, &options)
        }
        Commands::Computer { .. } => {
            do_with_serialization!(cli.command(), database, show_computers, &options)
        }
        Commands::Types { .. } => {
            do_with_serialization!(cli.command(), database, show_type_names, &options)
        }
        Commands::Timeline {
            all_objects,
            include_deleted,
            format,
        } => {
            options.set_show_all_objects(*all_objects);
            database.show_timeline(&options, *include_deleted, format)
        }
        Commands::Tree { max_depth } => Ok(database.show_tree(*max_depth)?),
        Commands::Entry {
            search_field,
            id,
            entry_format,
        } => {
            let entry_id = match search_field {
                EntrySearchField::EntryId => EntryId::Id(id.parse::<i32>()?.into()),
                EntrySearchField::Sid => EntryId::Rid(id.parse::<u32>()?),
                EntrySearchField::Guid => EntryId::Guid(Guid::from_str(id)?),
            };
            Ok(database.show_entry(entry_id, *entry_format)?)
        }
        Commands::Search { regex, ignore_case } => {
            let regex = if *ignore_case {
                format!("(?i:{regex})")
            } else {
                regex.to_owned()
            };
            database.search_entries(&regex)
        }
        Commands::Acl { acl_command } => {
            match acl_command {
                AclCommand::GA { format, include_dn } => {
                    database.show_objects_by_permission(*include_dn, format, |ace| {
                                    if ace.header().mask().contains(AccessMaskFlag::GENERIC_ALL)
                                                                //
                                                                // the following flags are set by AD
                                                                // when you grant GA to some user
                                                                    | ace.header().mask().contains(
                                                                        AccessMaskFlag::WRITE_OWNER
                                                                            | AccessMaskFlag::WRITE_DACL
                                                                            | AccessMaskFlag::READ_CONTROL
                                                                            | AccessMaskFlag::STANDARD_DELETE
                                                                            | AccessMaskFlag::CONTROL_ACCESS
                                                                            | AccessMaskFlag::LIST_OBJECT
                                                                            | AccessMaskFlag::DELETE_TREE
                                                                            | AccessMaskFlag::WRITE_PROPERTY
                                                                            | AccessMaskFlag::READ_PROPERTY
                                                                            | AccessMaskFlag::SELF_WRITE
                                                                            | AccessMaskFlag::LIST_CHILDREN
                                                                            | AccessMaskFlag::DELETE_CHILD
                                                                            | AccessMaskFlag::CREATE_CHILD,
                                                                    )
                                    {
                                        Some(ace.sid().sid())
                                    } else {
                                        None
                                    }
                                })
                }
                AclCommand::AllCr { format, include_dn } => {
                    database.show_objects_by_permission(*include_dn, format, |ace| {
                        if ace.header().mask().contains(AccessMaskFlag::CONTROL_ACCESS)
                            && ace.can_have_object_type()
                            && ace.object_type().is_none()
                        {
                            return Some(ace.sid().sid());
                        }
                        None
                    })
                }
                AclCommand::DCSync { format, include_dn } => {
                    database.show_objects_by_permission(*include_dn, format, |ace| {
                        if ace.header().mask().contains(AccessMaskFlag::CONTROL_ACCESS) {
                            if let Some(object_guid) = ace.object_type() {
                                if object_guid
                                    == &Guid::from_str("89e95b76-444d-4c62-991a-0facbeda640c")
                                        .unwrap()
                                {
                                    return Some(ace.sid().sid());
                                }
                            }
                        }
                        None
                    })
                }
                AclCommand::Hidden { include_dn, format } => {
                    database.show_hidden_objects(*include_dn, format)
                }
                AclCommand::ExtendedRights { command } => command.run(&database),
            }
        }
        Commands::Walk {} => run_tui(database),
        Commands::Info {} => Ok(()),
    }
}
