// prose-pod-api
//
// Copyright: 2024, Rémi Bardon <remi@remibardon.name>
// License: Mozilla Public License v2.0 (MPL v2.0)

use std::{ops::Deref, str::FromStr as _};

use hickory_proto::rr::Name as DomainName;
use prosody_config::{linked_hash_set::LinkedHashSet, *};
use util::{ApplyDefaultsAndOverrides, WithDefaultsAndOverrides};
use utils::def;

use crate::{app_config::PublicContactsConfig, AppConfig, ProseDefault, ServerConfig};

lazy_static::lazy_static! {
    static ref GLOBAL_HOST: DomainName = DomainName::from_str("global").unwrap();
}

#[derive(Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct ProsodyConfig(pub(super) prosody_config::ProsodyConfig);

impl Deref for ProsodyConfig {
    type Target = prosody_config::ProsodyConfig;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ProsodyConfig {
    pub fn all_sections(&self) -> Vec<&ProsodySettings> {
        let mut all_sections = vec![&self.global_settings];
        all_sections.extend(
            self.additional_sections
                .iter()
                .map(|section| section.settings()),
        );
        all_sections
    }
    pub fn all_enabled_modules(&self) -> LinkedHashSet<String> {
        self.all_sections()
            .iter()
            .map(|section| section.modules_enabled.clone().unwrap_or_default())
            .reduce(|acc, e| acc.union(&e).cloned().collect())
            .unwrap_or_default()
    }

    pub fn virtual_host_settings(&self, host: &str) -> Option<&ProsodySettings> {
        self.additional_sections
            .iter()
            .find_map(|section| match section {
                ProsodyConfigSection::VirtualHost {
                    hostname, settings, ..
                } if hostname.as_str() == host => Some(settings),
                _ => None,
            })
    }
    pub fn component_settings(&self, name: &str) -> Option<&ProsodySettings> {
        self.additional_sections
            .iter()
            .find_map(|section| match section {
                ProsodyConfigSection::Component {
                    plugin, settings, ..
                } if plugin.as_str() == name => Some(settings),
                _ => None,
            })
    }
}

impl ProsodyConfig {
    pub fn to_string(&self, raw_overrides: Option<String>) -> String {
        let header: Group<LuaComment> = vec![
            "Prose Pod Server".into(),
            "XMPP Server Configuration".into(),
            r#"/!\ This file has been automatically generated by Prose Pod API."#.into(),
            r#"/!\ Do NOT edit this file manually or your changes will be overridden during the next reload."#.into(),
        ].into();

        let mut config = self.0.clone();

        if let Some(mut raw_overrides) = raw_overrides {
            // Remove additional sections to print only the global settings first.
            let additional_sections = config.additional_sections;
            config.additional_sections = vec![];

            let mut res = config.print(header).to_string();

            // Indent overrides since they will be printed in non-global sections.
            raw_overrides = raw_overrides
                .lines()
                .map(|line| format!("  {line}"))
                .collect::<Vec<String>>()
                .join("\n");

            // Print all sections, appending the overrides at the end.
            for section in additional_sections {
                res.push_str(
                    prosody_config::ProsodyConfig {
                        additional_sections: vec![section],
                        ..Default::default()
                    }
                    .print(vec![].into())
                    .to_string()
                    .as_str(),
                );
                res.push_str("\n  -- Raw overrides\n");
                res.push_str(raw_overrides.as_str());
                res.push_str("\n\n");
            }

            res
        } else {
            config.print(header).to_string()
        }
    }
}

// MARK: Default configuration

impl ProseDefault for prosody_config::ProsodyConfig {
    fn prose_default(server_config: &ServerConfig, app_config: &AppConfig) -> Self {
        use prosody_config::LuaValue;

        let oauth2_access_token_ttl = (app_config.auth.token_ttl.num_seconds())
            .expect("`app_config.auth.token_ttl` contains years or months. Not supported.")
            .clamp(u32::MIN as f32, u32::MAX as f32) as u32;

        let global_settings = ProsodySettings {
            log: Some(LogConfig::Map(
                [(app_config.server.log_level, LogLevelValue::Console)]
                    .into_iter()
                    .collect(),
            )),
            http_ports: Some([app_config.server.http_port].into_iter().collect()),
            modules_enabled: Some(
                [
                    "auto_activate_hosts",
                    "blocklist",
                    "bookmarks",
                    "carbons",
                    "csi",
                    "csi_battery_saver",
                    "dialback",
                    "disco",
                    "groups_internal",
                    "groups_shell",
                    "lastactivity",
                    "limits",
                    "mam",
                    "pep",
                    "ping",
                    "private",
                    "reload_modules",
                    "roster",
                    "saslauth",
                    "server_contact_info",
                    "smacks",
                    "time",
                    "tls",
                    "uptime",
                    "vcard4",
                    "vcard_legacy",
                    "version",
                    "websocket",
                ]
                .into_iter()
                .map(ToString::to_string)
                .collect(),
            ),
            modules_disabled: Some(["s2s"].into_iter().map(ToString::to_string).collect()),
            c2s_require_encryption: Some(true),
            c2s_stanza_size_limit: Some(Bytes::KibiBytes(256)),
            limits: Some(
                [(
                    ConnectionType::ClientToServer,
                    ConnectionLimits {
                        rate: Some(DataRate::KiloBytesPerSec(50)),
                        burst: Some(Duration(TimeLike::Seconds(2))),
                    },
                )]
                .into_iter()
                .collect(),
            ),
            reload_modules: Some(["tls"].into_iter().map(ToString::to_string).collect()),
            ..Default::default()
        }
        .with_defaults_and_overrides_from(app_config, &GLOBAL_HOST)
        .shallow_merged_with(base_global_settings(), MergeStrategy::KeepSelf);

        let main_virtual_host = ProsodyConfigSection::VirtualHost {
            hostname: server_config.domain.to_string(),
            settings: ProsodySettings {
                modules_enabled: Some(
                    [
                        "admin_rest",
                        "cloud_notify",
                        "http_admin_api", // Has to be non-global.
                        "http_oauth2",
                        "invites",
                        "invites_groups",
                        "invites_register",
                        "invites_register_api",
                        "prose_version",
                        "register",
                        "rest",
                    ]
                    .into_iter()
                    .map(ToString::to_string)
                    .collect(),
                ),
                http_host: Some(app_config.server.local_hostname.to_owned()),
                custom_settings: vec![
                    // See <https://modules.prosody.im/mod_http_oauth2>
                    Group::new(
                        "mod_http_oauth2",
                        vec![
                            def(
                                "allowed_oauth2_grant_types",
                                vec![
                                    "authorization_code",
                                    "refresh_token",
                                    "password",
                                ],
                            ),
                            def("oauth2_access_token_ttl", oauth2_access_token_ttl),
                            // We don't want tokens to be refreshed
                            def("oauth2_refresh_token_ttl", 0),
                            def(
                                "oauth2_registration_key",
                                LuaValue::Env("OAUTH2_REGISTRATION_KEY".to_owned()),
                            ),
                        ],
                    ),
                ],
                reload_modules: Some(
                    ["http_oauth2"]
                        .into_iter()
                        .map(ToString::to_string)
                        .collect(),
                ),
                contact_info: ContactInfo::from(app_config.public_contacts.deref()).non_empty(),
                ..Default::default()
            },
        };
        let groups_virtual_host = ProsodyConfigSection::Component {
            hostname: server_config.groups_domain().to_string(),
            plugin: "muc".into(),
            name: "Chatrooms".into(),
            settings: ProsodySettings {
                restrict_room_creation: Some(RoomCreationRestriction::DomainOnly),
                max_archive_query_results: Some(100),
                muc_log_all_rooms: Some(true),
                muc_log_expires_after: Some(PossiblyInfinite::Infinite),
                muc_log_by_default: Some(true),
                modules_enabled: Some(
                    [
                        "muc_cloud_notify",
                        "muc_mam",
                        "muc_public_affiliations",
                    ]
                    .into_iter()
                    .map(ToString::to_string)
                    .collect(),
                ),
                custom_settings: vec![
                    // See <https://modules.prosody.im/mod_muc_cloud_notify>
                    Group::new(
                        "mod_muc_cloud_notify",
                        // NOTE: `push_notification_with_body` and
                        //   `push_notification_with_sender` already
                        //   defined in the global settings.
                        vec![],
                    ),
                ],
                ..Default::default()
            },
        };

        let mut additional_sections = vec![
            main_virtual_host,
            groups_virtual_host,
        ];

        for section in additional_sections.iter_mut() {
            let ref host =
                (section.hostname().parse()).expect("Invalid hostname in Prosody section");
            section.apply_defaults_and_overrides_from(app_config, host);
        }

        Self {
            global_settings,
            additional_sections,
        }
    }
}

pub fn base_global_settings() -> prosody_config::ProsodySettings {
    prosody_config::ProsodySettings {
        pidfile: Some("/var/run/prosody/prosody.pid".into()),
        admin_socket: Some("/var/run/prosody/prosody.sock".into()),
        authentication: Some(AuthenticationProvider::InternalHashed),
        default_storage: Some(StorageBackend::Internal),
        plugin_paths: Some(
            ["/usr/local/lib/prosody/modules"]
                .into_iter()
                .map(ToString::to_string)
                .collect(),
        ),
        // Disable in-band registrations (done through the Prose Pod Dashboard/API)
        allow_registration: Some(false),
        consider_websocket_secure: Some(true),
        upgrade_legacy_vcards: Some(true),
        ..Default::default()
    }
}

// MARK: - Mappings

impl From<&PublicContactsConfig> for ContactInfo {
    fn from(config: &PublicContactsConfig) -> Self {
        use crate::models::Url;
        use linked_hash_set::LinkedHashSet;

        fn map_or_default(list: &LinkedHashSet<Url>, default: &Vec<String>) -> Vec<String> {
            if list.is_empty() {
                default.to_owned()
            } else {
                list.iter().map(ToString::to_string).collect()
            }
        }
        let default = (config.default.iter())
            .map(ToString::to_string)
            .collect::<Vec<String>>();

        Self {
            abuse: map_or_default(&config.abuse, &default),
            admin: map_or_default(&config.admin, &default),
            feedback: map_or_default(&config.feedback, &default),
            sales: map_or_default(&config.sales, &default),
            security: map_or_default(&config.security, &default),
            support: map_or_default(&config.support, &default),
        }
    }
}

// MARK: - Helpers

// TODO: Remove unused code
pub mod util {
    use hickory_proto::rr::Name as DomainName;
    use prosody_config::*;

