use std::path::PathBuf;
use std::time::Duration;

use crate::error::{Error, Result};

/// TLS mode for the connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SslMode {
    /// No TLS. Connections are unencrypted.
    Disable,
    /// Try TLS, fall back to plaintext if server doesn't support it.
    #[default]
    Prefer,
    /// Require TLS. Fail if server doesn't support it.
    Require,
    /// Require TLS and verify the server certificate.
    VerifyCa,
    /// Require TLS, verify certificate, and verify hostname matches.
    VerifyFull,
}

/// Connection configuration for sentinel-driver.
///
/// # Connection String
///
/// ```text
/// postgres://user:password@host:port/database?sslmode=prefer&application_name=myapp
/// ```
///
/// # Builder
///
/// ```rust,no_run
/// use sentinel_driver::Config;
///
/// let config = Config::builder()
///     .host("localhost")
///     .port(5432)
///     .database("mydb")
///     .user("postgres")
///     .password("secret")
///     .build();
/// ```
#[derive(Clone)]
pub struct Config {
    pub(crate) hosts: Vec<(String, u16)>,
    pub(crate) database: String,
    pub(crate) user: String,
    pub(crate) password: Option<String>,
    pub(crate) ssl_mode: SslMode,
    pub(crate) application_name: Option<String>,
    pub(crate) connect_timeout: Duration,
    pub(crate) statement_timeout: Option<Duration>,
    pub(crate) keepalive: Option<Duration>,
    pub(crate) keepalive_idle: Option<Duration>,
    pub(crate) target_session_attrs: TargetSessionAttrs,
    pub(crate) extra_float_digits: Option<i32>,
    pub(crate) load_balance_hosts: LoadBalanceHosts,
    /// Path to client certificate file for certificate authentication.
    pub(crate) ssl_client_cert: Option<std::path::PathBuf>,
    /// Path to client private key file for certificate authentication.
    pub(crate) ssl_client_key: Option<std::path::PathBuf>,
    /// Use direct TLS connection (PG 17+) — skip SSLRequest negotiation.
    pub(crate) ssl_direct: bool,
    /// Enable SCRAM-SHA-256 channel binding (SCRAM-PLUS) when TLS is active.
    pub(crate) channel_binding: ChannelBinding,
    /// Pluggable instrumentation hook; `None` uses a no-op default.
    pub(crate) instrumentation: Option<std::sync::Arc<dyn crate::Instrumentation>>,
}

/// Channel binding preference for SCRAM authentication.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChannelBinding {
    /// Use channel binding if available (default).
    #[default]
    Prefer,
    /// Require channel binding — fail if server doesn't support it.
    Require,
    /// Disable channel binding.
    Disable,
}

/// Target session attributes for connection validation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TargetSessionAttrs {
    /// Any server is acceptable.
    #[default]
    Any,
    /// Only accept read-write servers (primary).
    ReadWrite,
    /// Only accept read-only servers (replica).
    ReadOnly,
}

/// Load balancing strategy for multi-host connections.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadBalanceHosts {
    /// Try hosts in order (default).
    #[default]
    Disable,
    /// Shuffle hosts before trying.
    Random,
}

