// SPDX-License-Identifier: MIT
//! Render Geyser's `config.yml` from [`crate::Options`].
//!
//! Flow: parse a baseline YAML → fold the typed [`Options`] knobs
//! ([`Options::listen`], [`Options::upstream`], [`Options::auth_type`],
//! [`Options::motd`]) onto it → deep-merge [`Options::config_overrides`]
//! last → marshal back to YAML. The typed knobs stay the easy path
//! while every Geyser config option remains reachable through
//! `config_overrides` for the long-tail of settings the typed surface
//! doesn't model.

use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;

use serde_yaml_ng::{Mapping, Value};

use crate::error::Result;
use crate::options::{AuthType, Options};

pub(crate) fn render_config(workdir: &Path, opts: &Options) -> Result<()> {
    let cfg = build_config(opts)?;
    let body =
        serde_yaml_ng::to_string(&cfg).map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;

    let path = workdir.join("config.yml");
    let mut f = OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .mode(0o600)
        .open(&path)?;
    f.write_all(b"# Generated by go.minekube.com/geyserlite/rust.\n")?;
    f.write_all(body.as_bytes())?;
    Ok(())
}

/// Pure-data half of `render_config`. Exposed so tests can assert the
/// merge result without writing files.
pub(crate) fn build_config(opts: &Options) -> Result<Mapping> {
    let mut cfg: Mapping = serde_yaml_ng::from_str(BASELINE_CONFIG_YAML)
        .map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;

    let (bedrock_addr, bedrock_port) = split_host_port(&opts.listen, "0.0.0.0", 19132);
    let (upstream_addr, upstream_port) = split_host_port(&opts.upstream, "127.0.0.1", 25565);
    let motd1 = if opts.motd.line1.is_empty() {
        "geyserlite".to_string()
    } else {
        opts.motd.line1.clone()
    };
    let motd2 = if opts.motd.line2.is_empty() {
        "cross-play".to_string()
    } else {
        opts.motd.line2.clone()
    };

    let mut typed = Mapping::new();
    typed.insert("bedrock".into(), {
        let mut m = Mapping::new();
        m.insert("address".into(), bedrock_addr.into());
        m.insert("port".into(), Value::from(bedrock_port));
        m.insert("motd1".into(), motd1.clone().into());
        m.insert("motd2".into(), motd2.into());
        m.insert("server-name".into(), motd1.into());
        m.into()
    });
    typed.insert("java".into(), {
        let mut m = Mapping::new();
        m.insert("address".into(), upstream_addr.into());
        m.insert("port".into(), Value::from(upstream_port));
        m.insert("auth-type".into(), opts.auth_type.as_str().into());
        m.insert(
            "use-proxy-protocol".into(),
            (opts.auth_type == AuthType::Floodgate).into(),
        );
        m.insert("forward-hostname".into(), true.into());
        m.into()
    });

    merge_into(&mut cfg, &typed);
    if opts.auth_type == AuthType::Floodgate {
        cfg.insert("floodgate-key-file".into(), "key.bin".into());
    }
    if !opts.config_overrides.is_empty() {
        merge_into(&mut cfg, &opts.config_overrides);
    }
    Ok(cfg)
}

/// Recursively merge `src` into `dst`. When both sides have a nested
/// mapping at the same key, recurse; otherwise `src` wins. Mirrors
/// the Go `mergeMap` and Gate's `mergeConfigMaps`.
fn merge_into(dst: &mut Mapping, src: &Mapping) {
    for (k, v) in src {
        match (dst.get_mut(k), v) {
            (Some(Value::Mapping(dst_m)), Value::Mapping(src_m)) => merge_into(dst_m, src_m),
            _ => {
                dst.insert(k.clone(), v.clone());
            }
        }
    }
}

const BASELINE_CONFIG_YAML: &str = r#"
bedrock:
  address: 0.0.0.0
  port: 19132
  motd1: geyserlite
  motd2: cross-play
  server-name: geyserlite
  compression-level: 6

java:
  address: 127.0.0.1
  port: 25565
  auth-type: floodgate
  use-proxy-protocol: false
  forward-hostname: true

passthrough-motd: false
passthrough-player-counts: false
legacy-ping-passthrough: false

forward-player-ping: true
max-players: 100
debug-mode: false

show-cooldown: title
show-coordinates: true
disable-bedrock-scaffolding: false

xbox-achievements-enabled: false
log-player-ip-addresses: true
notify-on-new-bedrock-update: false

scoreboard-packet-threshold: 20
enable-proxy-connections: false
mtu: 1400
use-direct-connection: false
disable-compression: false

config-version: 4
"#;

pub(crate) fn write_floodgate_key(workdir: &Path, opts: &Options) -> Result<()> {
    if opts.auth_type != AuthType::Floodgate {
        return Ok(());
    }
    if opts.floodgate_key.len() != 16 {
        return Err(crate::Error::InvalidFloodgateKey);
    }
    let path = workdir.join("key.bin");
    let mut f = OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .mode(0o600)
        .open(&path)?;
    f.write_all(&opts.floodgate_key)?;
    Ok(())
}