    use crate::AppConfig;

    pub(in crate::features::prosody) trait WithDefaultsAndOverrides {
        fn with_defaults_and_overrides_from(
            self,
            app_config: &AppConfig,
            host: &DomainName,
        ) -> Self;
    }

    impl WithDefaultsAndOverrides for ProsodySettings {
        fn with_defaults_and_overrides_from(
            self,
            app_config: &AppConfig,
            host: &DomainName,
        ) -> Self {
            if let Some(host_config) = app_config.prosody.get(host) {
                let mut res = self;
                if let Some(ref defaults) = host_config.defaults {
                    res = defaults.shallow_merged_with(res, MergeStrategy::KeepOther);
                };
                if let Some(ref overrides) = host_config.overrides {
                    res = overrides.shallow_merged_with(res, MergeStrategy::KeepSelf);
                };
                res
            } else {
                self
            }
        }
    }

    pub(in crate::features::prosody) trait ApplyDefaultsAndOverrides {
        fn apply_defaults_and_overrides_from(&mut self, app_config: &AppConfig, host: &DomainName);
    }

    impl ApplyDefaultsAndOverrides for ProsodyConfigSection {
        fn apply_defaults_and_overrides_from(&mut self, app_config: &AppConfig, host: &DomainName) {
            if let Some(host_config) = app_config.prosody.get(host) {
                let settings = self.settings_mut();
                if let Some(ref defaults) = host_config.defaults {
                    settings.shallow_merge(defaults.clone(), MergeStrategy::KeepSelf);
                };
                if let Some(ref overrides) = host_config.overrides {
                    settings.shallow_merge(overrides.clone(), MergeStrategy::KeepOther);
                };
            }
        }
    }

