#[cfg(target_os = "macos")]
use crossbeam_channel::Sender;
use std::cmp;
use std::io::Write;
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;

use utils::eventfd::{EventFd, EFD_NONBLOCK};
#[cfg(target_os = "macos")]
use utils::worker_message::WorkerMessage;
use virtio_bindings::{virtio_config::VIRTIO_F_VERSION_1, virtio_ring::VIRTIO_RING_F_EVENT_IDX};
use vm_memory::{ByteValued, GuestMemoryMmap};

use super::super::{
    ActivateError, ActivateResult, DeviceState, FsError, Queue as VirtQueue, VirtioDevice,
    VirtioShmRegion,
};
use super::passthrough;
use super::worker::FsWorker;
use super::ExportTable;
use super::{defs, defs::uapi};
use crate::virtio::InterruptTransport;

#[cfg(target_os = "windows")]
fn fs_device_debug_log(message: impl AsRef<str>) {
    static VALUE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
    if !*VALUE.get_or_init(|| {
        std::env::var("LIBKRUN_WINDOWS_VERBOSE_DEBUG")
            .map(|v| {
                matches!(
                    v.trim().to_ascii_lowercase().as_str(),
                    "1" | "true" | "yes" | "on"
                )
            })
            .unwrap_or(false)
    }) {
        return;
    }
    let message = message.as_ref();
    eprintln!("[VIRTIOFS-DEV] {message}");
    utils::windows_debug_log("virtiofs-device.log", message);
}

#[derive(Copy, Clone)]
#[repr(C, packed)]
struct VirtioFsConfig {
    tag: [u8; 36],
    num_request_queues: u32,
}

impl Default for VirtioFsConfig {
    fn default() -> Self {
        VirtioFsConfig {
            tag: [0; 36],
            num_request_queues: 0,
        }
    }
}

unsafe impl ByteValued for VirtioFsConfig {}

pub struct Fs {
    queues: Vec<VirtQueue>,
    queue_events: Vec<EventFd>,
    avail_features: u64,
    acked_features: u64,
    device_state: DeviceState,
    config: VirtioFsConfig,
    shm_region: Option<VirtioShmRegion>,
    passthrough_cfg: passthrough::Config,
    // The live FUSE filesystem, shared with the worker thread (via the Server) so
    // this device can snapshot/restore its inode map for snapshot-fork. `Some` once
    // activated.
    passthrough_fs: Option<Arc<passthrough::PassthroughFs>>,
    worker_thread: Option<JoinHandle<()>>,
    worker_stopfd: EventFd,
    exit_code: Arc<AtomicI32>,
    #[cfg(target_os = "macos")]
    map_sender: Option<Sender<WorkerMessage>>,
}

impl Fs {
    pub(crate) fn with_queues(
        fs_id: String,
        shared_dir: String,
        exit_code: Arc<AtomicI32>,
        queues: Vec<VirtQueue>,
    ) -> super::Result<Fs> {
        #[cfg(target_os = "windows")]
        fs_device_debug_log(format!(
            "Fs::with_queues fs_id={} shared_dir={}",
            fs_id, shared_dir
        ));
        let mut queue_events = Vec::new();
        for _ in 0..queues.len() {
            queue_events
                .push(EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(FsError::EventFd)?);
        }

        let avail_features = (1u64 << VIRTIO_F_VERSION_1) | (1u64 << VIRTIO_RING_F_EVENT_IDX);

        let tag = fs_id.into_bytes();
        let mut config = VirtioFsConfig::default();
        config.tag[..tag.len()].copy_from_slice(tag.as_slice());
        config.num_request_queues = 1;

        let fs_cfg = passthrough::Config {
            root_dir: shared_dir,
            ..Default::default()
        };

        Ok(Fs {
            queues,
            queue_events,
            avail_features,
            acked_features: 0,
            device_state: DeviceState::Inactive,
            config,
            shm_region: None,
            passthrough_cfg: fs_cfg,
            passthrough_fs: None,
            worker_thread: None,
            worker_stopfd: EventFd::new(EFD_NONBLOCK).map_err(FsError::EventFd)?,
            exit_code,
            #[cfg(target_os = "macos")]
            map_sender: None,
        })
    }

    pub fn new(fs_id: String, shared_dir: String, exit_code: Arc<AtomicI32>) -> super::Result<Fs> {
        let queues: Vec<VirtQueue> = defs::QUEUE_SIZES
            .iter()
            .map(|&max_size| VirtQueue::new(max_size))
            .collect();
        Self::with_queues(fs_id, shared_dir, exit_code, queues)
    }

    pub fn id(&self) -> &str {
        defs::FS_DEV_ID
    }

    pub fn set_shm_region(&mut self, shm_region: VirtioShmRegion) {
        self.shm_region = Some(shm_region);
    }

