use std::io;
use std::io::BufRead; // for .read_line()
use std::io::Write;
use std::os::unix::fs::chown;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use nix::{self};
use nix::unistd::{fork, ForkResult};
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::time::{SystemTime, UNIX_EPOCH};
use users::{get_current_uid, get_effective_uid};
use color_eyre::eyre::{self, Result};
use serde::{Deserialize, Serialize};
use serde_json;
use crate::utils;

// ======================================================================================
// Design Document: SUID Worker/Master Architecture for `epkg`
// ======================================================================================
//
// OVERVIEW:
//   The `epkg` package manager may fork a worker process to manage privileged operations
//   (e.g., unpacking packages to `/opt/epkg/store/`) if installed SUID.
//   The master process drops privileges and communicates with the worker process via IPC.
//
//   Key Features:
//   - Always fork a worker process for privileged operations.
//   - Drop privileges in the master process.
//   - Communicate via Unix domain sockets.
//
// ARCHITECTURE:
//
//   +---------------------+       +-----------------------+
//   | Master Process      |       | Worker Process        |
//   |---------------------|       |-----------------------|
//   | - Drops privileges  |       | - Runs as root        |
//   | - Handles user I/O  |       | - Manages store       |
//   | - Uses PrivilegedClient|<----->| - Handles PrivilegedCall|
//   |                     |  IPC  |   enum variants:      |
//   |                     |       |   unpack, gc, moves   |
//   +---------------------+       +-----------------------+
//
//   IPC Communication:
//   - Uses Unix domain sockets.
//   - JSON format for messages:
//     {
//       "command": "unpack" | "gc",
//       "params": { "files": ["file1.epkg", "file2.epkg"] }
//     }
//
// INTERNAL DATA STRUCTURES:
//
// 1. `PrivilegedClient`:
//    - Provides access to privileged operations via IPC.
//    - Always forks a worker process for privileged operations.
//    - Fields:
//      - `ipc_stream: Option<UnixStream>`: Connection to privileged worker process
//      - `socket_path: Option<String>`: Path to the Unix socket for IPC
//
// 2. `PrivilegedCall`:
//    - Enum representing all supported privileged operations.
//    - Serialized directly over IPC for type-safe communication.
//    - Represents commands sent from the master process to the worker process.
//
//    Variants:
//    - `Unpack(Vec<String>)`:
//      - Command to unpack a list of `.epkg` files.
//      - Contains the list of file paths to unpack.
//
//    - `GarbageCollect`:
//      - Command to perform garbage collection in the store.
//
// 3. `UnixStream`:
//    - Represents a connection to a Unix domain socket.
//    - Used for bidirectional communication between the master and worker processes.
//
// CALL FLOW:
//
// 1. Master Process:
//    - Always forks a worker process for privileged operations.
//    - Drops privileges.
//    - Connects to the worker via a Unix socket.
//
// 2. Worker Process:
//    - Binds to a Unix socket.
//    - Listens for commands from the master process.
//    - Executes privileged operations:
//      a. `unpack_packages(files)`: Unpack `.epkg` files to `/opt/epkg/store/`.
//      b. `garbage_collect()`: Clean up unused dirs in the store.
//
// 3. IPC Communication:
//    - Master sends `PrivilegedCall` enum instances as JSON.
//    - Worker responds with `PrivilegedResponse` enum instances.

static PRIVILEGED_CLIENT: LazyLock<Mutex<PrivilegedClient>> = LazyLock::new(|| {
    let mut client = PrivilegedClient::new();
    // Establish IPC connection at initialization time
    client.establish_ipc_connection()
        .expect("Failed to establish IPC connection for privileged client");
    Mutex::new(client)
});

#[allow(dead_code)]
pub fn privileged_client() -> std::sync::MutexGuard<'static, PrivilegedClient> {
    PRIVILEGED_CLIENT.lock().unwrap()
}

// Handler functions for privileged operations

fn handle_garbage_collect() -> PrivilegedResponse {
    PrivilegedResponse::Success {
        message: "Garbage collection completed".to_string(),
        data: None,
    }
}