impl Config {
    /// Parse a PostgreSQL connection string.
    ///
    /// Supported formats:
    /// - `postgres://user:password@host:port/database?param=value`
    /// - `postgresql://user:password@host:port/database?param=value`
    pub fn parse(s: &str) -> Result<Self> {
        let s = s.trim();

        let without_scheme = s
            .strip_prefix("postgres://")
            .or_else(|| s.strip_prefix("postgresql://"))
            .ok_or_else(|| {
                Error::Config(
                    "connection string must start with postgres:// or postgresql://".into(),
                )
            })?;

        let (userinfo, rest) = match without_scheme.split_once('@') {
            Some((ui, rest)) => (Some(ui), rest),
            None => (None, without_scheme),
        };

        let (user, password) = match userinfo {
            Some(ui) => match ui.split_once(':') {
                Some((u, p)) => (percent_decode(u)?, Some(percent_decode(p)?)),
                None => (percent_decode(ui)?, None),
            },
            None => (String::new(), None),
        };

        // Split host:port from database?params
        let (hostport, db_and_params) = match rest.split_once('/') {
            Some((hp, rest)) => (hp, Some(rest)),
            None => (rest, None),
        };

        // Parse comma-separated host:port pairs
        let mut hosts: Vec<(String, u16)> = Vec::new();
        if hostport.is_empty() {
            // Empty host — will be set via ?host= parameter (Unix socket)
        } else {
            for entry in hostport.split(',') {
                let (h, p) = match entry.rsplit_once(':') {
                    Some((h, p)) => {
                        let port: u16 = p
                            .parse()
                            .map_err(|_| Error::Config(format!("invalid port: {p}")))?;
                        (h.to_string(), port)
                    }
                    None => (entry.to_string(), 5432),
                };
                hosts.push((h, p));
            }
        }

        let (database, params_str) = match db_and_params {
            Some(dp) => match dp.split_once('?') {
                Some((db, params)) => (percent_decode(db)?, Some(params.to_string())),
                None => (percent_decode(dp)?, None),
            },
            None => (String::new(), None),
        };

        let mut config = ConfigBuilder::new();
        for (h, p) in &hosts {
            config = config.host_port(h.clone(), *p);
        }
        config = config.database(database).user(user);

        if let Some(pw) = password {
            config = config.password(pw);
        }

        // Parse query parameters
        if let Some(params) = params_str {
            for param in params.split('&') {
                let (key, value) = param
                    .split_once('=')
                    .ok_or_else(|| Error::Config(format!("invalid parameter: {param}")))?;
                let value = percent_decode(value)?;

                match key {
                    "sslmode" => {
                        config = config.ssl_mode(match value.as_str() {
                            "disable" => SslMode::Disable,
                            "prefer" => SslMode::Prefer,
                            "require" => SslMode::Require,
                            "verify-ca" => SslMode::VerifyCa,
                            "verify-full" => SslMode::VerifyFull,
                            _ => return Err(Error::Config(format!("invalid sslmode: {value}"))),
                        });
                    }
                    "application_name" => {
                        config = config.application_name(value);
                    }
                    "connect_timeout" => {
                        let secs: u64 = value.parse().map_err(|_| {
                            Error::Config(format!("invalid connect_timeout: {value}"))
                        })?;
                        config = config.connect_timeout(Duration::from_secs(secs));
                    }
                    "statement_timeout" => {
                        let secs: u64 = value.parse().map_err(|_| {
                            Error::Config(format!("invalid statement_timeout: {value}"))
                        })?;
                        config = config.statement_timeout(Duration::from_secs(secs));
                    }
                    "target_session_attrs" => {
                        config = config.target_session_attrs(match value.as_str() {
                            "any" => TargetSessionAttrs::Any,
                            "read-write" => TargetSessionAttrs::ReadWrite,
                            "read-only" => TargetSessionAttrs::ReadOnly,
                            _ => {
                                return Err(Error::Config(format!(
                                    "invalid target_session_attrs: {value}"
                                )))
                            }
                        });
                    }
                    "sslcert" => {
                        config = config.ssl_client_cert(PathBuf::from(value));
                    }
                    "sslkey" => {
                        config = config.ssl_client_key(PathBuf::from(value));
                    }
                    "ssldirect" | "sslnegotiation" => {
                        let direct = match value.as_str() {
                            "true" | "direct" => true,
                            "false" | "postgres" => false,
                            _ => return Err(Error::Config(format!("invalid {key}: {value}"))),
                        };
                        config = config.ssl_direct(direct);
                    }
                    "channel_binding" => {
                        config = config.channel_binding(match value.as_str() {
                            "prefer" => ChannelBinding::Prefer,
                            "require" => ChannelBinding::Require,
                            "disable" => ChannelBinding::Disable,
                            _ => {
                                return Err(Error::Config(format!(
                                    "invalid channel_binding: {value}"
                                )))
                            }
                        });
                    }
                    "load_balance_hosts" => {
                        config = config.load_balance_hosts(match value.as_str() {
                            "disable" => LoadBalanceHosts::Disable,
                            "random" => LoadBalanceHosts::Random,
                            _ => {
                                return Err(Error::Config(format!(
                                    "invalid load_balance_hosts: {value}"
                                )))
                            }
                        });
                    }
                    "host" => {
                        // Support ?host=/var/run/postgresql for Unix sockets
                        config = config.host_port(value, 5432);
                    }
                    _ => {
                        // Ignore unknown parameters for forward compatibility
                    }
                }
            }
        }

        Ok(config.build())
    }

