use std::{
    env::current_dir,
    net::{Ipv4Addr, SocketAddr},
    path::{Path, PathBuf},
};

use anyhow::{Context, bail};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::fs;

const DEFAULT_SYSTEM_TFTP_ROOT: &str = "/srv/tftp";
const SYSTEM_CONFIG_DIR: &str = "/etc/ostool-server";
const SYSTEM_DATA_DIR: &str = "/var/lib/ostool-server";

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ServerConfig {
    pub listen_addr: SocketAddr,
    pub data_dir: PathBuf,
    pub board_dir: PathBuf,
    pub dtb_dir: PathBuf,
    pub tftp: TftpConfig,
    pub network: TftpNetworkConfig,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self::default_for_path(Path::new(".ostool-server.toml"))
    }
}

impl ServerConfig {
    pub fn default_for_path(path: &Path) -> Self {
        let use_system_layout = path.starts_with(SYSTEM_CONFIG_DIR);

        let data_dir = if use_system_layout {
            PathBuf::from(SYSTEM_DATA_DIR)
        } else {
            PathBuf::from(".ostool-server")
        };
        let board_dir = data_dir.join("boards");
        let dtb_dir = data_dir.join("dtbs");

        #[cfg(target_os = "linux")]
        let tftp = TftpConfig::SystemTftpdHpa(SystemTftpdHpaConfig::default());

        #[cfg(not(target_os = "linux"))]
        let tftp = TftpConfig::Builtin(BuiltinTftpConfig::default_with_root(
            data_dir.join("tftp-root"),
        ));

        Self {
            listen_addr: SocketAddr::from(([0, 0, 0, 0], 2999)),
            data_dir,
            board_dir,
            dtb_dir,
            tftp,
            network: TftpNetworkConfig::default(),
        }
    }

    pub async fn load_or_create(path: &Path) -> anyhow::Result<Self> {
        match fs::read_to_string(path).await {
            Ok(content) => {
                let mut config: Self = toml::from_str(&content)
                    .with_context(|| format!("failed to parse {}", path.display()))?;
                config.normalize_paths(path)?;
                config.sync_system_tftpd_hpa_config()?;
                config.sync_network_defaults();
                config.validate()?;
                Ok(config)
            }
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                let mut config = Self::default_for_path(path);
                config.normalize_paths(path)?;
                config.sync_system_tftpd_hpa_config()?;
                config.sync_network_defaults();
                config.validate()?;
                config.write_to_path(path).await?;
                Ok(config)
            }
            Err(err) => Err(err.into()),
        }
    }

    pub async fn write_to_path(&self, path: &Path) -> anyhow::Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .await
                .with_context(|| format!("failed to create {}", parent.display()))?;
        }
        fs::write(path, toml::to_string_pretty(self)?)
            .await
            .with_context(|| format!("failed to write {}", path.display()))?;
        Ok(())
    }

    fn sync_system_tftpd_hpa_config(&mut self) -> anyhow::Result<()> {
        let TftpConfig::SystemTftpdHpa(cfg) = &mut self.tftp else {
            return Ok(());
        };

        match parse_tftpd_hpa_file(&cfg.config_path) {
            Ok(Some(existing)) => {
                let existing_dir = if existing.directory.is_absolute() {
                    existing.directory
                } else {
                    PathBuf::from(DEFAULT_SYSTEM_TFTP_ROOT)
                };
                cfg.root_dir = existing_dir;
                if let Some(username) = existing.username {
                    cfg.username = Some(username);
                }
                if let Some(address) = existing.address {
                    cfg.address = address;
                }
                if let Some(options) = existing.options {
                    cfg.options = options;
                }
            }
            Ok(None) => {
                cfg.root_dir = PathBuf::from(DEFAULT_SYSTEM_TFTP_ROOT);
            }
            Err(err) => return Err(err),
        }

        Ok(())
    }

    fn sync_network_defaults(&mut self) {
        if self.network.interface.trim().is_empty()
            && let Some(interface) = crate::serial::network::default_non_loopback_interface_name()
        {
            self.network.interface = interface;
        }
    }

    pub fn normalize_paths(&mut self, config_path: &Path) -> anyhow::Result<()> {
        let config_dir = config_path
            .parent()
            .filter(|dir| !dir.as_os_str().is_empty())
            .map(PathBuf::from)
            .unwrap_or(current_dir()?);

        self.data_dir = absolutize_path(&config_dir, &self.data_dir);
        self.board_dir = absolutize_path(&config_dir, &self.board_dir);
        self.dtb_dir = absolutize_path(&config_dir, &self.dtb_dir);

        match &mut self.tftp {
            TftpConfig::Builtin(cfg) => {
                cfg.root_dir = absolutize_path(&config_dir, &cfg.root_dir);
            }
            TftpConfig::SystemTftpdHpa(cfg) => {
                cfg.root_dir = absolutize_path(&config_dir, &cfg.root_dir);
                cfg.config_path = absolutize_path(&config_dir, &cfg.config_path);
            }
        }

        Ok(())
    }

    pub fn validate(&self) -> anyhow::Result<()> {
        if self.network.interface.trim().is_empty() {
            bail!(
                "network.interface must be configured or auto-detected from a non-loopback interface"
            );
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "provider", rename_all = "snake_case")]
pub enum TftpConfig {
    Builtin(BuiltinTftpConfig),
    SystemTftpdHpa(SystemTftpdHpaConfig),
}

