//! Password request parsing and watching.

use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;

use ini::Ini;
use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify, InotifyEvent};
use nix::unistd::Pid;

use crate::parse::{parse_pid, parse_systemd_bool};

/// The prefix for password request files.
const ASK_FILE_PREFIX: &str = "ask.";

/// Represents a parsed password request from systemd.
#[derive(Debug)]
#[allow(dead_code)] // Fields are used in Debug output and may be used in future
pub struct PasswordRequest {
    /// The PID of the process requesting the password (must be > 0).
    pub pid: Pid,
    /// The Unix socket path to send the response to.
    pub socket: PathBuf,
    /// Whether cached passwords are acceptable.
    pub accept_cached: bool,
    /// Whether input should be echoed (not relevant for non-interactive agent).
    pub echo: bool,
    /// Expiry timestamp from `CLOCK_MONOTONIC` in microseconds.
    /// A value of 0 means no expiry.
    pub not_after: u64,
    /// Whether the input should be silent.
    pub silent: bool,
    /// The message/prompt to display to the user.
    pub message: String,
    /// Icon name following XDG icon naming spec.
    pub icon: String,
    /// Optional identifier for the request (e.g., "cryptsetup:/dev/...").
    /// This field is not part of the core spec and may not always be present.
    pub id: Option<String>,
}

impl PasswordRequest {
    /// Parse a password request from an ask file.
    pub fn from_file(path: &Path) -> Result<Self, RequestError> {
        let ini = Ini::load_from_file(path).map_err(|e| RequestError::Parse(e.to_string()))?;

        let section = ini
            .section(Some("Ask"))
            .ok_or_else(|| RequestError::Parse("Missing [Ask] section".to_string()))?;

        let pid = section
            .get("PID")
            .ok_or_else(|| RequestError::Parse("Missing PID field".to_string()))?;
        let pid = parse_pid(pid).map_err(RequestError::Parse)?;

        let socket = section
            .get("Socket")
            .ok_or_else(|| RequestError::Parse("Missing Socket field".to_string()))?;
        let socket = PathBuf::from(socket);

        // Parse booleans using systemd's permissive rules (defaults to false if missing or invalid)
        let accept_cached = section
            .get("AcceptCached")
            .and_then(parse_systemd_bool)
            .unwrap_or(false);

        let echo = section
            .get("Echo")
            .and_then(parse_systemd_bool)
            .unwrap_or(false);

        // NotAfter is a u64 timestamp from CLOCK_MONOTONIC in microseconds.
        // A value of 0 (or missing/invalid) means no expiry.
        let not_after = section
            .get("NotAfter")
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(0);

        let silent = section
            .get("Silent")
            .and_then(parse_systemd_bool)
            .unwrap_or(false);

        let message = section.get("Message").unwrap_or("").to_string();

        let icon = section.get("Icon").unwrap_or("").to_string();

        let id = section.get("Id").map(|s| s.to_string());

        Ok(PasswordRequest {
            pid,
            socket,
            accept_cached,
            echo,
            not_after,
            silent,
            message,
            icon,
            id,
        })
    }
}

/// Error type for password request operations.
#[derive(Debug)]
pub enum RequestError {
    /// Error parsing the ask file.
    Parse(String),
    /// I/O error.
    Io(io::Error),
    /// Nix/system error.
    Nix(nix::Error),
}

impl std::fmt::Display for RequestError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RequestError::Parse(msg) => write!(f, "Parse error: {}", msg),
            RequestError::Io(e) => write!(f, "IO error: {}", e),
            RequestError::Nix(e) => write!(f, "System error: {}", e),
        }
    }
}

impl std::error::Error for RequestError {}

impl From<io::Error> for RequestError {
    fn from(e: io::Error) -> Self {
        RequestError::Io(e)
    }
}

impl From<nix::Error> for RequestError {
    fn from(e: nix::Error) -> Self {
        RequestError::Nix(e)
    }
}

/// An iterator that watches for password requests in a directory.
///
/// This iterator yields `PasswordRequest` items as ask files appear in the
/// watched directory. It first yields any existing ask files, then watches
/// for new ones via inotify.
pub struct PasswordRequestWatcher {
    inotify: Inotify,
    ask_dir: PathBuf,
    /// Buffered events from the last inotify read.
    pending_events: Vec<InotifyEvent>,
    /// Iterator over existing files (used during initial scan).
    initial_scan: Option<std::vec::IntoIter<PathBuf>>,
}

impl PasswordRequestWatcher {
    /// Create a new watcher for the given directory.
    ///
    /// The directory must exist. If it doesn't, the caller should wait for it
    /// to be created before calling this function.
    pub fn new(ask_dir: &Path) -> Result<Self, RequestError> {
        let inotify = Inotify::init(InitFlags::empty())?;

        // Watch for IN_CLOSE_WRITE and IN_MOVED_TO events
        let watch_flags = AddWatchFlags::IN_CLOSE_WRITE | AddWatchFlags::IN_MOVED_TO;
        inotify.add_watch(ask_dir, watch_flags)?;

        // Collect existing ask files for initial scan
        let existing_files: Vec<PathBuf> = fs::read_dir(ask_dir)?
            .filter_map(|entry| entry.ok())
            .map(|entry| entry.path())
            .filter(|path| {
                path.file_name()
                    .and_then(|n| n.to_str())
                    .is_some_and(|name| name.starts_with(ASK_FILE_PREFIX))
            })
            .collect();

        Ok(Self {
            inotify,
            ask_dir: ask_dir.to_path_buf(),
            pending_events: Vec::new(),
            initial_scan: Some(existing_files.into_iter()),
        })
    }

