// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::loader::{
    MergedTargetConfigRecord, collect_merged_target_configs_compat_from_env, collect_merged_target_configs_from_env,
};
use crate::TargetError;
use crate::domain::TargetDomain;
use rustfs_config::server_config::{Config, KVS};
use std::collections::HashSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TargetPluginInstanceCompatDescriptor<'a> {
    pub domain: TargetDomain,
    pub plugin_id: &'a str,
    pub target_type: &'a str,
    pub subsystem: &'a str,
    pub route_prefix: &'a str,
    pub valid_fields: &'a [&'a str],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetInstanceSourceClass {
    Config,
    Env,
    Mixed,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct TargetInstanceSourceHints {
    pub has_file_default: bool,
    pub has_file_instance: bool,
    pub has_env_default: bool,
    pub has_env_instance: bool,
}

impl TargetInstanceSourceHints {
    #[inline]
    pub fn has_config_source(self) -> bool {
        self.has_file_default || self.has_file_instance
    }

    #[inline]
    pub fn has_env_source(self) -> bool {
        self.has_env_default || self.has_env_instance
    }

    #[inline]
    pub fn classification(self) -> TargetInstanceSourceClass {
        match (self.has_config_source(), self.has_env_source()) {
            (true, true) => TargetInstanceSourceClass::Mixed,
            (true, false) => TargetInstanceSourceClass::Config,
            (false, true) => TargetInstanceSourceClass::Env,
            (false, false) => TargetInstanceSourceClass::Config,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetPluginInstanceRecord {
    pub domain: TargetDomain,
    pub plugin_id: String,
    pub target_type: String,
    pub subsystem: String,
    pub instance_id: String,
    pub enabled: bool,
    pub source_hints: TargetInstanceSourceHints,
    pub effective_config: KVS,
}

pub type LegacyTargetInstanceDescriptor<'a> = TargetPluginInstanceCompatDescriptor<'a>;
pub type TargetPluginInstance = TargetPluginInstanceRecord;

pub fn normalize_target_plugin_instances(
    config: &Config,
    descriptor: &TargetPluginInstanceCompatDescriptor<'_>,
) -> Vec<TargetPluginInstanceRecord> {
    normalize_target_plugin_instances_from_env(config, descriptor, std::env::vars())
}

pub fn try_normalize_target_plugin_instances(
    config: &Config,
    descriptor: &TargetPluginInstanceCompatDescriptor<'_>,
) -> Result<Vec<TargetPluginInstanceRecord>, TargetError> {
    try_normalize_target_plugin_instances_from_env(config, descriptor, std::env::vars())
}

pub fn normalize_target_plugin_instances_from_env<I>(
    config: &Config,
    descriptor: &TargetPluginInstanceCompatDescriptor<'_>,
    env_vars: I,
) -> Vec<TargetPluginInstanceRecord>
where
    I: IntoIterator<Item = (String, String)>,
{
    let valid_fields = descriptor
        .valid_fields
        .iter()
        .map(|field| (*field).to_string())
        .collect::<HashSet<_>>();

    collect_merged_target_configs_compat_from_env(
        config,
        descriptor.subsystem,
        descriptor.route_prefix,
        descriptor.target_type,
        &valid_fields,
        env_vars,
    )
    .into_iter()
    .map(|record| target_plugin_instance_record(descriptor, record))
    .collect()
}

pub fn try_normalize_target_plugin_instances_from_env<I>(
    config: &Config,
    descriptor: &TargetPluginInstanceCompatDescriptor<'_>,
    env_vars: I,
) -> Result<Vec<TargetPluginInstanceRecord>, TargetError>
where
    I: IntoIterator<Item = (String, String)>,
{
    let valid_fields = descriptor
        .valid_fields
        .iter()
        .map(|field| (*field).to_string())
        .collect::<HashSet<_>>();

    Ok(collect_merged_target_configs_from_env(
        config,
        descriptor.subsystem,
        descriptor.route_prefix,
        descriptor.target_type,
        &valid_fields,
        env_vars,
    )?
    .into_iter()
    .map(|record| target_plugin_instance_record(descriptor, record))
    .collect())
}

fn target_plugin_instance_record(
    descriptor: &TargetPluginInstanceCompatDescriptor<'_>,
    record: MergedTargetConfigRecord,
) -> TargetPluginInstanceRecord {
    TargetPluginInstanceRecord {
        domain: descriptor.domain,
        plugin_id: descriptor.plugin_id.to_string(),
        target_type: descriptor.target_type.to_string(),
        subsystem: descriptor.subsystem.to_string(),
        instance_id: record.instance_id,
        enabled: record.enabled,
        source_hints: TargetInstanceSourceHints {
            has_file_default: record.has_file_default,
            has_file_instance: record.has_file_instance,
            has_env_default: record.has_env_default,
            has_env_instance: record.has_env_instance,
        },
        effective_config: record.effective_config,
    }
}

pub fn normalize_legacy_target_instances(
    config: &Config,
    descriptor: &LegacyTargetInstanceDescriptor<'_>,
) -> Vec<TargetPluginInstance> {
    normalize_target_plugin_instances(config, descriptor)
}

pub fn normalize_legacy_target_instances_from_env<I>(
    config: &Config,
    descriptor: &LegacyTargetInstanceDescriptor<'_>,
    env_vars: I,
) -> Vec<TargetPluginInstance>
where
    I: IntoIterator<Item = (String, String)>,
{
    normalize_target_plugin_instances_from_env(config, descriptor, env_vars)
}

#[cfg(test)]
mod tests {
    use super::{
        TargetInstanceSourceClass, TargetPluginInstanceCompatDescriptor, normalize_legacy_target_instances_from_env,
        try_normalize_target_plugin_instances_from_env,
    };
    use crate::TargetError;
    use crate::domain::TargetDomain;
    use crate::manifest::builtin_target_manifest;
    use rustfs_config::audit::{AUDIT_ROUTE_PREFIX, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS};
    use rustfs_config::notify::{NOTIFY_ROUTE_PREFIX, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS};
    use rustfs_config::server_config::{Config, KVS};
    use rustfs_config::{ENABLE_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_LIMIT};
    use std::collections::HashMap;

    fn notify_webhook_descriptor() -> TargetPluginInstanceCompatDescriptor<'static> {
        TargetPluginInstanceCompatDescriptor {
            domain: TargetDomain::Notify,
            plugin_id: builtin_target_manifest("webhook").plugin_id,
            target_type: "webhook",
            subsystem: NOTIFY_WEBHOOK_SUB_SYS,
            route_prefix: NOTIFY_ROUTE_PREFIX,
            valid_fields: NOTIFY_WEBHOOK_KEYS,
        }
    }

    fn audit_webhook_descriptor() -> TargetPluginInstanceCompatDescriptor<'static> {
        TargetPluginInstanceCompatDescriptor {
            domain: TargetDomain::Audit,
            plugin_id: builtin_target_manifest("webhook").plugin_id,
            target_type: "webhook",
            subsystem: AUDIT_WEBHOOK_SUB_SYS,
            route_prefix: AUDIT_ROUTE_PREFIX,
            valid_fields: AUDIT_WEBHOOK_KEYS,
        }
    }

    #[test]
    fn normalize_notify_instances_merges_file_and_env_sources() {
        let mut cfg = Config(HashMap::new());
        let mut subsystem = HashMap::new();

        let mut default_kvs = KVS::new();
        default_kvs.insert(ENABLE_KEY.to_string(), "on".to_string());
        default_kvs.insert(WEBHOOK_QUEUE_LIMIT.to_string(), "10".to_string());
        subsystem.insert("_".to_string(), default_kvs);

        let mut primary = KVS::new();
        primary.insert(WEBHOOK_ENDPOINT.to_string(), "https://example.com/primary".to_string());
        subsystem.insert("primary".to_string(), primary);

        cfg.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), subsystem);

        let instances = normalize_legacy_target_instances_from_env(
            &cfg,
            &notify_webhook_descriptor(),
            vec![
                ("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT".to_string(), "42".to_string()),
                ("RUSTFS_NOTIFY_WEBHOOK_ENABLE_SECONDARY".to_string(), "on".to_string()),
                (
                    "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_SECONDARY".to_string(),
                    "https://example.com/secondary".to_string(),
                ),
            ],
        );

        assert_eq!(instances.len(), 2);

        let primary = instances
            .iter()
            .find(|instance| instance.instance_id == "primary")
            .expect("primary notify instance should be normalized");
        assert_eq!(primary.domain, TargetDomain::Notify);
        assert_eq!(primary.plugin_id, "builtin:webhook");
        assert!(primary.enabled);
        assert_eq!(primary.effective_config.lookup(WEBHOOK_QUEUE_LIMIT).as_deref(), Some("42"));
        assert_eq!(
            primary.effective_config.lookup(WEBHOOK_ENDPOINT).as_deref(),
            Some("https://example.com/primary")
        );
        assert_eq!(primary.source_hints.classification(), TargetInstanceSourceClass::Mixed);
        assert!(primary.source_hints.has_file_default);
        assert!(primary.source_hints.has_file_instance);
        assert!(primary.source_hints.has_env_default);
        assert!(!primary.source_hints.has_env_instance);

        let secondary = instances
            .iter()
            .find(|instance| instance.instance_id == "secondary")
            .expect("secondary env notify instance should be normalized");
        assert!(secondary.enabled);
        assert_eq!(
            secondary.effective_config.lookup(WEBHOOK_ENDPOINT).as_deref(),
            Some("https://example.com/secondary")
        );
        assert_eq!(secondary.effective_config.lookup(WEBHOOK_QUEUE_LIMIT).as_deref(), Some("42"));
        assert_eq!(secondary.source_hints.classification(), TargetInstanceSourceClass::Mixed);
        assert!(secondary.source_hints.has_file_default);
        assert!(!secondary.source_hints.has_file_instance);
        assert!(secondary.source_hints.has_env_default);
        assert!(secondary.source_hints.has_env_instance);
    }

    #[test]
    fn normalize_audit_instances_preserves_domain_and_subsystem() {
        let mut cfg = Config(HashMap::new());
        let mut subsystem = HashMap::new();

        let mut default_kvs = KVS::new();
        default_kvs.insert(ENABLE_KEY.to_string(), "off".to_string());
        subsystem.insert("_".to_string(), default_kvs);

        let mut primary = KVS::new();
        primary.insert(ENABLE_KEY.to_string(), "on".to_string());
        primary.insert(WEBHOOK_ENDPOINT.to_string(), "https://example.com/audit".to_string());
        subsystem.insert("primary".to_string(), primary);

        cfg.0.insert(AUDIT_WEBHOOK_SUB_SYS.to_string(), subsystem);

        let instances = normalize_legacy_target_instances_from_env(&cfg, &audit_webhook_descriptor(), Vec::new());

        assert_eq!(instances.len(), 1);
        let primary = &instances[0];
        assert_eq!(primary.domain, TargetDomain::Audit);
        assert_eq!(primary.target_type, "webhook");
        assert_eq!(primary.subsystem, AUDIT_WEBHOOK_SUB_SYS);
        assert_eq!(primary.instance_id, "primary");
        assert!(primary.enabled);
        assert_eq!(
            primary.effective_config.lookup(WEBHOOK_ENDPOINT).as_deref(),
            Some("https://example.com/audit")
        );
        assert_eq!(primary.source_hints.classification(), TargetInstanceSourceClass::Config);
    }

    #[test]
    fn normalize_instances_keeps_disabled_records() {
        let cfg = Config(HashMap::new());

        let instances = normalize_legacy_target_instances_from_env(
            &cfg,
            &notify_webhook_descriptor(),
            vec![
                ("RUSTFS_NOTIFY_WEBHOOK_ENABLE_DISABLED".to_string(), "off".to_string()),
                (
                    "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_DISABLED".to_string(),
                    "https://example.com/disabled".to_string(),
                ),
            ],
        );

        assert_eq!(instances.len(), 1);
        let disabled = &instances[0];
        assert_eq!(disabled.instance_id, "disabled");
        assert!(!disabled.enabled);
        assert_eq!(disabled.source_hints.classification(), TargetInstanceSourceClass::Env);
        assert!(disabled.source_hints.has_env_instance);
    }

    #[test]
    fn normalize_instances_excludes_default_only_entries() {
        let mut cfg = Config(HashMap::new());
        let mut subsystem = HashMap::new();

        let mut default_kvs = KVS::new();
        default_kvs.insert(ENABLE_KEY.to_string(), "on".to_string());
        default_kvs.insert(WEBHOOK_QUEUE_LIMIT.to_string(), "99".to_string());
        subsystem.insert("_".to_string(), default_kvs);

        cfg.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), subsystem);

        let instances = normalize_legacy_target_instances_from_env(
            &cfg,
            &notify_webhook_descriptor(),
            vec![("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT".to_string(), "100".to_string())],
        );

        assert!(instances.is_empty());
    }

    #[test]
    fn compatibility_wrapper_matches_canonical_instance_model() {
        let mut cfg = Config(HashMap::new());
        let mut subsystem = HashMap::new();

        let mut primary = KVS::new();
        primary.insert(ENABLE_KEY.to_string(), "on".to_string());
        primary.insert(WEBHOOK_ENDPOINT.to_string(), "https://example.com/primary".to_string());
        subsystem.insert("primary".to_string(), primary);
        cfg.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), subsystem);

        let descriptor = notify_webhook_descriptor();
        let env = vec![("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT".to_string(), "7".to_string())];

        let canonical = try_normalize_target_plugin_instances_from_env(&cfg, &descriptor, env.clone())
            .expect("canonical normalization should succeed");
        let compatibility = normalize_legacy_target_instances_from_env(&cfg, &descriptor, env);

        assert_eq!(canonical, compatibility);
    }

    #[test]
    fn normalize_instances_rejects_invalid_enable_value() {
        let error = try_normalize_target_plugin_instances_from_env(
            &Config(HashMap::new()),
            &notify_webhook_descriptor(),
            vec![("RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(), "invalid".to_string())],
        )
        .expect_err("invalid enable value must be propagated by the public normalizer");

        match error {
            TargetError::Configuration(detail) => assert_eq!(detail, "Invalid enable value 'invalid'"),
            other => panic!("expected a configuration error, got {other}"),
        }
    }

    #[test]
    fn legacy_normalizer_keeps_valid_instance_when_one_enable_is_invalid() {
        let instances = normalize_legacy_target_instances_from_env(
            &Config(HashMap::new()),
            &notify_webhook_descriptor(),
            vec![
                ("RUSTFS_NOTIFY_WEBHOOK_ENABLE_GOOD".to_string(), "on".to_string()),
                ("RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_GOOD".to_string(), "https://example.com/good".to_string()),
                ("RUSTFS_NOTIFY_WEBHOOK_ENABLE_BAD".to_string(), "invalid".to_string()),
            ],
        );

        assert_eq!(instances.len(), 2);
        let good = instances
            .iter()
            .find(|instance| instance.instance_id == "good")
            .expect("valid instance should remain present");
        let bad = instances
            .iter()
            .find(|instance| instance.instance_id == "bad")
            .expect("invalid legacy instance should remain visible");
        assert!(good.enabled);
        assert!(!bad.enabled);
    }
}
