//! Security: JWT validation and IP whitelist checking.
//!
//! Matches Go's security/guard.go and security/jwt.go.
//! - Guard: combines whitelist IP checking with JWT token validation
//! - JWT: HS256 HMAC signing with file-id claims

pub mod tls;

use std::collections::HashSet;
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};

use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};

// ============================================================================
// JWT Claims
// ============================================================================

/// Claims for volume server file access tokens.
/// Matches Go's `SeaweedFileIdClaims`.
#[derive(Debug, Serialize, Deserialize)]
pub struct FileIdClaims {
    /// File ID this token grants access to (e.g., "3,01637037d6").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fid: Option<String>,

    /// Expiration time (Unix timestamp).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exp: Option<u64>,

    /// Not before (Unix timestamp).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nbf: Option<u64>,
}

/// Signing key wrapper (empty = security disabled).
#[derive(Clone)]
pub struct SigningKey(pub Vec<u8>);

impl SigningKey {
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn from_string(s: &str) -> Self {
        SigningKey(s.as_bytes().to_vec())
    }
}

/// Generate a JWT token for file access.
pub fn gen_jwt(
    signing_key: &SigningKey,
    expires_after_sec: i64,
    file_id: &str,
) -> Result<String, JwtError> {
    if signing_key.is_empty() {
        return Err(JwtError::NoSigningKey);
    }

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();

    let claims = FileIdClaims {
        fid: Some(file_id.to_string()),
        exp: if expires_after_sec > 0 {
            Some(now + expires_after_sec as u64)
        } else {
            None
        },
        nbf: None,
    };

    let token = encode(
        &Header::new(Algorithm::HS256),
        &claims,
        &EncodingKey::from_secret(&signing_key.0),
    )?;

    Ok(token)
}

/// Decode and validate a JWT token.
pub fn decode_jwt(signing_key: &SigningKey, token: &str) -> Result<FileIdClaims, JwtError> {
    if signing_key.is_empty() {
        return Err(JwtError::NoSigningKey);
    }

    let mut validation = Validation::new(Algorithm::HS256);
    // Match Go behavior: tokens without exp are accepted (Go's jwt-go does not require exp)
    // But if exp IS present, it must be valid (not expired).
    validation.required_spec_claims.clear();
    validation.validate_exp = true;
    // Go's jwt-go/v5 validates nbf when present
    validation.validate_nbf = true;
    validation.leeway = 0;

    let data = decode::<FileIdClaims>(
        token,
        &DecodingKey::from_secret(&signing_key.0),
        &validation,
    )?;

    Ok(data.claims)
}

// ============================================================================
// Guard
// ============================================================================

/// Security guard: IP whitelist + JWT token validation.
pub struct Guard {
    whitelist_ips: HashSet<String>,
    whitelist_cidrs: Vec<(IpAddr, u8)>, // (network, prefix_len)
    pub signing_key: SigningKey,
    pub expires_after_sec: i64,
    pub read_signing_key: SigningKey,
    pub read_expires_after_sec: i64,
    /// Combined flag: true when whitelist is non-empty OR signing key is present.
    /// Matches Go's `isWriteActive = !isEmptyWhiteList || len(SigningKey) != 0`.
    is_write_active: bool,
}

impl Guard {
    pub fn new(
        whitelist: &[String],
        signing_key: SigningKey,
        expires_after_sec: i64,
        read_signing_key: SigningKey,
        read_expires_after_sec: i64,
    ) -> Self {
        let mut guard = Guard {
            whitelist_ips: HashSet::new(),
            whitelist_cidrs: Vec::new(),
            signing_key,
            expires_after_sec,
            read_signing_key,
            read_expires_after_sec,
            is_write_active: false,
        };
        guard.update_whitelist(whitelist);
        guard
    }

