use std::collections::{BTreeSet, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};

use serde::{
    de::{self, Deserializer, Visitor},
    Deserialize, Serialize,
};

use crate::coalesce::Settings;
use crate::env_matcher::EnvMatcher;
use crate::label_matcher::LabelMatcher;
use crate::sockaddr::SocketAddrMatcher;

fn default_state_file() -> Option<PathBuf> {
    Some(Path::new("state").into())
}

fn deserialize_state_file<'de, D>(d: D) -> Result<Option<PathBuf>, D::Error>
where
    D: Deserializer<'de>,
{
    match PathBuf::deserialize(d)? {
        p if p == PathBuf::default() => Ok(None),
        p => Ok(Some(p)),
    }
}

const fn default_state_max_age() -> u64 {
    60
}

#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Statefile {
    #[serde(
        default = "default_state_file",
        deserialize_with = "deserialize_state_file"
    )]
    pub file: Option<PathBuf>,
    #[serde(default)]
    pub generations: u64,
    #[serde(rename = "max-age")]
    #[serde(default = "default_state_max_age")]
    pub max_age: u64,
    #[serde(rename = "write-state-period")]
    pub write_state_period: Option<u64>,
}

impl Default for Statefile {
    fn default() -> Self {
        Self {
            file: default_state_file(),
            generations: 0,
            max_age: default_state_max_age(),
            write_state_period: None,
        }
    }
}

#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Logfile {
    #[serde(default)]
    pub file: PathBuf,
    #[serde(rename = "read-users")]
    pub users: Option<Vec<String>>,
    #[serde(rename = "read-groups")]
    pub groups: Option<Vec<String>>,
    #[serde(default, rename = "read-other")]
    pub other: bool,
    pub size: Option<u64>,
    pub generations: Option<u64>,
    #[serde(rename = "line-prefix")]
    pub line_prefix: Option<String>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct Debug {
    pub inputlog: Option<Logfile>,
    #[serde(rename = "parse-error-log")]
    pub parse_error_log: Option<Logfile>,
}

#[derive(PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArrayOrString {
    Array,
    String,
}

fn execve_argv_default() -> BTreeSet<ArrayOrString> {
    [ArrayOrString::Array].into()
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Transform {
    #[serde(default = "execve_argv_default", rename = "execve-argv")]
    pub execve_argv: BTreeSet<ArrayOrString>,
    #[serde(default, rename = "execve-argv-limit-bytes")]
    pub execve_argv_limit_bytes: Option<usize>,
}

impl Default for Transform {
    fn default() -> Self {
        Transform {
            execve_argv: execve_argv_default(),
            execve_argv_limit_bytes: None,
        }
    }
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Translate {
    #[serde(default)]
    pub universal: bool,
    #[serde(default, rename = "user-db")]
    pub userdb: bool,
    #[serde(default, rename = "drop-raw")]
    pub drop_raw: bool,
}

fn execve_env_default() -> Vec<String> {
    ["LD_PRELOAD", "LD_LIBRARY_PATH"]
        .into_iter()
        .map(String::from)
        .collect()
}

fn true_value() -> bool {
    true
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Enrich {
    #[serde(default = "execve_env_default", rename = "execve-env")]
    pub execve_env: Vec<String>,
    #[serde(default = "true_value")]
    pub container: bool,
    #[serde(default)]
    pub container_info: bool,
    #[serde(default = "true_value")]
    pub systemd: bool,
    #[serde(default = "true_value")]
    pub pid: bool,
    #[serde(default = "true_value", rename = "spawned-by")]
    pub spawned_by: bool,
    #[serde(default, rename = "ppid-verbose")]
    pub ppid_verbose: bool,
    #[serde(default = "true_value")]
    pub script: bool,
    #[serde(default = "true_value", rename = "uid-groups")]
    pub uid_groups: bool,
    #[serde(default)]
    pub prefix: Option<String>,
    #[serde(default, rename = "exe-hash")]
    pub exe_hash: bool,
    #[serde(
        default = "default_exe_hash_size_limit",
        rename = "exe-hash-size-limit"
    )]
    pub exe_hash_size_limit: u64,
    #[serde(
        default = "default_exe_hash_cache_entries",
        rename = "exe-hash-cache-entries"
    )]
    pub exe_hash_cache_entries: usize,
}

