use std::ffi::CString;
use std::fmt;
use std::io::{self, Error, ErrorKind};
use std::ptr;
use std::time::{SystemTime, UNIX_EPOCH};

/// A POSIX shared memory region for efficient terminal graphics transfer.
///
/// Wraps shm_open/mmap operations to create writeable memory regions that
/// can be shared with the terminal emulator via a filesystem path.
///
/// # Terminology
///
/// This module uses "path" to refer to POSIX shared memory names (e.g., `/kgfx_12345`).
/// While POSIX documentation calls these "names", "path" is used here for clarity
/// since they function like filesystem paths.
pub struct MemoryRegion {
    path: String,
    ptr: *mut u8,
    size: usize,
    fd: libc::c_int,
}

// SAFETY: The memory region has exclusive ownership of its mapped memory.
// After closing the FD, only unlink is called on drop, which doesn't access mapped memory.
unsafe impl Send for MemoryRegion {}
unsafe impl Sync for MemoryRegion {}

impl MemoryRegion {
    /// Creates a memory region with an explicit path.
    ///
    /// The path must be a valid POSIX shared memory name (typically starting with `/`).
    /// Any existing region at that path will be unlinked first.
    pub fn create(path: &str, size: usize) -> io::Result<Self> {
        Self::create_inner(path, size, true, false)
    }

    /// Creates a memory region with a pattern-based name.
    ///
    /// The pattern should contain a `*` placeholder that will be replaced with
    /// an 8-character hex suffix derived from system time and process ID.
    /// The resulting path will start with `/` (POSIX requirement).
    ///
    /// Retries up to 100 times if the generated name already exists.
    /// Existing regions are not unlinked; collisions will retry with a new name.
    ///
    /// # Name Generation
    ///
    /// The suffix is generated by XORing nanosecond timestamp with process ID,
    /// then taking the lower 32 bits as hex. This provides sufficient uniqueness
    /// for typical usage. If collisions become an issue in rapid-succession
    /// scenarios, consider using explicit paths with application-managed counters.
    pub fn create_with_pattern(pattern: &str, size: usize) -> io::Result<Self> {
        for _ in 0..100 {
            let path = generate_path(pattern);
            match Self::create_inner(&path, size, false, true) {
                Ok(region) => return Ok(region),
                Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
                Err(e) => return Err(e),
            }
        }

        Err(Error::new(
            ErrorKind::AlreadyExists,
            "failed to generate unique name after 100 attempts",
        ))
    }

    /// Writes data to the memory region starting at offset 0.
    ///
    /// Returns an error if the data exceeds the region size.
    pub fn write(&mut self, data: &[u8]) -> io::Result<()> {
        if data.len() > self.size {
            return Err(Error::new(
                ErrorKind::InvalidInput,
                format!(
                    "data too large: {} bytes exceeds region size of {} bytes",
                    data.len(),
                    self.size
                ),
            ));
        }

        unsafe {
            ptr::copy_nonoverlapping(data.as_ptr(), self.ptr, data.len());
        }

        Ok(())
    }

    /// Closes the file descriptor while keeping the memory mapping valid.
    ///
    /// This allows the terminal to continue reading from the shared memory
    /// after we've finished writing to it.
    pub fn close_fd(&mut self) {
        if self.fd >= 0 {
            unsafe {
                libc::close(self.fd);
            }
            self.fd = -1;
        }
    }

    /// Unlinks the shared memory path, removing it from the filesystem.
    ///
    /// After unlinking, no new processes can open this region, but existing
    /// mappings remain valid until unmapped.
    pub fn unlink(&self) -> io::Result<()> {
        let c_path = CString::new(self.path.as_str())
            .map_err(|_| Error::new(ErrorKind::InvalidInput, "path contains null byte"))?;

        if unsafe { libc::shm_unlink(c_path.as_ptr()) } < 0 {
            return Err(Error::last_os_error());
        }

        Ok(())
    }

    /// Returns the shared memory path.
    pub fn path(&self) -> &str {
        &self.path
    }

    /// Returns the size of the memory region in bytes.
    pub fn size(&self) -> usize {
        self.size
    }