    /// Update the IP whitelist.
    pub fn update_whitelist(&mut self, entries: &[String]) {
        self.whitelist_ips.clear();
        self.whitelist_cidrs.clear();

        for entry in entries {
            let entry = entry.trim();
            if entry.is_empty() {
                continue;
            }
            if entry.contains('/') {
                // CIDR range
                if let Some((ip, prefix)) = parse_cidr(entry) {
                    self.whitelist_cidrs.push((ip, prefix));
                } else {
                    tracing::error!("Parse CIDR {} in whitelist failed", entry);
                }
            } else {
                // Exact IP/hostname
                self.whitelist_ips.insert(entry.to_string());
            }
        }

        // Match Go: isWriteActive = !isEmptyWhiteList || len(SigningKey) != 0
        let is_empty_whitelist = self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty();
        self.is_write_active = !is_empty_whitelist || !self.signing_key.is_empty();
    }

    /// Refresh the JWT signing keys and their expirations in place so operators
    /// can rotate keys via SIGHUP without restarting the process. Mirrors Go's
    /// `Guard::UpdateSigningKeys`: it swaps the fields and recomputes
    /// `is_write_active`.
    pub fn update_signing_keys(
        &mut self,
        signing_key: SigningKey,
        expires_after_sec: i64,
        read_signing_key: SigningKey,
        read_expires_after_sec: i64,
    ) {
        self.signing_key = signing_key;
        self.expires_after_sec = expires_after_sec;
        self.read_signing_key = read_signing_key;
        self.read_expires_after_sec = read_expires_after_sec;

        let is_empty_whitelist = self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty();
        self.is_write_active = !is_empty_whitelist || !self.signing_key.is_empty();
    }

    /// Whether any write-side control is configured: a non-empty whitelist or a
    /// signing key. Mirrors Go's non-nil `guard` -- when this is false, every
    /// caller is allowed and admin RPCs need not carry peer info.
    pub fn is_write_active(&self) -> bool {
        self.is_write_active
    }

    /// Check if a remote IP is in the whitelist.
    /// Returns true if write security is inactive (no whitelist and no signing key),
    /// if the whitelist is empty, or if the IP matches.
    pub fn check_whitelist(&self, remote_addr: &str) -> bool {
        if !self.is_write_active {
            return true;
        }
        if self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty() {
            return true;
        }

        let host = extract_host(remote_addr);

        // Check exact match
        if self.whitelist_ips.contains(&host) {
            return true;
        }

        // Check CIDR ranges
        if let Ok(ip) = host.parse::<IpAddr>() {
            for &(ref network, prefix_len) in &self.whitelist_cidrs {
                if ip_in_cidr(&ip, network, prefix_len) {
                    return true;
                }
            }
        }

        false
    }

    /// Check if a read signing key is configured.
    pub fn has_read_signing_key(&self) -> bool {
        !self.read_signing_key.is_empty()
    }

    /// Validate a request's JWT token.
    /// `is_write` determines which signing key to use.
    /// Returns Ok(()) if valid, or if security is disabled.
    pub fn check_jwt(&self, token: Option<&str>, is_write: bool) -> Result<(), JwtError> {
        let key = if is_write {
            &self.signing_key
        } else {
            &self.read_signing_key
        };

        if key.is_empty() {
            return Ok(()); // Security disabled for this operation type
        }

        let token = token.ok_or(JwtError::MissingToken)?;
        decode_jwt(key, token)?;
        Ok(())
    }

    /// Check JWT and validate the file ID claim matches.
    pub fn check_jwt_for_file(
        &self,
        token: Option<&str>,
        expected_fid: &str,
        is_write: bool,
    ) -> Result<(), JwtError> {
        let key = if is_write {
            &self.signing_key
        } else {
            &self.read_signing_key
        };

        if key.is_empty() {
            return Ok(());
        }

        let token = token.ok_or(JwtError::MissingToken)?;
        let claims = decode_jwt(key, token)?;

        match claims.fid {
            None => {
                return Err(JwtError::MissingFileIdClaim);
            }
            Some(ref fid) if fid != expected_fid => {
                return Err(JwtError::FileIdMismatch {
                    expected: expected_fid.to_string(),
                    got: fid.to_string(),
                });
            }
            _ => {}
        }

        Ok(())
    }
}

// ============================================================================
// Helpers
// ============================================================================