fn default_exe_hash_size_limit() -> u64 {
    10_000_000
}

fn default_exe_hash_cache_entries() -> usize {
    1024
}

impl Default for Enrich {
    fn default() -> Self {
        Enrich {
            execve_env: execve_env_default(),
            container: true,
            container_info: false,
            systemd: true,
            pid: true,
            ppid_verbose: false,
            spawned_by: true,
            script: true,
            uid_groups: true,
            prefix: None,
            exe_hash: false,
            exe_hash_size_limit: default_exe_hash_size_limit(),
            exe_hash_cache_entries: default_exe_hash_cache_entries(),
        }
    }
}

fn default_32() -> usize {
    32
}
fn default_4096() -> usize {
    4096
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LabelProcess {
    #[serde(default, rename = "label-keys")]
    pub label_keys: HashSet<String>,
    #[serde(default, rename = "label-exe")]
    pub label_exe: Option<LabelMatcher>,
    #[serde(default, rename = "unlabel-exe")]
    pub unlabel_exe: Option<LabelMatcher>,
    #[serde(default, rename = "label-argv")]
    pub label_argv: Option<LabelMatcher>,
    #[serde(default, rename = "unlabel-argv")]
    pub unlabel_argv: Option<LabelMatcher>,
    #[serde(default = "default_4096", rename = "label-argv-bytes")]
    pub label_argv_bytes: usize,
    #[serde(default = "default_32", rename = "label-argv-count")]
    pub label_argv_count: usize,
    #[serde(default, rename = "label-script")]
    pub label_script: Option<LabelMatcher>,
    #[serde(default, rename = "unlabel-script")]
    pub unlabel_script: Option<LabelMatcher>,
    #[serde(default, rename = "propagate-labels")]
    pub propagate_labels: HashSet<String>,
}

impl Default for LabelProcess {
    fn default() -> Self {
        Self {
            label_keys: HashSet::default(),
            label_exe: None,
            unlabel_exe: None,
            label_argv: None,
            unlabel_argv: None,
            label_argv_bytes: 4096,
            label_argv_count: 32,
            label_script: None,
            unlabel_script: None,
            propagate_labels: HashSet::default(),
        }
    }
}

#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FilterAction {
    #[default]
    #[serde(alias = "Drop")]
    Drop,
    #[serde(alias = "Log")]
    Log,
}

pub(crate) mod regex_set {
    use regex::bytes::RegexSet;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    pub(crate) fn serialize<S>(v: &RegexSet, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        v.patterns().to_vec().serialize(s)
    }
    pub(crate) fn deserialize<'de, D>(d: D) -> Result<RegexSet, D::Error>
    where
        D: Deserializer<'de>,
    {
        let v: Vec<String> = Deserialize::deserialize(d)?;
        RegexSet::new(v).map_err(serde::de::Error::custom)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Filter {
    #[serde(default, rename = "filter-keys")]
    pub filter_keys: HashSet<String>,
    #[serde(default, rename = "filter-labels")]
    pub filter_labels: HashSet<String>,
    #[serde(default, rename = "filter-raw-lines", with = "regex_set")]
    pub filter_raw_lines: regex::bytes::RegexSet,
    #[serde(default, rename = "filter-null-keys")]
    pub filter_null_keys: bool,
    #[serde(default, rename = "filter-sockaddr")]
    pub filter_sockaddr: Vec<SocketAddrMatcher>,
    #[serde(default, rename = "filter-action")]
    pub filter_action: FilterAction,
    #[serde(default = "true_value", rename = "keep-first-per-process")]
    pub keep_first_per_process: bool,
}

impl Default for Filter {
    fn default() -> Self {
        Self {
            filter_keys: HashSet::default(),
            filter_labels: HashSet::default(),
            filter_raw_lines: regex::bytes::RegexSet::default(),
            filter_null_keys: false,
            filter_sockaddr: Vec::default(),
            filter_action: FilterAction::default(),
            keep_first_per_process: true,
        }
    }
}

#[derive(Debug, Serialize, Default)]
pub enum Input {
    #[default]
    Stdin,
    Unix(PathBuf),
}

impl std::fmt::Display for Input {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        match self {
            Input::Stdin => write!(fmt, "stdin"),
            Input::Unix(p) => write!(fmt, "unix:{}", p.to_string_lossy()),
        }
    }
}

impl<'de> Deserialize<'de> for Input {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_str(InputVisitor {})
    }
}