    // pub fn add_admin(settings: &mut ProsodySettings, jid: JID) {
    //     settings
    //         .admins
    //         .get_or_insert_with(Default::default)
    //         .insert(jid);
    // }
    // pub fn remove_admin(settings: &mut ProsodySettings, jid: &JID) {
    //     settings
    //         .admins
    //         .get_or_insert_with(Default::default)
    //         .remove(jid);
    // }

    pub fn add_enabled_module(settings: &mut ProsodySettings, module_name: &'static str) -> bool {
        settings
            .modules_enabled
            .get_or_insert_with(Default::default)
            .insert(module_name.into())
    }
    // pub fn remove_enabled_module(settings: &mut ProsodySettings, module_name: &String) -> bool {
    //     settings
    //         .modules_enabled
    //         .get_or_insert_with(Default::default)
    //         .remove(module_name)
    // }

    // pub fn add_disabled_module(settings: &mut ProsodySettings, module_name: String) -> bool {
    //     settings
    //         .modules_disabled
    //         .get_or_insert_with(Default::default)
    //         .insert(module_name)
    // }
    // pub fn remove_disabled_module(settings: &mut ProsodySettings, module_name: &String) -> bool {
    //     settings
    //         .modules_disabled
    //         .get_or_insert_with(Default::default)
    //         .remove(module_name)
    // }

    // pub fn set_rate_limit(settings: &mut ProsodySettings, conn_type: ConnectionType, value: DataRate) {
    //     settings
    //         .limits
    //         .get_or_insert_with(Default::default)
    //         .entry(conn_type.into())
    //         .or_insert_with(Default::default)
    //         .rate = Some(value)
    // }
    // pub fn set_burst_limit(
    //     settings: &mut ProsodySettings,
    //     conn_type: ConnectionType,
    //     value: Duration<TimeLike>,
    // ) {
    //     settings
    //         .limits
    //         .get_or_insert_with(Default::default)
    //         .entry(conn_type.into())
    //         .or_insert_with(Default::default)
    //         .burst = Some(value)
    // }
}
