use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use argh::FromArgs;
use base::error;
use base::Event;
use base::RawDescriptor;
use base::Terminal;
use cros_async::Executor;
use hypervisor::ProtectionType;
use snapshot::AnySnapshot;
use vm_memory::GuestMemory;
use vmm_vhost::message::VhostUserProtocolFeatures;
use vmm_vhost::VHOST_USER_F_PROTOCOL_FEATURES;
use crate::virtio::console::device::ConsoleDevice;
use crate::virtio::console::device::ConsoleSnapshot;
use crate::virtio::console::port::ConsolePort;
use crate::virtio::vhost_user_backend::handler::DeviceRequestHandler;
use crate::virtio::vhost_user_backend::handler::VhostUserDevice;
use crate::virtio::vhost_user_backend::BackendConnection;
use crate::virtio::vhost_user_backend::VhostUserDeviceBuilder;
use crate::virtio::Queue;
use crate::SerialHardware;
use crate::SerialParameters;
use crate::SerialType;
pub struct VhostUserConsoleDevice {
    console: ConsoleDevice,
    raw_stdin: bool,
}
impl Drop for VhostUserConsoleDevice {
    fn drop(&mut self) {
        if self.raw_stdin {
            match std::io::stdin().set_canon_mode() {
                Ok(()) => (),
                Err(e) => error!("failed to restore canonical mode for terminal: {:#}", e),
            }
        }
    }
}
impl VhostUserDeviceBuilder for VhostUserConsoleDevice {
    fn build(mut self: Box<Self>, _ex: &Executor) -> anyhow::Result<Box<dyn vmm_vhost::Backend>> {
        if self.raw_stdin {
            std::io::stdin()
                .set_raw_mode()
                .context("failed to set terminal in raw mode")?;
        }
        self.console.start_input_threads();
        let backend = ConsoleBackend { device: *self };
        let handler = DeviceRequestHandler::new(backend);
        Ok(Box::new(handler))
    }
}
struct ConsoleBackend {
    device: VhostUserConsoleDevice,
}
impl VhostUserDevice for ConsoleBackend {
    fn max_queue_num(&self) -> usize {
        self.device.console.max_queues()
    }
    fn features(&self) -> u64 {
        self.device.console.features() | 1 << VHOST_USER_F_PROTOCOL_FEATURES
    }
    fn protocol_features(&self) -> VhostUserProtocolFeatures {
        VhostUserProtocolFeatures::CONFIG
            | VhostUserProtocolFeatures::MQ
            | VhostUserProtocolFeatures::DEVICE_STATE
    }
    fn read_config(&self, offset: u64, data: &mut [u8]) {
        self.device.console.read_config(offset, data);
    }
    fn reset(&mut self) {
        if let Err(e) = self.device.console.reset() {
            error!("console reset failed: {:#}", e);
        }
    }
    fn start_queue(&mut self, idx: usize, queue: Queue, _mem: GuestMemory) -> anyhow::Result<()> {
        self.device.console.start_queue(idx, queue)
    }
    fn stop_queue(&mut self, idx: usize) -> anyhow::Result<Queue> {
        match self.device.console.stop_queue(idx) {
            Ok(Some(queue)) => Ok(queue),
            Ok(None) => Err(anyhow!("queue {idx} not started")),
            Err(e) => Err(e).with_context(|| format!("failed to stop queue {idx}")),
        }
    }
    fn enter_suspended_state(&mut self) -> anyhow::Result<()> {
        Ok(())
    }
    fn snapshot(&mut self) -> anyhow::Result<AnySnapshot> {
        let snap = self.device.console.snapshot()?;
        AnySnapshot::to_any(snap).context("failed to snapshot vhost-user console")
    }
    fn restore(&mut self, data: AnySnapshot) -> anyhow::Result<()> {
        let snap: ConsoleSnapshot =
            AnySnapshot::from_any(data).context("failed to deserialize vhost-user console")?;
        self.device.console.restore(&snap)
    }
}
#[derive(FromArgs)]
#[argh(subcommand, name = "console")]
pub struct Options {
    #[argh(option, arg_name = "PATH", hidden_help)]
    socket: Option<String>,
    #[argh(option, arg_name = "PATH")]
    socket_path: Option<String>,
    #[argh(option, arg_name = "FD")]
    fd: Option<RawDescriptor>,
    #[argh(option, arg_name = "OUTFILE")]
    output_file: Option<PathBuf>,
    #[argh(option, arg_name = "INFILE")]
    input_file: Option<PathBuf>,
    #[argh(switch)]
    syslog: bool,
    #[argh(option, arg_name = "type=TYPE,[path=PATH,input=PATH,console]")]
    port: Vec<SerialParameters>,
}
fn create_vu_multi_port_device(
    params: &[SerialParameters],
    keep_rds: &mut Vec<RawDescriptor>,
) -> anyhow::Result<VhostUserConsoleDevice> {
    let ports = params
        .iter()
        .map(|x| {
            let port = x
                .create_serial_device::<ConsolePort>(
                    ProtectionType::Unprotected,
                    &Event::new()?,
                    keep_rds,
                )
                .expect("failed to create multiport console");
            Ok(port)
        })
        .collect::<anyhow::Result<Vec<_>>>()?;
    let device = ConsoleDevice::new_multi_port(ProtectionType::Unprotected, ports);
    Ok(VhostUserConsoleDevice {
        console: device,
        raw_stdin: false, })
}
fn run_multi_port_device(opts: Options) -> anyhow::Result<()> {
    if opts.port.is_empty() {
        bail!("console: must have at least one `--port`");
    }
    let device = Box::new(create_vu_multi_port_device(&opts.port, &mut Vec::new())?);
    let ex = Executor::new().context("Failed to create executor")?;
    let conn =
        BackendConnection::from_opts(opts.socket.as_deref(), opts.socket_path.as_deref(), opts.fd)?;
    conn.run_device(ex, device)
}
pub fn create_vu_console_device(
    params: &SerialParameters,
    keep_rds: &mut Vec<RawDescriptor>,
) -> anyhow::Result<VhostUserConsoleDevice> {
    let device = params.create_serial_device::<ConsoleDevice>(
        ProtectionType::Unprotected,
        &Event::new()?,
        keep_rds,
    )?;
    Ok(VhostUserConsoleDevice {
        console: device,
        raw_stdin: params.stdin,
    })
}
pub fn run_console_device(opts: Options) -> anyhow::Result<()> {
    if !opts.port.is_empty() {
        return run_multi_port_device(opts);
    }
    let type_ = match opts.output_file {
        Some(_) => {
            if opts.syslog {
                bail!("--output-file and --syslog options cannot be used together.");
            }
            SerialType::File
        }
        None => {
            if opts.syslog {
                SerialType::Syslog
            } else {
                SerialType::Stdout
            }
        }
    };
    let params = SerialParameters {
        type_,
        hardware: SerialHardware::VirtioConsole,
        path: opts.output_file,
        input: opts.input_file,
        num: 1,
        console: true,
        earlycon: false,
        stdin: !opts.syslog,
        out_timestamp: false,
        ..Default::default()
    };
    let device = Box::new(create_vu_console_device(¶ms, &mut Vec::new())?);
    let ex = Executor::new().context("Failed to create executor")?;
    let conn =
        BackendConnection::from_opts(opts.socket.as_deref(), opts.socket_path.as_deref(), opts.fd)?;
    conn.run_device(ex, device)
}