    /// Get the next ask file path from pending events or wait for new ones.
    fn next_ask_file_path(&mut self) -> Result<PathBuf, RequestError> {
        loop {
            // Check pending events first
            while let Some(event) = self.pending_events.pop() {
                if let Some(name) = event.name {
                    let name_str = name.to_string_lossy();
                    if name_str.starts_with(ASK_FILE_PREFIX) {
                        return Ok(self.ask_dir.join(&*name));
                    }
                }
            }

            // Wait for new events
            let events = self.inotify.read_events()?;
            self.pending_events = events;
        }
    }
}

impl Iterator for PasswordRequestWatcher {
    type Item = Result<PasswordRequest, RequestError>;

    fn next(&mut self) -> Option<Self::Item> {
        // First, drain the initial scan
        if let Some(ref mut scan) = self.initial_scan {
            if let Some(path) = scan.next() {
                return Some(PasswordRequest::from_file(&path));
            }
            // Initial scan complete
            self.initial_scan = None;
        }

        // Then watch for new files
        loop {
            let path = match self.next_ask_file_path() {
                Ok(path) => path,
                Err(e) => return Some(Err(e)),
            };

            // Small delay to ensure file is fully written
            std::thread::sleep(Duration::from_millis(50));

            // Skip if the file no longer exists (race condition)
            if !path.exists() {
                continue;
            }

            return Some(PasswordRequest::from_file(&path));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::TempDir;

    fn create_test_ask_file(dir: &Path, content: &str) -> PathBuf {
        create_test_ask_file_named(dir, "ask.test123", content)
    }

    fn create_test_ask_file_named(dir: &Path, name: &str, content: &str) -> PathBuf {
        let path = dir.join(name);
        let mut file = fs::File::create(&path).unwrap();
        file.write_all(content.as_bytes()).unwrap();
        path
    }

    #[test]
    fn test_parse_cryptsetup_request() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=473501
Socket=/run/systemd/ask-password/sck.981bc3e35412af01
AcceptCached=0
Echo=0
NotAfter=0
Silent=0
Message=Please enter passphrase for disk foobar:
Icon=drive-harddisk
Id=cryptsetup:/dev/disk/by-backingfile/home-richard-Projects-id_from_block_device-empty.img
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let request = PasswordRequest::from_file(&path).unwrap();

        assert_eq!(request.pid, Pid::from_raw(473501));
        assert_eq!(
            request.socket,
            PathBuf::from("/run/systemd/ask-password/sck.981bc3e35412af01")
        );
        assert!(!request.accept_cached);
        assert!(!request.echo);
        assert_eq!(request.not_after, 0);
        assert!(!request.silent);
        assert_eq!(request.message, "Please enter passphrase for disk foobar:");
        assert_eq!(request.icon, "drive-harddisk");
        assert!(request
            .id
            .as_ref()
            .is_some_and(|id| id.starts_with("cryptsetup:")));
    }

    #[test]
    fn test_missing_ask_section() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Other]
PID=12345
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let result = PasswordRequest::from_file(&path);

        assert!(result.is_err());
    }

    #[test]
    fn test_missing_required_fields() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
Message=No PID or Socket
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let result = PasswordRequest::from_file(&path);

        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_pid_zero() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=0
Socket=/tmp/test.sock
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let result = PasswordRequest::from_file(&path);

        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_pid_negative() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=-1
Socket=/tmp/test.sock
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let result = PasswordRequest::from_file(&path);

        assert!(result.is_err());
    }

    #[test]
    fn test_boolean_parsing_various_formats() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=1
Socket=/tmp/test.sock
AcceptCached=yes
Echo=TRUE
Silent=on
Id=cryptsetup:test
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let request = PasswordRequest::from_file(&path).unwrap();

        assert!(request.accept_cached);
        assert!(request.echo);
        assert!(request.silent);
    }

    #[test]
    fn test_boolean_parsing_false_formats() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=1
Socket=/tmp/test.sock
AcceptCached=no
Echo=FALSE
Silent=off
Id=cryptsetup:test
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let request = PasswordRequest::from_file(&path).unwrap();

        assert!(!request.accept_cached);
        assert!(!request.echo);
        assert!(!request.silent);
    }

    #[test]
    fn test_boolean_parsing_invalid_defaults_to_false() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=1
Socket=/tmp/test.sock
AcceptCached=maybe
Echo=2
Silent=yesplease
Id=cryptsetup:test
"#;

        let path = create_test_ask_file(temp_dir.path(), content);
        let request = PasswordRequest::from_file(&path).unwrap();

        // Invalid values should default to false
        assert!(!request.accept_cached);
        assert!(!request.echo);
        assert!(!request.silent);
    }

    #[test]
    fn test_watcher_initial_scan() {
        let temp_dir = TempDir::new().unwrap();

        // Create some ask files before creating the watcher
        create_test_ask_file_named(
            temp_dir.path(),
            "ask.test1",
            r#"[Ask]
PID=1
Socket=/tmp/test1.sock
Id=cryptsetup:device1
"#,
        );
        create_test_ask_file_named(
            temp_dir.path(),
            "ask.test2",
            r#"[Ask]
PID=2
Socket=/tmp/test2.sock
Id=cryptsetup:device2
"#,
        );
        // Also create a non-ask file that should be ignored
        create_test_ask_file_named(temp_dir.path(), "other.txt", "ignored");

        let mut watcher = PasswordRequestWatcher::new(temp_dir.path()).unwrap();

        // Should get both ask files from initial scan
        let req1 = watcher.next().unwrap().unwrap();
        let req2 = watcher.next().unwrap().unwrap();

        // Order may vary, so just check we got both
        let pids: Vec<_> = [req1.pid, req2.pid]
            .into_iter()
            .map(|p| p.as_raw())
            .collect();
        assert!(pids.contains(&1));
        assert!(pids.contains(&2));
    }
}