/// Extract host from "host:port" or "[::1]:port" format.
fn extract_host(addr: &str) -> String {
    // Handle IPv6 with brackets
    if addr.starts_with('[') {
        if let Some(end) = addr.find(']') {
            return addr[1..end].to_string();
        }
    }
    // Handle host:port
    if let Some(pos) = addr.rfind(':') {
        return addr[..pos].to_string();
    }
    addr.to_string()
}

/// Parse CIDR notation "192.168.1.0/24" into (IpAddr, prefix_len).
fn parse_cidr(cidr: &str) -> Option<(IpAddr, u8)> {
    let parts: Vec<&str> = cidr.split('/').collect();
    if parts.len() != 2 {
        return None;
    }
    let ip: IpAddr = parts[0].parse().ok()?;
    let prefix: u8 = parts[1].parse().ok()?;
    Some((ip, prefix))
}

/// Check if an IP is within a CIDR range.
fn ip_in_cidr(ip: &IpAddr, network: &IpAddr, prefix_len: u8) -> bool {
    match (ip, network) {
        (IpAddr::V4(ip), IpAddr::V4(net)) => {
            let ip_bits = u32::from(*ip);
            let net_bits = u32::from(*net);
            let mask = if prefix_len == 0 {
                0
            } else if prefix_len >= 32 {
                u32::MAX
            } else {
                u32::MAX << (32 - prefix_len)
            };
            (ip_bits & mask) == (net_bits & mask)
        }
        (IpAddr::V6(ip), IpAddr::V6(net)) => {
            let ip_bits = u128::from(*ip);
            let net_bits = u128::from(*net);
            let mask = if prefix_len == 0 {
                0
            } else if prefix_len >= 128 {
                u128::MAX
            } else {
                u128::MAX << (128 - prefix_len)
            };
            (ip_bits & mask) == (net_bits & mask)
        }
        _ => false, // V4/V6 mismatch
    }
}

// ============================================================================
// Errors
// ============================================================================

#[derive(Debug, thiserror::Error)]
pub enum JwtError {
    #[error("no signing key configured")]
    NoSigningKey,

    #[error("missing JWT token")]
    MissingToken,

