//! Cryptsetup-specific password request handling.

use std::io;
use std::os::unix::net::UnixDatagram;
use std::path::PathBuf;

use nix::errno::Errno;
use nix::time::{clock_gettime, ClockId};
use nix::unistd::Pid;

use crate::password_request::PasswordRequest;

/// The prefix used by cryptsetup in the Id field.
const CRYPTSETUP_ID_PREFIX: &str = "cryptsetup:";

/// A validated cryptsetup passphrase request, ready to be answered.
///
/// This type is created from a `PasswordRequest` via `TryFrom`, which validates
/// that the request is indeed a cryptsetup request. It contains only the fields
/// needed to respond to the request.
#[derive(Debug)]
pub struct CryptsetupPassphraseRequest {
    /// The PID of the process requesting the password.
    pid: Pid,
    /// The Unix socket path to send the response to.
    socket: PathBuf,
    /// Expiry timestamp from `CLOCK_MONOTONIC` in microseconds (0 = no expiry).
    not_after: u64,
    /// The device identifier (the part after "cryptsetup:" in the Id field).
    /// e.g., "/dev/disk/by-backingfile/home-richard-Projects-..."
    device: String,
}

/// Reason why a `PasswordRequest` could not be converted to a `CryptsetupPassphraseRequest`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NotCryptsetupError {
    /// The request has no Id field.
    MissingId,
    /// The Id field does not start with "cryptsetup:".
    WrongIdPrefix,
}

impl std::fmt::Display for NotCryptsetupError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NotCryptsetupError::MissingId => write!(f, "request has no Id field"),
            NotCryptsetupError::WrongIdPrefix => {
                write!(f, "Id field does not start with '{}'", CRYPTSETUP_ID_PREFIX)
            }
        }
    }
}

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

/// Error returned when sending a passphrase fails.
#[derive(Debug)]
pub enum SendPassphraseError {
    /// The requesting process is no longer alive.
    ProcessDead,
    /// The request has expired (NotAfter time has passed).
    Expired,
    /// I/O error sending the passphrase.
    Io(io::Error),
}

impl std::fmt::Display for SendPassphraseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SendPassphraseError::ProcessDead => {
                write!(f, "requesting process no longer exists")
            }
            SendPassphraseError::Expired => write!(f, "request has expired"),
            SendPassphraseError::Io(e) => write!(f, "I/O error: {}", e),
        }
    }
}

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

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

impl TryFrom<PasswordRequest> for CryptsetupPassphraseRequest {
    type Error = NotCryptsetupError;

    fn try_from(request: PasswordRequest) -> Result<Self, Self::Error> {
        let id = request.id.ok_or(NotCryptsetupError::MissingId)?;

        let device = id
            .strip_prefix(CRYPTSETUP_ID_PREFIX)
            .ok_or(NotCryptsetupError::WrongIdPrefix)?
            .to_string();

        Ok(CryptsetupPassphraseRequest {
            pid: request.pid,
            socket: request.socket,
            not_after: request.not_after,
            device,
        })
    }
}

impl CryptsetupPassphraseRequest {
    /// Returns the device identifier for this request.
    pub fn device(&self) -> &str {
        &self.device
    }

    /// Returns the PID of the requesting process.
    #[allow(dead_code)] // Used in tests and useful for logging
    pub fn pid(&self) -> Pid {
        self.pid
    }

    /// Check if the requesting process is still alive.
    fn is_process_alive(&self) -> bool {
        // Use kill with signal 0 to check if process exists
        match nix::sys::signal::kill(self.pid, None) {
            Ok(()) => true,
            Err(Errno::ESRCH) => false, // No such process
            Err(_) => true,             // Other errors (e.g., EPERM) mean process exists
        }
    }

    /// Check if the request has expired based on NotAfter.
    ///
    /// The `NotAfter` field is a `CLOCK_MONOTONIC` timestamp in microseconds.
    /// A value of 0 means no expiry is set.
    fn is_expired(&self) -> bool {
        if self.not_after == 0 {
            return false; // No expiry set
        }

        // Get current CLOCK_MONOTONIC time
        let ts = match clock_gettime(ClockId::CLOCK_MONOTONIC) {
            Ok(ts) => ts,
            Err(_) => return false, // If we can't get the time, assume not expired
        };

        // Convert TimeSpec to microseconds for comparison
        let now_usec = ts.tv_sec() as u64 * 1_000_000 + ts.tv_nsec() as u64 / 1_000;
        now_usec > self.not_after
    }