    /// Create a new builder for `Config`.
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::new()
    }

    // Accessor methods

    /// Returns the first host (for backward compatibility and single-host use).
    pub fn host(&self) -> &str {
        self.hosts.first().map_or("localhost", |(h, _)| h.as_str())
    }

    /// Returns the first port (for backward compatibility and single-host use).
    pub fn port(&self) -> u16 {
        self.hosts.first().map_or(5432, |(_, p)| *p)
    }

    /// Returns all configured host/port pairs.
    pub fn hosts(&self) -> &[(String, u16)] {
        &self.hosts
    }

    /// Load balancing strategy for multi-host connections.
    pub fn load_balance_hosts(&self) -> LoadBalanceHosts {
        self.load_balance_hosts
    }

    /// Target session attributes for connection routing.
    pub fn target_session_attrs(&self) -> TargetSessionAttrs {
        self.target_session_attrs
    }

    pub fn database(&self) -> &str {
        &self.database
    }

    pub fn user(&self) -> &str {
        &self.user
    }

    pub fn password(&self) -> Option<&str> {
        self.password.as_deref()
    }

    pub fn ssl_mode(&self) -> SslMode {
        self.ssl_mode
    }

    pub fn application_name(&self) -> Option<&str> {
        self.application_name.as_deref()
    }

    pub fn connect_timeout(&self) -> Duration {
        self.connect_timeout
    }

    pub fn statement_timeout(&self) -> Option<Duration> {
        self.statement_timeout
    }

    /// Path to client certificate for certificate authentication.
    pub fn ssl_client_cert(&self) -> Option<&std::path::Path> {
        self.ssl_client_cert.as_deref()
    }

    /// Path to client private key for certificate authentication.
    pub fn ssl_client_key(&self) -> Option<&std::path::Path> {
        self.ssl_client_key.as_deref()
    }

    /// Whether direct TLS (PG 17+) is enabled.
    pub fn ssl_direct(&self) -> bool {
        self.ssl_direct
    }

    /// Channel binding preference for SCRAM authentication.
    pub fn channel_binding(&self) -> ChannelBinding {
        self.channel_binding
    }

    /// Install an `Instrumentation` impl. Inherited by every `Connection` and
    /// `Pool` built from this `Config`.
    pub fn with_instrumentation(
        mut self,
        instr: std::sync::Arc<dyn crate::Instrumentation>,
    ) -> Self {
        self.instrumentation = Some(instr);
        self
    }
}

impl std::fmt::Debug for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Config")
            .field("hosts", &self.hosts)
            .field("database", &self.database)
            .field("user", &self.user)
            .field("password", &self.password.as_ref().map(|_| "..."))
            .field("ssl_mode", &self.ssl_mode)
            .field("application_name", &self.application_name)
            .field("connect_timeout", &self.connect_timeout)
            .field("statement_timeout", &self.statement_timeout)
            .field("_keepalive", &self.keepalive)
            .field("_keepalive_idle", &self.keepalive_idle)
            .field("_extra_float_digits", &self.extra_float_digits)
            .field("target_session_attrs", &self.target_session_attrs)
            .field("load_balance_hosts", &self.load_balance_hosts)
            .field("ssl_client_cert", &self.ssl_client_cert)
            .field("ssl_client_key", &self.ssl_client_key)
            .field("ssl_direct", &self.ssl_direct)
            .field("channel_binding", &self.channel_binding)
            .field(
                "instrumentation",
                &self.instrumentation.as_ref().map(|_| "..."),
            )
            .finish()
    }
}

