//! Authorized Keys Manager
//!
//! Manages SSH authorized_keys file parsing and manipulation.

use std::path::Path;

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use tokio::fs;
use tracing::{debug, warn};

/// Entry in authorized_keys file
#[derive(Debug, Clone)]
pub struct AuthorizedKeysEntry {
    /// Public key
    pub public_key: String,
    /// Key ID (from comment)
    pub key_id: Option<String>,
    /// Expiration timestamp (from comment)
    pub expires_at: Option<DateTime<Utc>>,
    /// Full original line
    pub original_line: String,
}

/// Authorized keys file manager
pub struct AuthorizedKeysManager;

impl AuthorizedKeysManager {
    /// Parse authorized_keys file
    pub async fn parse_file<P: AsRef<Path>>(path: P) -> Result<Vec<AuthorizedKeysEntry>> {
        let content = fs::read_to_string(path.as_ref())
            .await
            .context("Failed to read authorized_keys file")?;

        Ok(Self::parse_content(&content))
    }

    /// Parse authorized_keys content
    pub fn parse_content(content: &str) -> Vec<AuthorizedKeysEntry> {
        content
            .lines()
            .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
            .map(Self::parse_line)
            .collect()
    }

    /// Parse single authorized_keys line
    fn parse_line(line: &str) -> AuthorizedKeysEntry {
        let original_line = line.to_string();

        // Extract key ID and expiration from comment
        let (key_id, expires_at) = Self::extract_metadata(line);

        // Extract public key (everything before the comment)
        let public_key = if let Some(comment_idx) = line.rfind('#') {
            line[..comment_idx].trim().to_string()
        } else {
            line.trim().to_string()
        };

        AuthorizedKeysEntry {
            public_key,
            key_id,
            expires_at,
            original_line,
        }
    }

    /// Extract metadata from comment
    fn extract_metadata(line: &str) -> (Option<String>, Option<DateTime<Utc>>) {
        let comment = if let Some(idx) = line.rfind('#') {
            &line[idx + 1..]
        } else {
            return (None, None);
        };

        let mut key_id = None;
        let mut expires_at = None;

        // Parse KeyID: value
        if let Some(start) = comment.find("KeyID:") {
            let rest = &comment[start + 6..];
            if let Some(end) = rest.find(',') {
                key_id = Some(rest[..end].trim().to_string());
            } else {
                key_id = Some(rest.trim().to_string());
            }
        }

        // Parse Expires: value
        if let Some(start) = comment.find("Expires:") {
            let rest = &comment[start + 8..];
            let timestamp_str = if let Some(end) = rest.find(',') {
                &rest[..end]
            } else {
                rest
            }
            .trim();

            if let Ok(dt) = DateTime::parse_from_rfc3339(timestamp_str) {
                expires_at = Some(dt.with_timezone(&Utc));
            } else {
                warn!("Failed to parse expiration timestamp: {}", timestamp_str);
            }
        }

        (key_id, expires_at)
    }

    /// Find expired entries
    pub fn find_expired(entries: &[AuthorizedKeysEntry]) -> Vec<&AuthorizedKeysEntry> {
        let now = Utc::now();
        entries
            .iter()
            .filter(|e| {
                if let Some(expires) = e.expires_at {
                    expires <= now
                } else {
                    false
                }
            })
            .collect()
    }