    pub fn set_export_table(&mut self, export_table: ExportTable) -> u64 {
        static FS_UNIQUE_ID: AtomicU64 = AtomicU64::new(0);

        self.passthrough_cfg.export_fsid = FS_UNIQUE_ID.fetch_add(1, Ordering::Relaxed);
        self.passthrough_cfg.export_table = Some(export_table);

        self.passthrough_cfg.export_fsid
    }

    #[cfg(target_os = "macos")]
    pub fn set_no_fsync(&mut self, no_fsync: bool) {
        self.passthrough_cfg.no_fsync = no_fsync;
    }

    #[cfg(target_os = "macos")]
    pub fn set_map_sender(&mut self, map_sender: Sender<WorkerMessage>) {
        self.map_sender = Some(map_sender);
    }
}

impl VirtioDevice for Fs {
    fn avail_features(&self) -> u64 {
        self.avail_features
    }

    fn acked_features(&self) -> u64 {
        self.acked_features
    }

    fn set_acked_features(&mut self, acked_features: u64) {
        self.acked_features = acked_features
    }

    fn device_type(&self) -> u32 {
        uapi::VIRTIO_ID_FS
    }

    fn device_name(&self) -> &str {
        "fs"
    }

    fn queues(&self) -> &[VirtQueue] {
        &self.queues
    }

    fn queues_mut(&mut self) -> &mut [VirtQueue] {
        &mut self.queues
    }

    fn queue_events(&self) -> &[EventFd] {
        &self.queue_events
    }

    fn save_device_state(&self) -> Option<Vec<u8>> {
        self.passthrough_fs.as_ref().map(|fs| fs.save_fs_state())
    }

    fn restore_device_state(&mut self, blob: &[u8]) {
        if let Some(fs) = self.passthrough_fs.as_ref() {
            fs.restore_fs_state(blob);
        }
    }

    fn read_config(&self, offset: u64, mut data: &mut [u8]) {
        let config_slice = self.config.as_slice();
        let config_len = config_slice.len() as u64;
        if offset >= config_len {
            error!("Failed to read config space");
            return;
        }
        if let Some(end) = offset.checked_add(data.len() as u64) {
            // This write can't fail, offset and end are checked against config_len.
            data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize])
                .unwrap();
        }
    }

    fn write_config(&mut self, offset: u64, data: &[u8]) {
        warn!(
            "fs: guest driver attempted to write device config (offset={:x}, len={:x})",
            offset,
            data.len()
        );
    }

    fn activate(&mut self, mem: GuestMemoryMmap, interrupt: InterruptTransport) -> ActivateResult {
        #[cfg(target_os = "windows")]
        fs_device_debug_log(format!(
            "Fs::activate tag={}",
            String::from_utf8_lossy(&self.config.tag)
        ));
        if self.worker_thread.is_some() {
            panic!("virtio_fs: worker thread already exists");
        }

        let event_idx: bool = (self.acked_features & (1 << VIRTIO_RING_F_EVENT_IDX)) != 0;
        self.queues[defs::HPQ_INDEX].set_event_idx(event_idx);
        self.queues[defs::REQ_INDEX].set_event_idx(event_idx);

        let queue_evts = self
            .queue_events
            .iter()
            .map(|e| e.try_clone().unwrap())
            .collect();
        // Build the FUSE filesystem and keep a shared handle so this device can
        // snapshot/restore its inode map (the worker thread otherwise owns it).
        let passthrough_fs = Arc::new(
            passthrough::PassthroughFs::new(self.passthrough_cfg.clone())
                .map_err(|_| ActivateError::BadActivate)?,
        );
        self.passthrough_fs = Some(passthrough_fs.clone());

        let worker = FsWorker::new(
            self.queues.clone(),
            queue_evts,
            interrupt.clone(),
            mem.clone(),
            self.shm_region.clone(),
            passthrough_fs,
            self.worker_stopfd.try_clone().unwrap(),
            self.exit_code.clone(),
            #[cfg(target_os = "macos")]
            self.map_sender.clone(),
        );
        self.worker_thread = Some(worker.run());

        self.device_state = DeviceState::Activated(mem, interrupt);
        Ok(())
    }

    fn is_activated(&self) -> bool {
        self.device_state.is_activated()
    }

    fn shm_region(&self) -> Option<&VirtioShmRegion> {
        self.shm_region.as_ref()
    }

    fn reset(&mut self) -> bool {
        if let Some(worker) = self.worker_thread.take() {
            let _ = self.worker_stopfd.write(1);
            if let Err(e) = worker.join() {
                error!("error waiting for worker thread: {e:?}");
            }
        }
        self.device_state = DeviceState::Inactive;
        true
    }
}