/// Builder for [`Config`].
#[derive(Debug, Clone)]
pub struct ConfigBuilder {
    hosts: Vec<(String, u16)>,
    default_port: u16,
    database: String,
    user: String,
    password: Option<String>,
    ssl_mode: SslMode,
    application_name: Option<String>,
    connect_timeout: Duration,
    statement_timeout: Option<Duration>,
    keepalive: Option<Duration>,
    keepalive_idle: Option<Duration>,
    target_session_attrs: TargetSessionAttrs,
    extra_float_digits: Option<i32>,
    load_balance_hosts: LoadBalanceHosts,
    ssl_client_cert: Option<PathBuf>,
    ssl_client_key: Option<PathBuf>,
    ssl_direct: bool,
    channel_binding: ChannelBinding,
}

impl ConfigBuilder {
    fn new() -> Self {
        Self {
            hosts: Vec::new(),
            default_port: 5432,
            database: String::new(),
            user: String::new(),
            password: None,
            ssl_mode: SslMode::default(),
            application_name: None,
            connect_timeout: Duration::from_secs(10),
            statement_timeout: None,
            keepalive: Some(Duration::from_secs(60)),
            keepalive_idle: None,
            target_session_attrs: TargetSessionAttrs::default(),
            extra_float_digits: Some(3),
            load_balance_hosts: LoadBalanceHosts::default(),
            ssl_client_cert: None,
            ssl_client_key: None,
            ssl_direct: false,
            channel_binding: ChannelBinding::default(),
        }
    }

    /// Append a host with the current default port.
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.hosts.push((host.into(), self.default_port));
        self
    }

    /// Append a host with a specific port.
    pub fn host_port(mut self, host: impl Into<String>, port: u16) -> Self {
        self.hosts.push((host.into(), port));
        self
    }

    /// Set the default port for subsequent `.host()` calls and update
    /// any hosts that still have the old default port.
    pub fn port(mut self, port: u16) -> Self {
        let old_default = self.default_port;
        self.default_port = port;
        for (_, p) in &mut self.hosts {
            if *p == old_default {
                *p = port;
            }
        }
        self
    }

    pub fn load_balance_hosts(mut self, strategy: LoadBalanceHosts) -> Self {
        self.load_balance_hosts = strategy;
        self
    }

    pub fn database(mut self, database: impl Into<String>) -> Self {
        self.database = database.into();
        self
    }

    pub fn user(mut self, user: impl Into<String>) -> Self {
        self.user = user.into();
        self
    }

    pub fn password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }

    pub fn ssl_mode(mut self, ssl_mode: SslMode) -> Self {
        self.ssl_mode = ssl_mode;
        self
    }

    pub fn application_name(mut self, name: impl Into<String>) -> Self {
        self.application_name = Some(name.into());
        self
    }

    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
        self.connect_timeout = timeout;
        self
    }

    pub fn statement_timeout(mut self, timeout: Duration) -> Self {
        self.statement_timeout = Some(timeout);
        self
    }

    pub fn keepalive(mut self, interval: Duration) -> Self {
        self.keepalive = Some(interval);
        self
    }

    pub fn target_session_attrs(mut self, attrs: TargetSessionAttrs) -> Self {
        self.target_session_attrs = attrs;
        self
    }

    /// Set the path to the client certificate file for certificate authentication.
    pub fn ssl_client_cert(mut self, path: impl Into<PathBuf>) -> Self {
        self.ssl_client_cert = Some(path.into());
        self
    }

    /// Set the path to the client private key file for certificate authentication.
    pub fn ssl_client_key(mut self, path: impl Into<PathBuf>) -> Self {
        self.ssl_client_key = Some(path.into());
        self
    }

    /// Enable direct TLS connection (PG 17+), skipping SSLRequest negotiation.
    pub fn ssl_direct(mut self, direct: bool) -> Self {
        self.ssl_direct = direct;
        self
    }

    /// Set the channel binding preference for SCRAM authentication.
    pub fn channel_binding(mut self, binding: ChannelBinding) -> Self {
        self.channel_binding = binding;
        self
    }

    /// Build the final `Config`.
    pub fn build(self) -> Config {
        let hosts = if self.hosts.is_empty() {
            vec![("localhost".to_string(), self.default_port)]
        } else {
            self.hosts
        };
        Config {
            hosts,
            database: self.database,
            user: self.user,
            password: self.password,
            ssl_mode: self.ssl_mode,
            application_name: self.application_name,
            connect_timeout: self.connect_timeout,
            statement_timeout: self.statement_timeout,
            keepalive: self.keepalive,
            keepalive_idle: self.keepalive_idle,
            target_session_attrs: self.target_session_attrs,
            extra_float_digits: self.extra_float_digits,
            load_balance_hosts: self.load_balance_hosts,
            ssl_client_cert: self.ssl_client_cert,
            ssl_client_key: self.ssl_client_key,
            ssl_direct: self.ssl_direct,
            channel_binding: self.channel_binding,
            instrumentation: None,
        }
    }
}