    /// Remove entries by key ID
    pub fn remove_by_key_ids(content: &str, key_ids: &[String]) -> String {
        content
            .lines()
            .filter(|line| {
                // Keep empty lines and comments
                if line.trim().is_empty() || line.trim().starts_with('#') {
                    return true;
                }

                // Check if line contains any of the key IDs
                for key_id in key_ids {
                    if line.contains(&format!("KeyID: {}", key_id)) {
                        debug!("Removing line with KeyID: {}", key_id);
                        return false;
                    }
                }

                true
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Remove expired entries
    pub fn remove_expired(content: &str) -> (String, usize) {
        let entries = Self::parse_content(content);
        let expired = Self::find_expired(&entries);
        let expired_count = expired.len();

        if expired_count == 0 {
            return (content.to_string(), 0);
        }

        let expired_ids: Vec<String> = expired.iter().filter_map(|e| e.key_id.clone()).collect();

        let cleaned = Self::remove_by_key_ids(content, &expired_ids);
        (cleaned, expired_count)
    }

    /// Format authorized_keys entry
    pub fn format_entry(public_key: &str, key_id: &str, expires_at: &DateTime<Utc>) -> String {
        format!(
            "{} # KeyID: {}, Expires: {}",
            public_key,
            key_id,
            expires_at.to_rfc3339()
        )
    }

    /// Validate public key format
    pub fn validate_public_key(public_key: &str) -> bool {
        let parts: Vec<&str> = public_key.split_whitespace().collect();

        // Minimum: key-type base64-data
        if parts.len() < 2 {
            return false;
        }

        // Check key type
        let valid_types = [
            "ssh-rsa",
            "ssh-dss",
            "ssh-ed25519",
            "ecdsa-sha2-nistp256",
            "ecdsa-sha2-nistp384",
            "ecdsa-sha2-nistp521",
        ];
        if !valid_types.contains(&parts[0]) {
            return false;
        }

        // Validate base64 data (basic check)
        let data = parts[1];
        !data.is_empty()
            && data
                .chars()
                .all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=')
    }
}

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

    #[test]
    fn test_parse_line_with_metadata() {
        let line = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest # KeyID: abc-123, Expires: \
                    2024-12-31T23:59:59Z";
        let entry = AuthorizedKeysManager::parse_line(line);

        assert_eq!(entry.key_id, Some("abc-123".to_string()));
        assert!(entry.expires_at.is_some());
        assert!(entry.public_key.starts_with("ssh-ed25519"));
    }

    #[test]
    fn test_parse_line_without_metadata() {
        let line = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest user@host";
        let entry = AuthorizedKeysManager::parse_line(line);

        assert!(entry.key_id.is_none());
        assert!(entry.expires_at.is_none());
    }

    #[test]
    fn test_extract_metadata() {
        let line = "ssh-ed25519 AAAA # KeyID: test-123, Expires: 2024-01-01T00:00:00Z";
        let (key_id, expires) = AuthorizedKeysManager::extract_metadata(line);

        assert_eq!(key_id, Some("test-123".to_string()));
        assert!(expires.is_some());
    }

    #[test]
    fn test_remove_by_key_ids() {
        let content = "ssh-ed25519 AAA1 # KeyID: key1, Expires: 2024-01-01T00:00:00Z\nssh-ed25519 \
                       AAA2 # KeyID: key2, Expires: 2024-01-01T00:00:00Z\nssh-ed25519 AAA3 \
                       user@host\n";

        let result = AuthorizedKeysManager::remove_by_key_ids(content, &["key1".to_string()]);

        assert!(!result.contains("key1"));
        assert!(result.contains("key2"));
        assert!(result.contains("AAA3"));
    }

    #[test]
    fn test_validate_public_key() {
        assert!(AuthorizedKeysManager::validate_public_key(
            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest"
        ));

        assert!(AuthorizedKeysManager::validate_public_key(
            "ssh-rsa AAAAB3NzaC1yc2EAAAADAQ user@host"
        ));

        assert!(!AuthorizedKeysManager::validate_public_key("invalid"));
        assert!(!AuthorizedKeysManager::validate_public_key("ssh-ed25519"));
    }

    #[test]
    fn test_format_entry() {
        let expires = Utc::now();
        let entry = AuthorizedKeysManager::format_entry("ssh-ed25519 AAAA", "test-key", &expires);

        assert!(entry.contains("ssh-ed25519"));
        assert!(entry.contains("KeyID: test-key"));
        assert!(entry.contains("Expires:"));
    }

    #[test]
    fn test_parse_content() {
        let content =
            "# Comment line\nssh-ed25519 AAA1 # KeyID: key1\n\nssh-ed25519 AAA2 user@host\n";

        let entries = AuthorizedKeysManager::parse_content(content);

        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].key_id, Some("key1".to_string()));
        assert!(entries[1].key_id.is_none());
    }
}