struct InputVisitor {}

impl Visitor<'_> for InputVisitor {
    type Value = Input;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "an input specification string")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        if s == "stdin" {
            Ok(Input::Stdin)
        } else if let Some(s) = s.strip_prefix("unix:") {
            let p = Path::new(s).to_path_buf();
            Ok(Input::Unix(p))
        } else {
            Err(de::Error::custom("unrecognized input specification"))
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
    pub user: Option<String>,
    pub directory: Option<PathBuf>,
    #[serde(default)]
    pub input: Input,
    #[serde(default, rename = "statusreport-period")]
    pub statusreport_period: Option<u64>,
    #[serde(default)]
    pub marker: Option<String>,
    #[serde(default)]
    pub state: Statefile,
    #[serde(default)]
    pub auditlog: Logfile,
    #[serde(default)]
    pub filterlog: Logfile,
    #[serde(default)]
    pub debug: Debug,
    #[serde(default)]
    pub transform: Transform,
    #[serde(default)]
    pub translate: Translate,
    #[serde(default)]
    pub enrich: Enrich,
    #[serde(default, rename = "label-process")]
    pub label_process: LabelProcess,
    #[serde(default)]
    pub filter: Filter,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            user: None,
            directory: Some(".".into()),
            input: Input::Stdin,
            statusreport_period: None,
            marker: None,
            state: Statefile {
                file: Some("state".into()),
                generations: 3,
                max_age: 60,
                write_state_period: None,
            },
            auditlog: Logfile {
                file: "audit.log".into(),
                size: Some(10 * 1024 * 1024),
                generations: Some(5),
                ..Logfile::default()
            },
            filterlog: Logfile {
                file: "filtered.log".into(),
                size: Some(10 * 1024 * 1024),
                generations: Some(5),
                ..Logfile::default()
            },
            debug: Debug::default(),
            transform: Transform::default(),
            translate: Translate::default(),
            enrich: Enrich::default(),
            label_process: LabelProcess::default(),
            filter: Filter::default(),
        }
    }
}

impl std::fmt::Display for Config {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(
            fmt,
            "({}user={} directory={} statusreport-period={} file={} users={} size={} generations={})",
            self.marker.as_ref().map(|m| format!("marker={m} ")).unwrap_or("".to_string()),
            self.user.clone().unwrap_or_else(|| "n/a".to_string()),
            self.directory
                .clone()
                .unwrap_or_else(|| PathBuf::from("."))
                .display(),
            self.statusreport_period.unwrap_or(0),
            self.auditlog.file.to_string_lossy(),
            self.auditlog
                .users
                .clone()
                .unwrap_or_else(|| vec!["n/a".to_string()])
                .join(","),
            self.auditlog.size.unwrap_or(0),
            self.auditlog.generations.unwrap_or(0)
        )
    }
}

