use std::{
    fs::{self, File},
    io::{BufReader, BufWriter},
    os::unix::fs::PermissionsExt,
    path::PathBuf,
    sync::Arc,
};

use dfir_windows_types::Sid;
use getset::Getters;
use serde::Serialize;

use crate::{
    cache::{self, DataEntryCore, MetaDataCache, RecordId},
    cli::{EntryFormat, OutputOptions, TimelineFormat},
    ntds::{
        self, Computer, DataTable, DataTableRecord, Group, LinkTable, ObjectType, Person, Schema,
        SdTable,
    },
    object_tree::ObjectTree,
    EntryId, EsedbInfo, SerializationType,
};

#[derive(Getters)]
#[getset(get = "pub")]
pub struct CDatabase<'info, 'db> {
    esedbinfo: &'info EsedbInfo<'db>,
    data_table: DataTable<'info, 'db>,
    link_table: Arc<LinkTable>,
    sd_table: Option<Arc<SdTable>>,
}

impl<'info, 'db> CDatabase<'info, 'db> {
    pub fn new(
        esedbinfo: &'info EsedbInfo<'db>,
        load_sd_table: bool,
        cache_file: Option<PathBuf>,
    ) -> anyhow::Result<Self> {
        let cached_sd_table = cache::SdTable::try_from("sd_table", esedbinfo)?;
        let sd_table = if load_sd_table {
            match SdTable::new(&cached_sd_table) {
                Ok(sd_table) => Some(Arc::new(sd_table)),
                Err(why) => {
                    log::warn!("Error while reading table 'sd_table': {why}. Security descriptors will not be available");
                    None
                }
            }
        } else {
            None
        };

        let metadata_cache = if let Some(cache_file) = cache_file {
            if cache_file.exists() {
                log::info!("using cache from {}", cache_file.to_string_lossy());
                rmp_serde::from_read(BufReader::new(File::open(&cache_file)?))?
            } else {
                log::warn!("cache file does not exist, creating it");
                let metadata_cache = MetaDataCache::try_from(esedbinfo)?;

                let file = BufWriter::new(File::create(&cache_file)?);
                let mut serializer = rmp_serde::encode::Serializer::new(file);
                metadata_cache.serialize(&mut serializer)?;

                let mut permissions = fs::metadata(&cache_file)?.permissions();
                permissions.set_mode(0o600);
                fs::set_permissions(&cache_file, permissions)?;

                metadata_cache
            }
        } else {
            MetaDataCache::try_from(esedbinfo)?
        };

        let object_tree = Arc::new(ObjectTree::new(&metadata_cache, sd_table.clone()));

        let special_records = object_tree.get_special_records()?;
        let schema_record_id = special_records.schema().record_ptr();
        log::debug!("found the schema record id is '{}'", schema_record_id);

        let schema = Schema::new(&metadata_cache, &special_records);

        let cached_data_table = cache::DataTable::new(
            esedbinfo.data_table(),
            "datatable",
            esedbinfo,
            metadata_cache,
        )?;

        let cached_link_table =
            cache::LinkTable::try_from(esedbinfo.link_table(), "link_table", esedbinfo)?;

        let link_table = Arc::new(LinkTable::new(
            cached_link_table,
            &cached_data_table,
            *schema_record_id,
        )?);

        let data_table = DataTable::new(
            cached_data_table,
            object_tree,
            *schema_record_id,
            Arc::clone(&link_table),
            sd_table.clone(),
            schema,
            special_records,
        )?;

        Ok(Self {
            esedbinfo,
            link_table,
            data_table,
            sd_table,
        })
    }

    pub fn show_users<T: SerializationType>(&self, options: &OutputOptions) -> anyhow::Result<()> {
        self.show_typed_objects::<Person<T>>(options, ObjectType::Person)
    }

    pub fn show_groups<T: SerializationType>(&self, options: &OutputOptions) -> anyhow::Result<()> {
        self.show_typed_objects::<Group<T>>(options, ObjectType::Group)
    }

    pub fn show_computers<T: SerializationType>(
        &self,
        options: &OutputOptions,
    ) -> anyhow::Result<()> {
        self.show_typed_objects::<Computer<T>>(options, ObjectType::Computer)
    }

    pub fn show_typed_objects<O: ntds::FromDataTable + ntds::IsMemberOf>(
        &self,
        options: &OutputOptions,
        object_type: ObjectType,
    ) -> anyhow::Result<()> {
        self.data_table
            .show_typed_objects::<O>(options, object_type)
    }

    pub fn show_type_names<T>(&self, options: &OutputOptions) -> anyhow::Result<()>
    where
        T: SerializationType,
    {
        self.data_table.show_type_names::<T>(options)
    }

    pub fn show_timeline(
        &self,
        options: &OutputOptions,
        include_deleted: bool,
        format: &TimelineFormat,
    ) -> anyhow::Result<()> {
        self.data_table
            .show_timeline(options, &self.link_table, include_deleted, format)
    }

    pub fn entry(
        &self,
        entry_id: EntryId,
    ) -> crate::ntds::Result<Option<DataTableRecord<'info, 'db>>> {
        self.data_table.entry(entry_id)
    }

    pub fn show_entry(
        &self,
        entry_id: EntryId,
        entry_format: EntryFormat,
    ) -> crate::ntds::Result<()> {
        self.data_table.show_entry(entry_id, entry_format)
    }

    pub fn show_tree(&self, max_depth: u8) -> crate::ntds::Result<()> {
        self.data_table.show_tree(max_depth)
    }

    pub fn object_tree(&self) -> &ObjectTree {
        self.data_table.object_tree()
    }

    pub fn search_entries(&self, regex: &str) -> anyhow::Result<()> {
        self.data_table.search_entries(regex)
    }

    pub fn show_objects_by_permission(
        &self,
        include_dn: bool,
        format: &crate::cli::OutputFormat,
        ace_filter: fn(&sddl::Ace) -> Option<&Sid>,
    ) -> Result<(), anyhow::Error> {
        self.data_table
            .show_objects_by_permission(include_dn, format, ace_filter)
    }

    pub fn show_hidden_objects(
        &self,
        include_dn: bool,
        format: &crate::cli::OutputFormat,
    ) -> Result<(), anyhow::Error> {
        self.data_table.show_hidden_objects(include_dn, format)
    }
    /*
    pub fn show_extended_rights (&self) {
        self.data_table.show_extended_rights(&self)
    }
     */

    pub fn record(&self, record_id: &RecordId) -> Option<&DataEntryCore> {
        self.data_table().data_table().metadata().record(record_id)
    }
}