pub(crate) fn write_permissions_yml(workdir: &Path) -> Result<()> {
    let path = workdir.join("permissions.yml");
    std::fs::write(path, "default-permissions: []\n")?;
    Ok(())
}

/// Split `"host:port"` with defaults for missing pieces. Handles IPv6
/// in brackets. Port comes out as `i64` because that's the YAML int
/// type and lets `config_overrides` merge against the same shape
/// without type confusion.
pub(crate) fn split_host_port(s: &str, default_host: &str, default_port: i64) -> (String, i64) {
    if s.is_empty() {
        return (default_host.into(), default_port);
    }
    let (host, port_str) = split_host_port_str(s);
    let host = if host.is_empty() {
        default_host.to_string()
    } else {
        host
    };
    let port = port_str.parse::<i64>().unwrap_or(default_port);
    (host, port)
}

fn split_host_port_str(s: &str) -> (String, String) {
    if let Some(rest) = s.strip_prefix('[') {
        if let Some(end) = rest.find(']') {
            let host = &rest[..end];
            let after = &rest[end + 1..];
            let port = after.strip_prefix(':').unwrap_or("");
            return (host.into(), port.into());
        }
    }
    if let Some(idx) = s.rfind(':') {
        return (s[..idx].into(), s[idx + 1..].into());
    }
    (s.into(), String::new())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::options::{AuthType, Motd};

    fn opts(listen: &str, upstream: &str) -> Options {
        Options {
            listen: listen.into(),
            upstream: upstream.into(),
            auth_type: AuthType::Floodgate,
            floodgate_key: vec![0u8; 16],
            motd: Motd {
                line1: "alpha".into(),
                line2: "beta".into(),
            },
            ..Default::default()
        }
    }

    #[test]
    fn typed_options_apply() {
        let cfg = build_config(&opts("10.0.0.1:25599", "192.168.1.5:25567")).unwrap();
        let bedrock = cfg.get("bedrock").unwrap().as_mapping().unwrap();
        assert_eq!(bedrock.get("address").unwrap().as_str(), Some("10.0.0.1"));
        assert_eq!(bedrock.get("port").unwrap().as_i64(), Some(25599));
        assert_eq!(bedrock.get("motd1").unwrap().as_str(), Some("alpha"));
        let java = cfg.get("java").unwrap().as_mapping().unwrap();
        assert_eq!(java.get("address").unwrap().as_str(), Some("192.168.1.5"));
        assert_eq!(java.get("auth-type").unwrap().as_str(), Some("floodgate"));
        assert_eq!(
            cfg.get("floodgate-key-file").unwrap().as_str(),
            Some("key.bin"),
        );
    }

    #[test]
    fn overrides_deep_merge() {
        let mut o = opts(":19132", "127.0.0.1:25565");
        let mut bedrock_overrides = Mapping::new();
        bedrock_overrides.insert("compression-level".into(), 9.into());
        o.config_overrides
            .insert("bedrock".into(), bedrock_overrides.into());
        o.config_overrides
            .insert("passthrough-motd".into(), true.into());
        o.config_overrides.insert("max-players".into(), 50.into());

        let cfg = build_config(&o).unwrap();
        let bedrock = cfg.get("bedrock").unwrap().as_mapping().unwrap();
        assert_eq!(bedrock.get("compression-level").unwrap().as_i64(), Some(9));
        // Sibling under `bedrock` survives partial override.
        assert!(bedrock.get("server-name").unwrap().as_str().is_some());
        assert_eq!(cfg.get("passthrough-motd").unwrap().as_bool(), Some(true));
        assert_eq!(cfg.get("max-players").unwrap().as_i64(), Some(50));
    }

    #[test]
    fn override_beats_typed_option() {
        let mut o = opts("10.0.0.1:19132", "127.0.0.1:25565");
        let mut bedrock_overrides = Mapping::new();
        bedrock_overrides.insert("port".into(), 19999.into());
        o.config_overrides
            .insert("bedrock".into(), bedrock_overrides.into());

        let cfg = build_config(&o).unwrap();
        let bedrock = cfg.get("bedrock").unwrap().as_mapping().unwrap();
        assert_eq!(bedrock.get("port").unwrap().as_i64(), Some(19999));
    }

    #[test]
    fn split_simple() {
        assert_eq!(
            split_host_port("127.0.0.1:25567", "0.0.0.0", 19132),
            ("127.0.0.1".into(), 25567)
        );
    }
    #[test]
    fn split_default_host() {
        assert_eq!(
            split_host_port(":19132", "0.0.0.0", 19132),
            ("0.0.0.0".into(), 19132)
        );
    }
    #[test]
    fn split_ipv6() {
        assert_eq!(
            split_host_port("[::1]:25567", "0.0.0.0", 19132),
            ("::1".into(), 25567)
        );
    }
    #[test]
    fn split_fly() {
        assert_eq!(
            split_host_port("fly-global-services:19132", "0.0.0.0", 19132),
            ("fly-global-services".into(), 19132)
        );
    }
}