    #[error("JWT error: {0}")]
    Jwt(#[from] jsonwebtoken::errors::Error),

    #[error("JWT token missing required fid claim")]
    MissingFileIdClaim,

    #[error("file ID mismatch: expected {expected}, got {got}")]
    FileIdMismatch { expected: String, got: String },
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_jwt_round_trip() {
        let key = SigningKey::from_string("test-secret-key");
        let token = gen_jwt(&key, 3600, "3,01637037d6").unwrap();
        let claims = decode_jwt(&key, &token).unwrap();
        assert_eq!(claims.fid, Some("3,01637037d6".to_string()));
    }

    #[test]
    fn test_jwt_no_signing_key() {
        let key = SigningKey(vec![]);
        assert!(gen_jwt(&key, 3600, "1,abc").is_err());
    }

    #[test]
    fn test_jwt_invalid_token() {
        let key = SigningKey::from_string("secret");
        let result = decode_jwt(&key, "invalid.token.here");
        assert!(result.is_err());
    }

    #[test]
    fn test_jwt_wrong_key() {
        let key1 = SigningKey::from_string("secret1");
        let key2 = SigningKey::from_string("secret2");
        let token = gen_jwt(&key1, 3600, "1,abc").unwrap();
        assert!(decode_jwt(&key2, &token).is_err());
    }

    #[test]
    fn test_guard_empty_whitelist() {
        let guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
        assert!(guard.check_whitelist("192.168.1.1:8080"));
    }

    #[test]
    fn test_guard_whitelist_exact() {
        let guard = Guard::new(
            &["192.168.1.1".to_string(), "10.0.0.1".to_string()],
            SigningKey(vec![]),
            0,
            SigningKey(vec![]),
            0,
        );
        assert!(guard.check_whitelist("192.168.1.1:8080"));
        assert!(guard.check_whitelist("10.0.0.1:1234"));
        assert!(!guard.check_whitelist("172.16.0.1:8080"));
    }

    #[test]
    fn test_guard_whitelist_cidr() {
        let guard = Guard::new(
            &["10.0.0.0/8".to_string()],
            SigningKey(vec![]),
            0,
            SigningKey(vec![]),
            0,
        );
        assert!(guard.check_whitelist("10.1.2.3:8080"));
        assert!(guard.check_whitelist("10.255.255.255:80"));
        assert!(!guard.check_whitelist("11.0.0.1:80"));
    }

    #[test]
    fn test_guard_check_jwt_disabled() {
        let guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
        // No signing key = security disabled
        assert!(guard.check_jwt(None, true).is_ok());
        assert!(guard.check_jwt(None, false).is_ok());
    }

    #[test]
    fn test_guard_check_jwt_enabled() {
        let key = SigningKey::from_string("write-secret");
        let read_key = SigningKey::from_string("read-secret");
        let guard = Guard::new(&[], key.clone(), 3600, read_key.clone(), 3600);

        // Missing token
        assert!(guard.check_jwt(None, true).is_err());

        // Valid write token
        let token = gen_jwt(&key, 3600, "1,abc").unwrap();
        assert!(guard.check_jwt(Some(&token), true).is_ok());

        // Write token for read should fail (different key)
        assert!(guard.check_jwt(Some(&token), false).is_err());

        // Valid read token
        let read_token = gen_jwt(&read_key, 3600, "1,abc").unwrap();
        assert!(guard.check_jwt(Some(&read_token), false).is_ok());
    }

    #[test]
    fn test_guard_check_jwt_file_id() {
        let key = SigningKey::from_string("secret");
        let guard = Guard::new(&[], key.clone(), 3600, SigningKey(vec![]), 0);

        let token = gen_jwt(&key, 3600, "3,01637037d6").unwrap();

        // Correct file ID
        assert!(guard
            .check_jwt_for_file(Some(&token), "3,01637037d6", true)
            .is_ok());

        // Wrong file ID
        let err = guard.check_jwt_for_file(Some(&token), "4,deadbeef", true);
        assert!(matches!(err, Err(JwtError::FileIdMismatch { .. })));
    }

    #[test]
    fn test_update_signing_keys_rotates() {
        let mut guard = Guard::new(
            &[],
            SigningKey::from_string("old-write"),
            10,
            SigningKey::from_string("old-read"),
            60,
        );

        guard.update_signing_keys(
            SigningKey::from_string("new-write"),
            11,
            SigningKey::from_string("new-read"),
            61,
        );

        let token = gen_jwt(&guard.signing_key, 60, "3,01637037d6").unwrap();
        // Validates with the rotated key, not the old one.
        assert!(decode_jwt(&guard.signing_key, &token).is_ok());
        assert!(decode_jwt(&SigningKey::from_string("old-write"), &token).is_err());
        assert_eq!(guard.expires_after_sec, 11);
        assert_eq!(guard.read_expires_after_sec, 61);
    }

    #[test]
    fn test_update_signing_keys_toggles_write_active() {
        let mut guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
        assert!(guard.check_jwt(None, true).is_ok());

        guard.update_signing_keys(SigningKey::from_string("write"), 10, SigningKey(vec![]), 0);
        assert!(guard.is_write_active);

        guard.update_signing_keys(SigningKey(vec![]), 0, SigningKey(vec![]), 0);
        assert!(!guard.is_write_active);
    }

    #[test]
    fn test_extract_host() {
        assert_eq!(extract_host("192.168.1.1:8080"), "192.168.1.1");
        assert_eq!(extract_host("[::1]:8080"), "::1");
        assert_eq!(extract_host("localhost"), "localhost");
    }

    #[test]
    fn test_ip_in_cidr() {
        let net: IpAddr = "10.0.0.0".parse().unwrap();
        let ip1: IpAddr = "10.1.2.3".parse().unwrap();
        let ip2: IpAddr = "11.0.0.1".parse().unwrap();
        assert!(ip_in_cidr(&ip1, &net, 8));
        assert!(!ip_in_cidr(&ip2, &net, 8));
    }
}