fn handle_move_to_cache(temp_path: &str, final_path: &str) -> PrivilegedResponse {
    match atomic_move_file(temp_path, final_path) {
        Ok(msg) => PrivilegedResponse::Success {
            message: msg,
            data: None,
        },
        Err(e) => PrivilegedResponse::Error {
            message: format!("Failed to move file: {}", e),
        },
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub enum PrivilegedCall {
    GarbageCollect,
    MoveToDownloadsCache { temp_path: String, final_path: String },
}

#[derive(Serialize, Deserialize, Debug)]
pub enum PrivilegedResponse {
    Success { message: String, data: Option<String> },
    Error { message: String },
}

/// Client for seamless privileged operations
///
/// Provides a clean, minimal API for calling privileged functions with minimal boilerplate.
/// Features a generic `call()` method for any privileged operation.
/// Always forks a worker process for privileged operations.
///
/// The system uses unified handlers that minimize per-operation code duplication.
///
/// # Examples
///
/// ## Using the generic call method (for any privileged operation):
/// ```rust
/// let mut client = PrivilegedClient::new();
/// client.call(PrivilegedCall::GarbageCollect)?;
/// client.call(PrivilegedCall::MoveToDownloadsCache {
///     temp_path: "/tmp/user_channel".to_string(),
///     final_path: "/opt/epkg/cache/channels/final".to_string()
/// })?;
/// ```
///
/// ## Adding a new privileged operation:
/// 1. Add enum variant to `PrivilegedCall`
/// 2. Implement a handler function for the operation
/// 3. Add match arm in the `execute_privileged_call` function
/// That's it! Simple function-based implementation.
pub struct PrivilegedClient {
    ipc_stream: Option<UnixStream>,
    socket_path: Option<String>,
}

impl PrivilegedClient {
    pub fn new() -> Self {
        Self {
            ipc_stream: None,
            socket_path: None,
        }
    }

    /// Generic privileged call method - use this for any privileged operation
    ///
    /// This method eliminates the need for individual wrapper methods per operation.
    /// Simply pass the PrivilegedCall enum variant directly.
    #[allow(dead_code)]
    pub fn call(&mut self, call: PrivilegedCall) -> Result<String> {
        // Send PrivilegedCall enum directly as JSON
        let request = serde_json::to_string(&call)?;
        let response_str = self.send_privileged_request(&request)?;
        let response: PrivilegedResponse = serde_json::from_str(&response_str)?;

        match response {
            PrivilegedResponse::Success { message, .. } => Ok(message),
            PrivilegedResponse::Error { message } => Err(eyre::eyre!("Privileged operation failed: {}", message)),
        }
    }

    fn establish_ipc_connection(&mut self) -> Result<()> {
        if self.ipc_stream.is_some() {
            return Ok(());
        }

        // Create random socket path
        let socket_path = create_random_socket_path();
        let socket_path_str = socket_path.to_string_lossy().to_string();

        // Fork privileged worker
        match unsafe { fork() } {
            Ok(ForkResult::Parent { .. }) => {
                // Drop privileges in parent
                nix::unistd::setuid(nix::unistd::Uid::from_raw(nix::unistd::getuid().as_raw()))
                    .map_err(|e| eyre::eyre!("Failed to drop privileges: {}", e))?;

                // Wait for worker to create socket
                for _ in 0..10 {
                    if socket_path.exists() {
                        break;
                    }
                    std::thread::sleep(std::time::Duration::from_millis(50));
                }
                if !socket_path.exists() {
                    return Err(eyre::eyre!("Socket file creation timeout"));
                }

                // Connect to worker
                self.ipc_stream = Some(UnixStream::connect(&socket_path)?);
                self.socket_path = Some(socket_path_str);
            }
            Ok(ForkResult::Child) => {
                // Run privileged worker
                if let Err(e) = privilege_worker_main(&socket_path) {
                    eprintln!("Privileged worker error: {}", e);
                    std::process::exit(1);
                }
                std::process::exit(0);
            }
            Err(e) => return Err(eyre::eyre!("Fork failed: {}", e)),
        }

        Ok(())
    }

    #[allow(dead_code)]
    fn send_privileged_request(&mut self, request: &str) -> Result<String> {
        let stream = self.ipc_stream.as_mut()
            .ok_or_else(|| eyre::eyre!("IPC stream not initialized"))?;

        // Send request
        {
            let mut writer = io::BufWriter::new(&mut *stream);
            writer.write_all(request.as_bytes())?;
            writer.write_all(b"\n")?;
            writer.flush()?;
        } // writer is dropped here

        // Receive response
        let mut reader = io::BufReader::new(&mut *stream);
        let mut response_line = String::new();
        reader.read_line(&mut response_line)?;
        let response = response_line.trim();

        Ok(response.to_string())
    }
}

fn create_random_socket_path() -> PathBuf {
    let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64
        ^ (get_effective_uid() as u64)
        ^ (std::process::id() as u64);
    let mut rng = StdRng::seed_from_u64(seed);
    PathBuf::from(format!(
        "/tmp/epkg-{}-{:x}.sock",
        get_effective_uid(),
        rng.random::<u64>()
    ))
}

fn privilege_worker_main(socket_path: &Path) -> Result<()> {
    let listener = UnixListener::bind(socket_path)?;

    // Set permissions for master process
    utils::set_permissions_from_mode(socket_path, 0o700)?;
    chown(socket_path, Some(get_current_uid()), None)?;

    for stream in listener.incoming() {
        // The steam is automatically closed when it goes out of scope.
        match stream {
            Ok(mut stream) => handle_client(&mut stream)?,
            Err(e) => eprintln!("Connection error: {}", e),
        }
    }
    Ok(())
}

fn handle_client(stream: &mut UnixStream) -> Result<()> {
    let call = read_privileged_call(stream)?;
    let response = execute_privileged_call(call);
    send_privileged_response(stream, &response)
}

fn read_privileged_call(stream: &mut UnixStream) -> Result<PrivilegedCall> {
    let mut buf = String::new();
    io::BufReader::new(stream).read_line(&mut buf)?;
    let call: PrivilegedCall = serde_json::from_str(buf.trim())?;
    Ok(call)
}

fn execute_privileged_call(call: PrivilegedCall) -> PrivilegedResponse {
    match call {
        PrivilegedCall::GarbageCollect => handle_garbage_collect(),
        PrivilegedCall::MoveToDownloadsCache {
            temp_path,
            final_path,
        } => handle_move_to_cache(&temp_path, &final_path),
    }
}

fn send_privileged_response(stream: &mut UnixStream, response: &PrivilegedResponse) -> Result<()> {
    let response_json = serde_json::to_string(response)?;
    let mut writer = io::BufWriter::new(stream);
    writer.write_all(response_json.as_bytes())?;
    writer.write_all(b"\n")?;
    writer.flush()?;
    Ok(())
}

fn atomic_move_file(temp_path: &str, final_path: &str) -> Result<String> {
    use std::fs;
    let temp = PathBuf::from(temp_path);
    let final_path = PathBuf::from(final_path);

    // Ensure the destination directory exists
    if let Some(parent) = final_path.parent() {
        fs::create_dir_all(parent)?;
    }

    // Perform atomic move (rename on same filesystem)
    fs::rename(&temp, &final_path)?;

    Ok(format!("Successfully moved {} to {}",
               temp.display(), final_path.display()))
}

// send side