impl Config {
    pub fn make_coalesce_settings(&self) -> Settings {
        let (mut execve_prefix, execve_exact) = self
            .enrich
            .execve_env
            .iter()
            .map(|s| s.as_bytes().to_vec())
            .partition::<Vec<_>, _>(|s| s.ends_with(&b"*"[..]));
        for s in &mut execve_prefix {
            s.pop(); // strip "*"
        }
        let env_matcher = if execve_exact.is_empty() && execve_prefix.is_empty() {
            None
        } else {
            Some(EnvMatcher::new(execve_exact, execve_prefix))
        };
        Settings {
            execve_argv_list: self.transform.execve_argv.contains(&ArrayOrString::Array),
            execve_argv_string: self.transform.execve_argv.contains(&ArrayOrString::String),
            execve_argv_limit_bytes: self.transform.execve_argv_limit_bytes,
            env_matcher,
            enrich_container: self.enrich.container,
            enrich_container_info: self.enrich.container_info,
            enrich_systemd: self.enrich.systemd,
            enrich_pid: self.enrich.pid,
            enrich_ppid_verbose: self.enrich.ppid_verbose,
            enrich_spawned_by: self.enrich.spawned_by,
            enrich_script: self.enrich.script,
            enrich_uid_groups: self.enrich.uid_groups,
            enrich_exe_hash: self.enrich.exe_hash,
            enrich_exe_hash_size_limit: self.enrich.exe_hash_size_limit,
            enrich_exe_hash_cache_entries: self.enrich.exe_hash_cache_entries,
            enrich_prefix: self.enrich.prefix.clone(),
            proc_label_keys: self
                .label_process
                .label_keys
                .iter()
                .map(|s| s.as_bytes().to_vec())
                .collect(),
            proc_propagate_labels: self
                .label_process
                .propagate_labels
                .iter()
                .map(|s| s.as_bytes().to_vec())
                .collect(),
            translate_universal: self.translate.universal,
            translate_userdb: self.translate.userdb,
            drop_translated: self.translate.drop_raw,
            label_exe: self.label_process.label_exe.clone(),
            unlabel_exe: self.label_process.unlabel_exe.clone(),
            label_argv: self.label_process.label_argv.clone(),
            unlabel_argv: self.label_process.unlabel_argv.clone(),
            label_argv_bytes: self.label_process.label_argv_bytes,
            label_argv_count: self.label_process.label_argv_count,
            label_script: self.label_process.label_script.clone(),
            unlabel_script: self.label_process.unlabel_script.clone(),
            filter_keys: self
                .filter
                .filter_keys
                .iter()
                .map(|s| s.as_bytes().to_vec())
                .collect(),
            filter_labels: self
                .filter
                .filter_labels
                .iter()
                .map(|s| s.as_bytes().to_vec())
                .collect(),
            filter_null_keys: self.filter.filter_null_keys,
            filter_sockaddr: self.filter.filter_sockaddr.clone(),
            filter_raw_lines: self.filter.filter_raw_lines.clone(),
            filter_first_per_process: !self.filter.keep_first_per_process,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use std::path::Path;

    #[test]
    fn simple() {
        let c: Config = toml::de::from_str(
            r#"
user = "somebody"
directory = "/path/to/somewhere"
statusreport-period = 86400
[auditlog]
file = "somefile"
read-users = ["splunk"]
[state]
"#,
        )
        .unwrap();
        println!("{:#?}", &c);
        assert_eq!(c.user, Some("somebody".to_string()));
        assert_eq!(
            c.directory,
            Some(Path::new("/path/to/somewhere").to_path_buf())
        );
        assert_eq!(c.statusreport_period, Some(86400));
        assert_eq!(
            c.auditlog,
            Logfile {
                file: Path::new("somefile").to_path_buf(),
                users: Some(vec!["splunk".to_string()]),
                ..Logfile::default()
            }
        );
        assert_eq!(
            c.state,
            Statefile {
                file: Some(Path::new("state").into()),
                ..Statefile::default()
            }
        );
    }

    #[test]
    fn parse_defaults() {
        let cfg_default = toml::de::from_str::<Config>("").unwrap();
        println!("{}", toml::to_string(&cfg_default).unwrap());

        println!("--------------------");

        let cfg_empty_sections = toml::de::from_str::<Config>(
            r#"
[auditlog]
[filterlog]
[state]
[debug]
[transform]
[translate]
[enrich]
[label-process]
[filter]
"#,
        )
        .unwrap();
        println!("{}", toml::to_string(&cfg_empty_sections).unwrap());

        assert!(toml::to_string(&cfg_default) == toml::to_string(&cfg_empty_sections));
    }

    #[test]
    fn statefile() {
        let cfg: Config = toml::de::from_str(
            r#"
[state] 
file = ""
"#,
        )
        .expect("toml parse error");
        assert_eq!(cfg.state.file, None);
    }
}
