use k8s_openapi::api::core::v1::ConfigMap;
use kube::api::ObjectMeta;
use kube::Resource;

use crate::types::ErgoNetwork;

pub fn build_configmap(network: &ErgoNetwork, oper_hash: Option<&str>, oauth2_secret: Option<&str>) -> ConfigMap {
    let name = network.metadata.name.clone().unwrap();
    let ns = network.metadata.namespace.clone().unwrap();
    let oref = network.controller_owner_ref(&()).unwrap();

    let config = generate_ergo_config(network, oper_hash, oauth2_secret);

    ConfigMap {
        metadata: ObjectMeta {
            name: Some(format!("{name}-config")),
            namespace: Some(ns),
            owner_references: Some(vec![oref]),
            ..Default::default()
        },
        data: Some([("ircd.yaml".to_string(), config)].into()),
        ..Default::default()
    }
}

fn sanitize_yaml_value(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "")
        .replace('\r', "")
}

fn generate_ergo_config(network: &ErgoNetwork, oper_hash: Option<&str>, oauth2_secret: Option<&str>) -> String {
    let spec = &network.spec;
    let network_name = spec
        .network_name
        .as_deref()
        .unwrap_or(&spec.hostname);

    let mut lines = Vec::new();

    let hostname = sanitize_yaml_value(&spec.hostname);
    lines.push(format!("server:\n    name: {hostname}"));

    lines.push("    listeners:".to_string());
    for listener in &spec.listeners {
        if listener.tls {
            lines.push(format!(
                "        \"{}\":\n            tls:\n                cert: /config/tls/tls.crt\n                key: /config/tls/tls.key",
                listener.address
            ));
        } else {
            lines.push(format!("        \"{}\":", listener.address));
        }
    }

    let max_sendq = sanitize_yaml_value(spec.max_sendq.as_deref().unwrap_or("96k"));
    lines.push(format!("    max-sendq: {max_sendq}"));

    if let Some(cloaking) = &spec.cloaking {
        let cidr4 = cloaking.cidr_len_ipv4.unwrap_or(32);
        let cidr6 = cloaking.cidr_len_ipv6.unwrap_or(64);
        let bits = cloaking.num_bits.unwrap_or(64);
        let netname = sanitize_yaml_value(&cloaking.netname);
        lines.push(format!(
            "    ip-cloaking:\n        enabled: true\n        enabled-for-always-on: true\n        netname: \"{netname}\"\n        cidr-len-ipv4: {cidr4}\n        cidr-len-ipv6: {cidr6}\n        num-bits: {bits}"
        ));
    }

    let network_name = sanitize_yaml_value(network_name);
    lines.push(format!("\nnetwork:\n    name: \"{network_name}\""));

    lines.push("\ndatastore:\n    path: /ircd/ircd.db\n    autoupgrade: true".to_string());

    {
        let l = spec.limits.as_ref();
        let nicklen = l.and_then(|l| l.nicklen).unwrap_or(32);
        let identlen = l.and_then(|l| l.identlen).unwrap_or(20);
        let realnamelen = l.and_then(|l| l.realnamelen).unwrap_or(150);
        let channellen = l.and_then(|l| l.channellen).unwrap_or(64);
        let awaylen = l.and_then(|l| l.awaylen).unwrap_or(390);
        let kicklen = l.and_then(|l| l.kicklen).unwrap_or(390);
        let topiclen = l.and_then(|l| l.topiclen).unwrap_or(390);
        let monitor = l.and_then(|l| l.monitor_entries).unwrap_or(100);
        let whowas = l.and_then(|l| l.whowas_entries).unwrap_or(100);
        let chanlist = l.and_then(|l| l.chan_list_modes).unwrap_or(100);
        let regmsgs = l.and_then(|l| l.registration_messages).unwrap_or(1024);
        let ml = l.and_then(|l| l.multiline.as_ref());
        let ml_bytes = ml.and_then(|m| m.max_bytes).unwrap_or(4096);
        let ml_lines = ml.and_then(|m| m.max_lines).unwrap_or(100);
        lines.push(format!(
            "\nlimits:\n    nicklen: {nicklen}\n    identlen: {identlen}\n    realnamelen: {realnamelen}\n    channellen: {channellen}\n    awaylen: {awaylen}\n    kicklen: {kicklen}\n    topiclen: {topiclen}\n    monitor-entries: {monitor}\n    whowas-entries: {whowas}\n    chan-list-modes: {chanlist}\n    registration-messages: {regmsgs}\n    multiline:\n        max-bytes: {ml_bytes}\n        max-lines: {ml_lines}"
        ));
    }

    if let Some(hash) = oper_hash {
        let class_name = spec
            .oper_credentials
            .as_ref()
            .and_then(|o| o.class_name.as_deref())
            .unwrap_or("server-admin");
        lines.push(concat!(
            "\noper-classes:",
            "\n    chat-moderator:",
            "\n        title: Chat Moderator",
            "\n        capabilities:",
            "\n            - kill",
            "\n            - ban",
            "\n            - nofakelag",
            "\n            - relaymsg",
            "\n            - vhosts",
            "\n            - sajoin",
            "\n            - samode",
            "\n            - snomasks",
            "\n    server-admin:",
            "\n        title: Server Admin",
            "\n        extends: chat-moderator",
            "\n        capabilities:",
            "\n            - rehash",
            "\n            - accreg",
            "\n            - chanreg",
            "\n            - history",
            "\n            - defcon",
            "\n            - massmessage",
            "\n            - metadata",
        ).to_string());
        let hash = sanitize_yaml_value(hash);
        let class_name = sanitize_yaml_value(class_name);
        lines.push(format!(
            "\nopers:\n    admin:\n        class: {class_name}\n        password: \"{hash}\""
        ));
    }

    {
        let v = spec.vhosts.as_ref();
        let vhost_enabled = v.and_then(|v| v.enabled).unwrap_or(true);
        let vhost_maxlen = v.and_then(|v| v.max_length).unwrap_or(64);
        let vhost_regexp = sanitize_yaml_value(v
            .and_then(|v| v.valid_regexp.as_deref())
            .unwrap_or("^[a-zA-Z0-9.\\-]+$"));
        lines.push(format!(
            "\naccounts:\n    authentication-enabled: true\n    registration:\n        enabled: true\n        enabled-callbacks:\n            - none\n        enabled-credential-types:\n            - passphrase\n    nick-reservation:\n        enabled: true\n        method: strict\n    multiclient:\n        enabled: true\n    vhosts:\n        enabled: {vhost_enabled}\n        max-length: {vhost_maxlen}\n        valid-regexp: '{vhost_regexp}'"
        ));
    }

    if let Some(oauth2) = &spec.oauth2 {
        if let Some(secret) = oauth2_secret {
            let client_id = oauth2.client_id.as_deref().unwrap_or("ergo");
            let introspection_url = sanitize_yaml_value(&oauth2.introspection_url);
            let client_id = sanitize_yaml_value(client_id);
            let secret = sanitize_yaml_value(secret);
            lines.push(format!(
                "    oauth2:\n        enabled: true\n        autocreate: {}\n        introspection-url: \"{introspection_url}\"\n        introspection-timeout: 10s\n        client-id: {client_id}\n        client-secret: \"{secret}\"",
                oauth2.autocreate
            ));
        }
    }

    lines.push(String::new());
    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::test_network;

    #[test]
    fn config_contains_hostname() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("test-network.irc.now"));
    }

    #[test]
    fn config_contains_network_name() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("Test Network"));
    }

    #[test]
    fn config_contains_listeners() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains(":6697"));
        assert!(config.contains(":6667"));
    }

    #[test]
    fn config_contains_tls_paths() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("/config/tls/tls.crt"));
        assert!(config.contains("/config/tls/tls.key"));
    }

    #[test]
    fn config_contains_oper_hash() {
        let hash = "$2a$10$examplehash";
        let cm = build_configmap(&test_network(), Some(hash), None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains(hash));
        assert!(config.contains("server-admin"));
    }

    #[test]
    fn config_without_oper_omits_opers_section() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(!config.contains("opers"));
    }

    #[test]
    fn network_name_defaults_to_hostname() {
        let mut network = test_network();
        network.spec.network_name = None;
        let cm = build_configmap(&network, None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("test-network.irc.now"));
    }

    #[test]
    fn configmap_named_correctly() {
        let cm = build_configmap(&test_network(), None, None);
        assert_eq!(cm.metadata.name.unwrap(), "test-network-config");
    }

    #[test]
    fn configmap_has_owner_reference() {
        let cm = build_configmap(&test_network(), None, None);
        let orefs = cm.metadata.owner_references.unwrap();
        assert_eq!(orefs.len(), 1);
        assert_eq!(orefs[0].name, "test-network");
    }

    #[test]
    fn config_contains_oauth2_when_configured() {
        let cm = build_configmap(&test_network(), None, Some("test-secret"));
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("oauth2"));
        assert!(config.contains("introspection"));
        assert!(config.contains("autocreate: true"));
    }

    #[test]
    fn config_omits_oauth2_when_not_configured() {
        let mut network = test_network();
        network.spec.oauth2 = None;
        let cm = build_configmap(&network, None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(!config.contains("oauth2"));
    }

    #[test]
    fn config_omits_oauth2_without_secret() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(!config.contains("oauth2"));
    }

    #[test]
    fn config_contains_cloaking_when_configured() {
        let cm = build_configmap(&test_network(), None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(config.contains("ip-cloaking"));
        assert!(config.contains("irc.now"));
    }

    #[test]
    fn config_omits_cloaking_when_not_configured() {
        let mut network = test_network();
        network.spec.cloaking = None;
        let cm = build_configmap(&network, None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(!config.contains("ip-cloaking"));
    }

    #[test]
    fn config_uses_default_max_sendq() {
        let cm = build_configmap(&test_network(), None, None);
        let config = &cm.data.unwrap()["ircd.yaml"];
        assert!(config.contains("max-sendq: 96k"));
    }

    #[test]
    fn config_uses_custom_max_sendq() {
        let mut network = test_network();
        network.spec.max_sendq = Some("128k".to_string());
        let config = &build_configmap(&network, None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("max-sendq: 128k"));
    }

    #[test]
    fn config_uses_default_cloaking_params() {
        let cm = build_configmap(&test_network(), None, None);
        let config = &cm.data.unwrap()["ircd.yaml"];
        assert!(config.contains("cidr-len-ipv4: 32"));
        assert!(config.contains("cidr-len-ipv6: 64"));
        assert!(config.contains("num-bits: 64"));
    }

    #[test]
    fn config_uses_custom_cloaking_params() {
        let mut network = test_network();
        let cloaking = network.spec.cloaking.as_mut().unwrap();
        cloaking.cidr_len_ipv4 = Some(24);
        cloaking.cidr_len_ipv6 = Some(48);
        cloaking.num_bits = Some(80);
        let config = &build_configmap(&network, None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("cidr-len-ipv4: 24"));
        assert!(config.contains("cidr-len-ipv6: 48"));
        assert!(config.contains("num-bits: 80"));
    }

    #[test]
    fn config_uses_default_limits() {
        let config = &build_configmap(&test_network(), None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("nicklen: 32"));
        assert!(config.contains("identlen: 20"));
        assert!(config.contains("realnamelen: 150"));
        assert!(config.contains("channellen: 64"));
        assert!(config.contains("awaylen: 390"));
        assert!(config.contains("kicklen: 390"));
        assert!(config.contains("topiclen: 390"));
        assert!(config.contains("monitor-entries: 100"));
        assert!(config.contains("whowas-entries: 100"));
        assert!(config.contains("chan-list-modes: 100"));
        assert!(config.contains("registration-messages: 1024"));
        assert!(config.contains("max-bytes: 4096"));
        assert!(config.contains("max-lines: 100"));
    }

    #[test]
    fn config_uses_custom_limits() {
        use crate::types::{LimitsSpec, MultilineSpec};
        let mut network = test_network();
        network.spec.limits = Some(LimitsSpec {
            nicklen: Some(16),
            topiclen: Some(500),
            multiline: Some(MultilineSpec {
                max_bytes: Some(8192),
                max_lines: Some(200),
            }),
            ..Default::default()
        });
        let config = &build_configmap(&network, None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("nicklen: 16"));
        assert!(config.contains("topiclen: 500"));
        assert!(config.contains("max-bytes: 8192"));
        assert!(config.contains("max-lines: 200"));
        assert!(config.contains("identlen: 20"));
    }

    #[test]
    fn config_uses_custom_oper_class() {
        let mut network = test_network();
        network.spec.oper_credentials = Some(crate::types::OperCredentials {
            secret_name: "test-oper".to_string(),
            class_name: Some("network-admin".to_string()),
        });
        let config = &build_configmap(&network, Some("$hash"), None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("class: network-admin"));
    }

    #[test]
    fn config_uses_default_oper_class() {
        let config = &build_configmap(&test_network(), Some("$hash"), None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("class: server-admin"));
    }

    #[test]
    fn config_uses_default_vhosts() {
        let config = &build_configmap(&test_network(), None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("enabled: true"));
        assert!(config.contains("max-length: 64"));
        assert!(config.contains("valid-regexp:"));
    }

    #[test]
    fn config_uses_custom_vhosts() {
        use crate::types::VhostSpec;
        let mut network = test_network();
        network.spec.vhosts = Some(VhostSpec {
            enabled: Some(false),
            max_length: Some(128),
            valid_regexp: Some("^[a-z.]+$".to_string()),
        });
        let config = &build_configmap(&network, None, None).data.unwrap()["ircd.yaml"];
        assert!(config.contains("enabled: false"));
        assert!(config.contains("max-length: 128"));
        assert!(config.contains("'^[a-z.]+$'"));
    }

    #[test]
    fn config_uses_custom_oauth2_client_id() {
        let mut network = test_network();
        network.spec.oauth2.as_mut().unwrap().client_id = Some("custom-client".to_string());
        let config = &build_configmap(&network, None, Some("secret")).data.unwrap()["ircd.yaml"];
        assert!(config.contains("client-id: custom-client"));
    }

    #[test]
    fn config_uses_default_oauth2_client_id() {
        let config = &build_configmap(&test_network(), None, Some("secret")).data.unwrap()["ircd.yaml"];
        assert!(config.contains("client-id: ergo"));
    }

    #[test]
    fn sanitize_yaml_value_escapes_backslash() {
        assert_eq!(sanitize_yaml_value("a\\b"), "a\\\\b");
    }

    #[test]
    fn sanitize_yaml_value_escapes_quotes() {
        assert_eq!(sanitize_yaml_value(r#"a"b"#), r#"a\"b"#);
    }

    #[test]
    fn sanitize_yaml_value_strips_newlines() {
        assert_eq!(sanitize_yaml_value("a\nb\r"), "ab");
    }

    #[test]
    fn sanitize_yaml_value_combined() {
        assert_eq!(sanitize_yaml_value("a\\\"\n"), "a\\\\\\\"");
    }

    #[test]
    fn sanitize_yaml_value_empty() {
        assert_eq!(sanitize_yaml_value(""), "");
    }

    #[test]
    fn config_rejects_netname_injection() {
        let mut network = test_network();
        network.spec.cloaking = Some(crate::types::CloakingSpec {
            netname: "evil\"\nnew-key: injected".to_string(),
            cidr_len_ipv4: None,
            cidr_len_ipv6: None,
            num_bits: None,
        });
        let cm = build_configmap(&network, None, None);
        let data = cm.data.unwrap();
        let config = &data["ircd.yaml"];
        assert!(!config.contains("new-key: injected"));
    }
}