    /// Send a passphrase response for this request.
    ///
    /// This method validates that the requesting process is still alive and
    /// that the request hasn't expired before sending the passphrase.
    ///
    /// # Errors
    ///
    /// Returns `SendPassphraseError::ProcessDead` if the requesting process no longer exists.
    /// Returns `SendPassphraseError::Expired` if the request has expired.
    /// Returns `SendPassphraseError::Io` if there's an I/O error sending to the socket.
    pub fn send_passphrase(&self, passphrase: &str) -> Result<(), SendPassphraseError> {
        // Validate the request is still actionable
        if !self.is_process_alive() {
            return Err(SendPassphraseError::ProcessDead);
        }

        if self.is_expired() {
            return Err(SendPassphraseError::Expired);
        }

        // Send the passphrase
        let socket = UnixDatagram::unbound()?;

        // Prefix passphrase with '+' to indicate success
        let mut response = Vec::with_capacity(passphrase.len() + 1);
        response.push(b'+');
        response.extend_from_slice(passphrase.as_bytes());

        socket.send_to(&response, &self.socket)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::password_request::PasswordRequest;
    use std::fs;
    use std::io::Write;
    use std::time::Duration;
    use tempfile::TempDir;

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

    #[test]
    fn test_try_from_cryptsetup_request() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=473501
Socket=/run/systemd/ask-password/sck.981bc3e35412af01
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();
        let cryptsetup_req = CryptsetupPassphraseRequest::try_from(request).unwrap();

        assert_eq!(
            cryptsetup_req.device(),
            "/dev/disk/by-backingfile/home-richard-Projects-id_from_block_device-empty.img"
        );
        assert_eq!(cryptsetup_req.pid(), Pid::from_raw(473501));
    }

    #[test]
    fn test_non_cryptsetup_request() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=12345
Socket=/run/systemd/ask-password/sck.abc123
Message=Enter password for something else
Id=other:something
"#;

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

        let result = CryptsetupPassphraseRequest::try_from(request);
        assert_eq!(result.unwrap_err(), NotCryptsetupError::WrongIdPrefix);
    }

    #[test]
    fn test_missing_id_field() {
        let temp_dir = TempDir::new().unwrap();
        let content = r#"[Ask]
PID=12345
Socket=/run/systemd/ask-password/sck.abc123
Message=Enter password for something
"#;

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

        let result = CryptsetupPassphraseRequest::try_from(request);
        assert_eq!(result.unwrap_err(), NotCryptsetupError::MissingId);
    }

    #[test]
    fn test_send_passphrase_expired_request() {
        let temp_dir = TempDir::new().unwrap();
        // Use a timestamp in the past (1 microsecond)
        let content = r#"[Ask]
PID=1
Socket=/tmp/test.sock
NotAfter=1
Id=cryptsetup:test
"#;

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

        let result = cryptsetup_req.send_passphrase("test");
        assert!(matches!(result, Err(SendPassphraseError::Expired)));
    }

    #[test]
    fn test_send_passphrase_dead_process() {
        let temp_dir = TempDir::new().unwrap();
        // Use PID 1 which we can't signal (EPERM), so it counts as alive
        // Use a non-existent PID instead - PID 2^22 is unlikely to exist
        let content = r#"[Ask]
PID=4194304
Socket=/tmp/test.sock
NotAfter=0
Id=cryptsetup:test
"#;

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

        let result = cryptsetup_req.send_passphrase("test");
        assert!(matches!(result, Err(SendPassphraseError::ProcessDead)));
    }

    #[test]
    fn test_send_passphrase_success() {
        use std::os::unix::net::UnixDatagram;

        let temp_dir = TempDir::new().unwrap();
        let socket_path = temp_dir.path().join("test.sock");

        // Create a receiving socket
        let receiver = UnixDatagram::bind(&socket_path).unwrap();
        receiver
            .set_read_timeout(Some(Duration::from_secs(1)))
            .unwrap();

        // Create a CryptsetupPassphraseRequest with our test socket
        let pid = std::process::id();
        let content = format!(
            r#"[Ask]
PID={}
Socket={}
Id=cryptsetup:/dev/test
"#,
            pid,
            socket_path.display()
        );

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

        // Send the passphrase
        cryptsetup_req.send_passphrase("test_passphrase").unwrap();

        // Receive and verify
        let mut buf = [0u8; 256];
        let len = receiver.recv(&mut buf).unwrap();
        let received = &buf[..len];

        assert_eq!(received[0], b'+');
        assert_eq!(&received[1..], b"test_passphrase");
    }
}