impl TftpConfig {
    pub fn enabled(&self) -> bool {
        match self {
            Self::Builtin(cfg) => cfg.enabled,
            Self::SystemTftpdHpa(cfg) => cfg.enabled,
        }
    }

    pub fn root_dir(&self) -> &Path {
        match self {
            Self::Builtin(cfg) => &cfg.root_dir,
            Self::SystemTftpdHpa(cfg) => &cfg.root_dir,
        }
    }

    pub fn provider_name(&self) -> &'static str {
        match self {
            Self::Builtin(_) => "builtin",
            Self::SystemTftpdHpa(_) => "system_tftpd_hpa",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct TftpNetworkConfig {
    pub interface: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BuiltinTftpConfig {
    pub enabled: bool,
    pub root_dir: PathBuf,
    pub bind_addr: SocketAddr,
}

impl BuiltinTftpConfig {
    pub fn default_with_root(root_dir: PathBuf) -> Self {
        Self {
            enabled: true,
            root_dir,
            bind_addr: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 69)),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SystemTftpdHpaConfig {
    pub enabled: bool,
    pub root_dir: PathBuf,
    pub config_path: PathBuf,
    pub service_name: String,
    pub username: Option<String>,
    pub address: String,
    pub options: String,
    pub manage_config: bool,
    pub reconcile_on_start: bool,
}

impl Default for SystemTftpdHpaConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            root_dir: PathBuf::from(DEFAULT_SYSTEM_TFTP_ROOT),
            config_path: PathBuf::from("/etc/default/tftpd-hpa"),
            service_name: "tftpd-hpa".to_string(),
            username: Some("tftp".to_string()),
            address: ":69".to_string(),
            options: "-l -s -c".to_string(),
            manage_config: false,
            reconcile_on_start: true,
        }
    }
}

#[derive(Debug)]
struct ParsedTftpdHpaConfig {
    username: Option<String>,
    directory: PathBuf,
    address: Option<String>,
    options: Option<String>,
}

fn parse_tftpd_hpa_file(path: &Path) -> anyhow::Result<Option<ParsedTftpdHpaConfig>> {
    if !path.exists() {
        return Ok(None);
    }

    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    let mut username = None;
    let mut directory = None;
    let mut address = None;
    let mut options = None;

    for line in content.lines() {
        let Some((key, value)) = parse_key_value(line) else {
            continue;
        };
        match key {
            "TFTP_USERNAME" => username = Some(value),
            "TFTP_DIRECTORY" => directory = Some(PathBuf::from(value)),
            "TFTP_ADDRESS" => address = Some(value),
            "TFTP_OPTIONS" => options = Some(value),
            _ => {}
        }
    }

    let directory = directory.unwrap_or_else(|| PathBuf::from(DEFAULT_SYSTEM_TFTP_ROOT));
    Ok(Some(ParsedTftpdHpaConfig {
        username,
        directory,
        address,
        options,
    }))
}

fn parse_key_value(line: &str) -> Option<(&str, String)> {
    let trimmed = line.trim();
    if trimmed.is_empty() || trimmed.starts_with('#') {
        return None;
    }
    let (key, value) = trimmed.split_once('=')?;
    Some((key.trim(), unquote(value.trim())))
}

fn unquote(value: &str) -> String {
    let mut chars = value.chars();
    match (chars.next(), value.chars().last()) {
        (Some('"'), Some('"')) | (Some('\''), Some('\'')) if value.len() >= 2 => {
            value[1..value.len() - 1].to_string()
        }
        _ => value.to_string(),
    }
}

fn absolutize_path(base_dir: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        base_dir.join(path)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BoardConfig {
    pub id: String,
    pub board_type: String,
    #[serde(default)]
    pub tags: Vec<String>,
    pub serial: Option<SerialConfig>,
    pub power_management: PowerManagementConfig,
    pub boot: BootConfig,
    pub notes: Option<String>,
    #[serde(default)]
    pub disabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SerialConfig {
    pub port: String,
    pub baud_rate: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PowerManagementConfig {
    Custom(CustomPowerManagement),
    ZhongshengRelay(ZhongshengRelayPowerManagement),
    Virtual(VirtualPowerManagement),
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CustomPowerManagement {
    pub power_on_cmd: String,
    pub power_off_cmd: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ZhongshengRelayPowerManagement {
    pub serial_port: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct VirtualPowerManagement {}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BootConfig {
    Uboot(UbootProfile),
    Pxe(PxeProfile),
}

impl BootConfig {
    pub fn kind_name(&self) -> &'static str {
        match self {
            Self::Uboot(_) => "uboot",
            Self::Pxe(_) => "pxe",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(deny_unknown_fields)]
pub struct UbootProfile {
    #[serde(default)]
    pub use_tftp: bool,
    pub dtb_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct PxeProfile {
    pub notes: Option<String>,
}

#[cfg(test)]
mod tests {
    use std::{
        net::SocketAddr,
        path::{Path, PathBuf},
    };

    use serde_json::json;
    use tempfile::tempdir;

    use super::{
        BoardConfig, BootConfig, CustomPowerManagement, PowerManagementConfig, ServerConfig,
        UbootProfile, VirtualPowerManagement,
    };

    #[test]
    fn server_config_round_trip_includes_network() {
        let config = ServerConfig::default();
        let encoded = toml::to_string_pretty(&config).unwrap();
        let decoded: ServerConfig = toml::from_str(&encoded).unwrap();
        assert_eq!(decoded.listen_addr, SocketAddr::from(([0, 0, 0, 0], 2999)));
        assert_eq!(decoded.network.interface, "");
        assert!(decoded.dtb_dir.ends_with("dtbs"));
    }

    #[test]
    fn system_config_defaults_use_fhs_layout() {
        let config = ServerConfig::default_for_path(Path::new("/etc/ostool-server/config.toml"));
        assert_eq!(config.data_dir, PathBuf::from("/var/lib/ostool-server"));
        assert_eq!(
            config.board_dir,
            PathBuf::from("/var/lib/ostool-server/boards")
        );
        assert_eq!(config.dtb_dir, PathBuf::from("/var/lib/ostool-server/dtbs"));
    }

    #[tokio::test]
    async fn write_to_path_persists_default_port_2999() {
        let temp = tempdir().unwrap();
        let path = temp.path().join("config.toml");
        let mut config =
            ServerConfig::default_for_path(Path::new("/etc/ostool-server/config.toml"));
        config.network.interface = "eth0".into();

        config.write_to_path(&path).await.unwrap();

        let content = std::fs::read_to_string(path).unwrap();
        assert!(content.contains("listen_addr = \"0.0.0.0:2999\""));
    }

    #[test]
    fn board_config_rejects_legacy_uboot_net_fields() {
        let config = r#"
id = "demo"
board_type = "demo"
tags = []
disabled = false

[boot]
kind = "uboot"
use_tftp = true

[boot.net]
interface = "eth0"
"#;

        let err = toml::from_str::<BoardConfig>(config).unwrap_err();
        let message = err.to_string();
        assert!(
            message.contains("unknown field") || message.contains("net"),
            "unexpected error: {message}"
        );
    }

    #[test]
    fn board_config_rejects_legacy_power_command_fields() {
        let config = r#"
id = "demo"
board_type = "demo"
tags = []
disabled = false

[boot]
kind = "uboot"
use_tftp = false
board_reset_cmd = "reboot"
board_power_off_cmd = "shutdown"
"#;

        let err = toml::from_str::<BoardConfig>(config).unwrap_err();
        let message = err.to_string();
        assert!(
            message.contains("unknown field") || message.contains("board_reset_cmd"),
            "unexpected error: {message}"
        );
    }

    #[test]
    fn board_config_serialization_omits_removed_fields() {
        let board = BoardConfig {
            id: "demo-1".into(),
            board_type: "demo".into(),
            tags: vec!["lab".into()],
            serial: None,
            power_management: PowerManagementConfig::Custom(CustomPowerManagement {
                power_on_cmd: "echo on".into(),
                power_off_cmd: "echo off".into(),
            }),
            boot: BootConfig::Uboot(UbootProfile {
                use_tftp: true,
                dtb_name: Some("board.dtb".into()),
            }),
            notes: None,
            disabled: false,
        };

        let value = serde_json::to_value(&board).unwrap();
        assert_eq!(value["id"], json!("demo-1"));
        assert!(value.get("name").is_none());
        assert!(value["boot"].get("success_regex").is_none());
        assert!(value["boot"].get("fail_regex").is_none());
        assert!(value["boot"].get("uboot_cmd").is_none());
        assert!(value["boot"].get("shell_prefix").is_none());
        assert!(value["boot"].get("shell_init_cmd").is_none());
        assert_eq!(value["boot"]["dtb_name"], json!("board.dtb"));
    }

    #[test]
    fn board_config_round_trip_supports_virtual_power_management() {
        let board = BoardConfig {
            id: "demo-virtual".into(),
            board_type: "demo".into(),
            tags: vec![],
            serial: None,
            power_management: PowerManagementConfig::Virtual(VirtualPowerManagement::default()),
            boot: BootConfig::Pxe(Default::default()),
            notes: None,
            disabled: false,
        };

        let encoded = toml::to_string_pretty(&board).unwrap();
        assert!(encoded.contains("kind = \"virtual\""));

        let decoded: BoardConfig = toml::from_str(&encoded).unwrap();
        assert!(matches!(
            decoded.power_management,
            PowerManagementConfig::Virtual(_)
        ));
    }
}
