use std::path::PathBuf;

use anyhow::{Result, ensure};
use serde::{Deserialize, Serialize};
use tici_system::readable::{ReadableDuration, ReadableSize};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct GrpcConfig {
    pub max_decoding_message_size: ReadableSize,
    pub max_encoding_message_size: ReadableSize,
    pub connect_timeout: ReadableDuration,
    pub request_timeout: ReadableDuration,
}

impl Default for GrpcConfig {
    fn default() -> Self {
        GrpcConfig {
            max_decoding_message_size: ReadableSize::mb(64),
            max_encoding_message_size: ReadableSize::mb(64),
            connect_timeout: ReadableDuration::secs(5),
            request_timeout: ReadableDuration::secs(10),
        }
    }
}

impl GrpcConfig {
    pub fn validate(&self) -> Result<()> {
        ensure!(
            self.max_decoding_message_size >= ReadableSize::mb(1),
            "max_decoding_message_size must be at least 1MB"
        );
        ensure!(
            self.max_encoding_message_size >= ReadableSize::mb(1),
            "max_encoding_message_size must be at least 1MB"
        );
        ensure!(
            self.connect_timeout > ReadableDuration::ZERO,
            "connect_timeout must be greater than 0"
        );
        ensure!(
            self.request_timeout > ReadableDuration::ZERO,
            "request_timeout must be greater than 0"
        );
        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct ServerConfig {
    // The server listen to `addr`.
    pub addr: String,
    // The client use `advertise_addr` to connect the server.
    pub(super) advertise_addr: String,
    pub status_addr: String,
    advertise_status_addr: String,
    // The address of PD, for leader campaign and lease
    pub pd_addr: String,
    pub status_server_store_path: String,
    pub grpc: GrpcConfig,
    pub grpc_client: GrpcConfig,
}

impl ServerConfig {
    pub fn deduce_status_server_store_path() -> PathBuf {
        if let Ok(path) = std::env::current_exe() {
            if let Some(parent_dir) = path.parent() {
                parent_dir.to_path_buf()
            } else {
                PathBuf::new()
            }
        } else {
            PathBuf::new()
        }
    }

    pub fn get_advertise_addr(&self) -> &str {
        if self.advertise_addr.is_empty() {
            self.addr.as_str()
        } else {
            self.advertise_addr.as_str()
        }
    }

    pub fn set_advertise_addr(&mut self, addr: String) {
        self.advertise_addr = addr;
    }

    pub fn get_advertise_status_addr(&self) -> &str {
        if self.advertise_status_addr.is_empty() {
            self.status_addr.as_str()
        } else {
            self.advertise_status_addr.as_str()
        }
    }

    /// Parse PD addresses from the pd_addr field
    pub fn get_pd_addresses(&self) -> Vec<String> {
        self.pd_addr
            .split(',')
            .map(|addr| addr.trim().to_string())
            .filter(|addr| !addr.is_empty())
            .collect()
    }

    pub fn default_for_meta_test(enable_status_server: bool) -> ServerConfig {
        let f = tempfile::NamedTempFile::new()
            .map_err(|e| format!("create tmp file fail, err={}", e))
            .unwrap();
        ServerConfig {
            addr: "127.0.0.1:8500".to_string(),
            advertise_addr: "127.0.0.1:8500".to_string(),
            status_addr: if enable_status_server {
                "127.0.0.1:8501".to_string()
            } else {
                String::new()
            },
            advertise_status_addr: if enable_status_server {
                "127.0.0.1:8501".to_string()
            } else {
                String::new()
            },
            pd_addr: "127.0.0.1:2379".to_string(),
            status_server_store_path: f.path().to_str().unwrap().to_string(),
            grpc: GrpcConfig::default(),
            grpc_client: GrpcConfig::default(),
        }
    }

    pub fn default_for_worker_node_test(enable_status_server: bool) -> ServerConfig {
        let f = tempfile::NamedTempFile::new()
            .map_err(|e| format!("create tmp file fail, err={}", e))
            .unwrap();
        ServerConfig {
            addr: "127.0.0.1:8510".to_string(),
            advertise_addr: "127.0.0.1:8510".to_string(),
            status_addr: if enable_status_server {
                "127.0.0.1:8511".to_string()
            } else {
                String::new()
            },
            advertise_status_addr: if enable_status_server {
                "127.0.0.1:8511".to_string()
            } else {
                String::new()
            },
            pd_addr: "127.0.0.1:2379".to_string(),
            status_server_store_path: f.path().to_str().unwrap().to_string(),
            grpc: GrpcConfig::default(),
            grpc_client: GrpcConfig::default(),
        }
    }

    pub fn adjust_by_cmdline(
        &mut self,
        host: &str,
        port: &str,
        status_port: &str,
        advertise_host: &str,
        pd_addr: &str,
    ) -> Result<()> {
        if self.addr.is_empty() {
            self.addr = format!("{}:{}", host, port);
        } else if host.is_empty() && port.is_empty() {
            // do nothing
        } else {
            let addr = format!("{}:{}", host, port);
            ensure!(
                self.addr == addr,
                "Config addr {} conflicts with CLI arg {}",
                self.addr,
                addr
            );
        }

        if self.status_addr.is_empty() && !status_port.is_empty() {
            self.status_addr = format!("{}:{}", host, status_port);
        } else if !self.status_addr.is_empty() && status_port.is_empty() {
            // do nothing
        } else {
            let status_addr = if status_port.is_empty() {
                String::new()
            } else {
                format!("{}:{}", host, status_port)
            };
            ensure!(
                self.status_addr == status_addr,
                "Config status_addr {} conflicts with CLI arg {}",
                self.status_addr,
                status_addr
            );
        }

        if self.advertise_addr.is_empty() {
            self.advertise_addr = format!("{}:{}", advertise_host, port);
        } else if port.is_empty() {
            // do nothing
        } else {
            let advertise_addr = format!("{}:{}", advertise_host, port);
            ensure!(
                self.advertise_addr == advertise_addr,
                "Config advertise_addr {} conflicts with CLI arg {}",
                self.advertise_addr,
                advertise_addr
            );
        }

        if self.advertise_status_addr.is_empty() && !status_port.is_empty() {
            self.advertise_status_addr = format!("{}:{}", advertise_host, status_port);
        } else if !self.advertise_status_addr.is_empty() && status_port.is_empty() {
            // do nothing
        } else {
            let advertise_status_addr = if status_port.is_empty() {
                String::new()
            } else {
                format!("{}:{}", advertise_host, status_port)
            };
            ensure!(
                self.advertise_status_addr == advertise_status_addr,
                "Config advertise_status_addr {} conflicts with CLI arg {}",
                self.advertise_status_addr,
                advertise_status_addr
            );
        }

        if self.pd_addr.is_empty() {
            self.pd_addr = pd_addr.to_string();
        } else if pd_addr.is_empty() {
            // do nothing
        } else {
            ensure!(
                self.pd_addr == *pd_addr,
                "Config pd_addr {} conflicts with CLI arg {}",
                self.pd_addr,
                pd_addr
            );
        }

        Ok(())
    }
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            addr: "".to_string(),
            advertise_addr: "".to_string(),
            status_addr: "".to_string(),
            advertise_status_addr: "".to_string(),
            pd_addr: "".to_string(),
            status_server_store_path: "".to_string(),
            grpc: GrpcConfig::default(),
            grpc_client: GrpcConfig::default(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct S3Config {
    pub endpoint: String,
    pub region: String,
    pub access_key: String,
    pub secret_key: String,
    pub use_path_style: bool,
    pub bucket: String,
    pub prefix: String,
}

impl Default for S3Config {
    fn default() -> Self {
        Self {
            endpoint: "http://localhost:9000".to_string(),
            region: "us-east-1".to_string(),
            access_key: "".to_string(),
            secret_key: "".to_string(),
            use_path_style: true,
            bucket: "ticidefaultbucket".to_string(),
            prefix: "tici_default_prefix".to_string(),
        }
    }
}

impl S3Config {
    pub fn has_key(&self) -> bool {
        !self.access_key.trim().is_empty() && !self.secret_key.trim().is_empty()
    }

    #[allow(clippy::field_reassign_with_default)]
    pub fn default_for_test() -> Self {
        let mut config = S3Config::default();
        // Or
        // ```
        // export AWS_ACCESS_KEY_ID=minioadmin
        // export AWS_SECRET_ACCESS_KEY=minioadmin
        // ```
        config.access_key = "minioadmin".to_string();
        config.secret_key = "minioadmin".to_string();
        config
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct LogConfig {
    pub filename: String,
    level: String,
    pub size: ReadableSize,
    pub count: usize,
}

impl Default for LogConfig {
    fn default() -> Self {
        Self {
            filename: "".to_string(),
            level: "".to_string(),
            // After compression, the log size on disk will be much smaller than its rotate size.
            // For example, in meta service, an 256 MiB log will compressed into ~26 MiB .gz file.
            size: ReadableSize::gb(2),
            count: 100,
        }
    }
}

impl LogConfig {
    pub fn new(filename: String) -> Self {
        Self {
            filename,
            ..Default::default()
        }
    }

    pub fn overwrite_by_cmdline(
        &mut self,
        filename: &Option<String>,
        level: &Option<String>,
        size: &Option<ReadableSize>,
        count: &Option<usize>,
    ) {
        if let Some(filename) = filename {
            self.filename = filename.to_string();
        }
        if let Some(level) = level {
            if !level.is_empty() {
                self.level = level.to_string();
            }
        }
        if let Some(size) = size {
            self.size = *size;
        }
        if let Some(count) = count {
            self.count = *count;
        }
    }

    pub fn get_level_or(&self, default: &str) -> String {
        if !self.level.is_empty() {
            self.level.clone()
        } else if default.is_empty() {
            "info".to_string()
        } else {
            default.to_string()
        }
    }
}