/// Percent-decode a URL component.
fn percent_decode(s: &str) -> Result<String> {
    let mut result = String::with_capacity(s.len());
    let mut chars = s.as_bytes().iter();

    while let Some(&b) = chars.next() {
        if b == b'%' {
            let hi = chars
                .next()
                .ok_or_else(|| Error::Config("incomplete percent encoding".into()))?;
            let lo = chars
                .next()
                .ok_or_else(|| Error::Config("incomplete percent encoding".into()))?;
            let byte = hex_digit(*hi)? << 4 | hex_digit(*lo)?;
            result.push(byte as char);
        } else {
            result.push(b as char);
        }
    }

    Ok(result)
}

fn hex_digit(b: u8) -> Result<u8> {
    match b {
        b'0'..=b'9' => Ok(b - b'0'),
        b'a'..=b'f' => Ok(b - b'a' + 10),
        b'A'..=b'F' => Ok(b - b'A' + 10),
        _ => Err(Error::Config(format!("invalid hex digit: {}", b as char))),
    }
}

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

    #[test]
    fn config_builder_build_populates_all_fields() {
        let cfg = ConfigBuilder::new()
            .host("localhost".to_string())
            .port(5432)
            .database("test".to_string())
            .user("postgres".to_string())
            .password("secret".to_string())
            .application_name("test_app".to_string())
            .connect_timeout(std::time::Duration::from_secs(10))
            .channel_binding(ChannelBinding::Prefer)
            .build();
        assert_eq!(cfg.user, "postgres");
        assert_eq!(cfg.database, "test");
        assert_eq!(cfg.application_name.as_deref(), Some("test_app"));
        assert_eq!(cfg.channel_binding, ChannelBinding::Prefer);
        // keepalive defaults to Some(60s) in ConfigBuilder::new()
        assert_eq!(cfg.keepalive, Some(std::time::Duration::from_secs(60)));
        assert!(cfg.keepalive_idle.is_none());
        // extra_float_digits defaults to Some(3) in ConfigBuilder::new()
        assert_eq!(cfg.extra_float_digits, Some(3));
        assert!(cfg.instrumentation.is_none());
    }

    #[test]
    fn channel_binding_accessor() {
        let cfg = ConfigBuilder::new()
            .channel_binding(ChannelBinding::Require)
            .build();
        assert_eq!(cfg.channel_binding(), ChannelBinding::Require);
    }

    #[test]
    fn with_instrumentation_sets_field() {
        struct NoOp;
        impl crate::Instrumentation for NoOp {
            fn on_event(&self, _: &crate::Event<'_>) {}
        }
        let cfg = ConfigBuilder::new()
            .build()
            .with_instrumentation(std::sync::Arc::new(NoOp));
        assert!(cfg.instrumentation.is_some());
    }

    #[test]
    fn debug_redacts_password() {
        let cfg = ConfigBuilder::new()
            .password("super_secret".to_string())
            .build();
        let s = format!("{cfg:?}");
        assert!(!s.contains("super_secret"));
        assert!(s.contains("password"));
    }
}