    /// Returns a mutable slice of the mapped memory.
    ///
    /// # Safety
    /// The caller must ensure no concurrent writes occur.
    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
    }

    fn create_inner(
        path: &str,
        size: usize,
        unlink_existing: bool,
        exclusive: bool,
    ) -> io::Result<Self> {
        let c_path = CString::new(path)
            .map_err(|_| Error::new(ErrorKind::InvalidInput, "path contains null byte"))?;

        if unlink_existing {
            unsafe {
                libc::shm_unlink(c_path.as_ptr());
            }
        }

        let mut flags = libc::O_RDWR | libc::O_CREAT;
        if exclusive {
            flags |= libc::O_EXCL;
        }

        // Open with read/write, create, owner read+write (0600)
        let fd = unsafe {
            libc::shm_open(
                c_path.as_ptr(),
                flags,
                (libc::S_IRUSR | libc::S_IWUSR) as libc::c_uint,
            )
        };

        if fd < 0 {
            return Err(Error::last_os_error());
        }

        // Set the size
        if unsafe { libc::ftruncate(fd, size as libc::off_t) } < 0 {
            let err = Error::last_os_error();
            unsafe {
                libc::close(fd);
                libc::shm_unlink(c_path.as_ptr());
            }
            return Err(err);
        }

        // Memory-map the region
        let ptr = unsafe {
            libc::mmap(
                ptr::null_mut(),
                size,
                libc::PROT_READ | libc::PROT_WRITE,
                libc::MAP_SHARED,
                fd,
                0,
            )
        };

        if ptr == libc::MAP_FAILED {
            let err = Error::last_os_error();
            unsafe {
                libc::close(fd);
                libc::shm_unlink(c_path.as_ptr());
            }
            return Err(err);
        }

        Ok(Self {
            path: path.to_string(),
            ptr: ptr as *mut u8,
            size,
            fd,
        })
    }
}

impl Drop for MemoryRegion {
    fn drop(&mut self) {
        // Unmap memory if pointer is valid
        if !self.ptr.is_null() {
            unsafe {
                libc::munmap(self.ptr as *mut libc::c_void, self.size);
            }
            self.ptr = ptr::null_mut();
        }

        // Close file descriptor if open
        if self.fd >= 0 {
            unsafe {
                libc::close(self.fd);
            }
            self.fd = -1;
        }

        // Intentionally do NOT unlink - the terminal needs to read the data
    }
}

impl fmt::Debug for MemoryRegion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("MemoryRegion")
            .field("path", &self.path)
            .field("size", &self.size)
            .field("fd", &self.fd)
            .finish_non_exhaustive()
    }
}

/// Generates a unique path from a pattern containing `*`.
///
/// Replaces `*` with an 8-character hex suffix derived from:
/// - Current system time in nanoseconds
/// - Process ID
/// - XOR of the two values
fn generate_path(pattern: &str) -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos() as u64)
        .unwrap_or(0);

    let pid = std::process::id() as u64;
    let unique = nanos ^ pid;
    let suffix = format!("{:08x}", unique & 0xFFFF_FFFF);

    let path = pattern.replace('*', &suffix);

    // Ensure path starts with /
    if path.starts_with('/') {
        path
    } else {
        format!("/{path}")
    }
}

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

    #[test]
    fn test_path_generation_format() {
        let path = generate_path("test-*");
        assert!(
            path.starts_with("/test-"),
            "path should start with /test-: {path}"
        );
        assert_eq!(path.len(), 14, "path length should be 14: {path}");

        // Check suffix is lowercase hex
        let suffix = &path[6..];
        assert!(
            suffix
                .chars()
                .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
        );
    }

    #[test]
    fn test_create_and_write() {
        let mut region = MemoryRegion::create_with_pattern("kgfx-test-*", 1024)
            .expect("failed to create region");

        let data = b"hello world";
        region.write(data).expect("failed to write");

        // Verify data was written
        let slice = region.as_mut_slice();
        assert_eq!(&slice[..data.len()], data);

        // Cleanup
        region.unlink().expect("failed to unlink");
    }

    #[test]
    fn test_write_overflow() {
        let mut region =
            MemoryRegion::create_with_pattern("kgfx-test-*", 10).expect("failed to create region");

        let data = [0u8; 20];
        let err = region.write(&data).expect_err("should fail");

        let msg = err.to_string();
        assert!(msg.contains("20"), "error should mention data size: {msg}");
        assert!(
            msg.contains("10"),
            "error should mention region size: {msg}"
        );

        region.unlink().expect("failed to unlink");
    }

    #[test]
    fn test_name_collision_handling() {
        // Create region A with explicit name
        let region_a =
            MemoryRegion::create("/kgfx_test_collision", 100).expect("failed to create region A");

        // Create region B with same name - should succeed (implementation unlinks first)
        let region_b =
            MemoryRegion::create("/kgfx_test_collision", 100).expect("failed to create region B");

        // Both exist but point to the same underlying path
        assert_eq!(region_a.path(), region_b.path());

        // Cleanup (only need to unlink once)
        region_b.unlink().expect("failed to unlink");
    }

    #[test]
    fn test_debug_format() {
        let region =
            MemoryRegion::create_with_pattern("kgfx-dbg-*", 1024).expect("failed to create region");

        let debug_str = format!("{region:?}");

        // Should contain path and size, but not raw pointer
        assert!(debug_str.contains("path:"));
        assert!(debug_str.contains("size:"));
        assert!(debug_str.contains("1024"));
        assert!(!debug_str.contains("ptr"));

        region.unlink().expect("failed to unlink");
    }
}
