use super::windows_common::finish_driver_spawn;
use super::windows_common::normalize_windows_tty_input;
use crate::conpty::ConptyInstance;
use crate::conpty::spawn_conpty_process_as_user;
use crate::desktop::LaunchDesktop;
use crate::logging::log_failure;
use crate::logging::log_success;
use crate::process::StderrMode;
use crate::process::StdinMode;
use crate::process::read_handle_loop;
use crate::process::spawn_process_with_pipes;
use crate::spawn_prep::LegacyAclSids;
use crate::spawn_prep::SpawnPrepOptions;
use crate::spawn_prep::allow_null_device_for_workspace_write;
use crate::spawn_prep::apply_legacy_session_acl_rules;
use crate::spawn_prep::legacy_session_capability_roots;
use crate::spawn_prep::prepare_legacy_session_security;
use crate::spawn_prep::prepare_legacy_spawn_context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use std::collections::HashMap;
use std::path::Path;
use std::ptr;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::WriteFile;
use windows_sys::Win32::System::Console::COORD;
use windows_sys::Win32::System::Console::ResizePseudoConsole;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::INFINITE;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::TerminateProcess;
use windows_sys::Win32::System::Threading::WaitForSingleObject;

const WAIT_TIMEOUT: u32 = 0x0000_0102;

struct LegacyProcessHandles {
    process: PROCESS_INFORMATION,
    output_join: std::thread::JoinHandle<()>,
    writer_handle: tokio::task::JoinHandle<()>,
    hpc: Option<HANDLE>,
    conpty_owner: Option<ConptyInstance>,
    token_handle: HANDLE,
    desktop: Option<LaunchDesktop>,
}

#[allow(clippy::too_many_arguments)]
fn spawn_legacy_process(
    h_token: HANDLE,
    command: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    use_private_desktop: bool,
    tty: bool,
    stdin_open: bool,
    stdout_tx: broadcast::Sender<Vec<u8>>,
    stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
    writer_rx: mpsc::Receiver<Vec<u8>>,
    logs_base_dir: Option<&Path>,
) -> Result<LegacyProcessHandles> {
    let (pi, output_join, writer_handle, hpc, conpty_owner, desktop) = if tty {
        let (pi, mut conpty) = spawn_conpty_process_as_user(
            h_token,
            command,
            cwd,
            env_map,
            use_private_desktop,
            logs_base_dir,
        )?;
        let hpc = conpty.raw_handle();
        let output_join = spawn_output_reader(conpty.take_output_read(), stdout_tx);
        let writer_handle = spawn_input_writer(
            Some(conpty.take_input_write()),
            writer_rx,
            /*normalize_newlines*/ true,
        );
        (pi, output_join, writer_handle, hpc, Some(conpty), None)
    } else {
        let pipe_handles = spawn_process_with_pipes(
            h_token,
            command,
            cwd,
            env_map,
            if stdin_open {
                StdinMode::Open
            } else {
                StdinMode::Closed
            },
            StderrMode::Separate,
            use_private_desktop,
            logs_base_dir,
        )?;
        let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx);
        let Some(stderr_read) = pipe_handles.stderr_read else {
            anyhow::bail!("separate stderr handle should be present");
        };
        let Some(stderr_tx) = stderr_tx else {
            anyhow::bail!("separate stderr channel should be present");
        };
        let stderr_join = spawn_output_reader(stderr_read, stderr_tx);
        let output_join = std::thread::spawn(move || {
            let _ = stdout_join.join();
            let _ = stderr_join.join();
        });
        let writer_handle = spawn_input_writer(
            pipe_handles.stdin_write,
            writer_rx,
            /*normalize_newlines*/ false,
        );
        (
            pipe_handles.process,
            output_join,
            writer_handle,
            None,
            None,
            Some(pipe_handles.desktop),
        )
    };
    Ok(LegacyProcessHandles {
        process: pi,
        output_join,
        writer_handle,
        hpc,
        conpty_owner,
        token_handle: h_token,
        desktop,
    })
}

fn spawn_output_reader(
    output_read: HANDLE,
    output_tx: broadcast::Sender<Vec<u8>>,
) -> std::thread::JoinHandle<()> {
    read_handle_loop(output_read, move |chunk| {
        let _ = output_tx.send(chunk.to_vec());
    })
}

fn spawn_input_writer(
    input_write: Option<HANDLE>,
    mut writer_rx: mpsc::Receiver<Vec<u8>>,
    normalize_newlines: bool,
) -> tokio::task::JoinHandle<()> {
    tokio::task::spawn_blocking(move || {
        let mut previous_was_cr = false;
        while let Some(bytes) = writer_rx.blocking_recv() {
            let Some(handle) = input_write else {
                continue;
            };
            let bytes = if normalize_newlines {
                normalize_windows_tty_input(&bytes, &mut previous_was_cr)
            } else {
                bytes
            };
            if write_all_handle(handle, &bytes).is_err() {
                break;
            }
        }
        if let Some(handle) = input_write {
            unsafe {
                CloseHandle(handle);
            }
        }
    })
}

fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> {
    while !bytes.is_empty() {
        let mut written = 0u32;
        let ok = unsafe {
            WriteFile(
                handle,
                bytes.as_ptr() as *const _,
                bytes.len() as u32,
                &mut written,
                ptr::null_mut(),
            )
        };
        if ok == 0 {
            let err = unsafe { GetLastError() } as i32;
            return Err(anyhow::anyhow!("WriteFile failed: {err}"));
        }
        if written == 0 {
            anyhow::bail!("WriteFile returned success but wrote 0 bytes");
        }
        bytes = &bytes[written as usize..];
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn finalize_exit(
    exit_tx: oneshot::Sender<i32>,
    process_handle: Arc<StdMutex<Option<HANDLE>>>,
    thread_handle: HANDLE,
    output_join: std::thread::JoinHandle<()>,
    logs_base_dir: Option<&Path>,
    command: Vec<String>,
) {
    let exit_code = {
        let mut raw_exit = 1u32;
        if let Ok(guard) = process_handle.lock()
            && let Some(handle) = guard.as_ref()
        {
            unsafe {
                WaitForSingleObject(*handle, INFINITE);
                GetExitCodeProcess(*handle, &mut raw_exit);
            }
        }
        raw_exit as i32
    };

    let _ = output_join.join();
    let _ = exit_tx.send(exit_code);

    unsafe {
        if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
            CloseHandle(thread_handle);
        }
        if let Ok(mut guard) = process_handle.lock()
            && let Some(handle) = guard.take()
        {
            CloseHandle(handle);
        }
    }

    if exit_code == 0 {
        log_success(&command, logs_base_dir);
    } else {
        log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
    }
}

fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {
    let guard = hpc
        .lock()
        .map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?;
    let hpc = guard
        .as_ref()
        .copied()
        .ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?;
    let result = unsafe {
        ResizePseudoConsole(
            hpc,
            COORD {
                X: size.cols as i16,
                Y: size.rows as i16,
            },
        )
    };
    if result == 0 {
        Ok(())
    } else {
        Err(anyhow::anyhow!(
            "failed to resize console: HRESULT {result}"
        ))
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_legacy(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    command: Vec<String>,
    cwd: &Path,
    mut env_map: HashMap<String, String>,
    timeout_ms: Option<u64>,
    additional_deny_read_paths: &[AbsolutePathBuf],
    additional_deny_write_paths: &[AbsolutePathBuf],
    tty: bool,
    stdin_open: bool,
    use_private_desktop: bool,
) -> Result<SpawnedProcess> {
    let common = prepare_legacy_spawn_context(
        permission_profile,
        workspace_roots,
        codex_home,
        cwd,
        &mut env_map,
        &command,
        SpawnPrepOptions {
            inherit_path: false,
            add_git_safe_directory: false,
        },
    )?;
    if !common.permissions.has_full_disk_read_access() {
        anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
    }
    // WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this
    // backend cannot make capability-SID deny-read ACLs authoritative.
    if !additional_deny_read_paths.is_empty() {
        anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");
    }
    let additional_deny_write_paths = additional_deny_write_paths
        .iter()
        .map(AbsolutePathBuf::to_path_buf)
        .collect::<Vec<_>>();
    let capability_roots = legacy_session_capability_roots(
        &common.permissions,
        &common.current_dir,
        &env_map,
        codex_home,
    );
    let security = prepare_legacy_session_security(
        common.uses_write_capabilities,
        codex_home,
        cwd,
        capability_roots,
    )?;
    allow_null_device_for_workspace_write(common.uses_write_capabilities);

    apply_legacy_session_acl_rules(
        &common.permissions,
        codex_home,
        &common.current_dir,
        &env_map,
        &[],
        &additional_deny_write_paths,
        LegacyAclSids {
            readonly_sid: security.readonly_sid.as_ref(),
            readonly_sid_str: security.readonly_sid_str.as_deref(),
            write_root_sids: &security.write_root_sids,
        },
    )?;

    let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
    let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
    let stderr_rx = if tty {
        None
    } else {
        Some(broadcast::channel::<Vec<u8>>(256))
    };
    let (exit_tx, exit_rx) = oneshot::channel::<i32>();

    let LegacyProcessHandles {
        process: pi,
        output_join,
        writer_handle,
        hpc,
        mut conpty_owner,
        token_handle,
        desktop,
    } = match spawn_legacy_process(
        security.h_token,
        &command,
        cwd,
        &env_map,
        use_private_desktop,
        tty,
        stdin_open,
        stdout_tx,
        stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
        writer_rx,
        common.logs_base_dir.as_deref(),
    ) {
        Ok(handles) => handles,
        Err(err) => {
            unsafe {
                CloseHandle(security.h_token);
            }
            return Err(err);
        }
    };
    let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc))));

    let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
    let wait_handle = Arc::clone(&process_handle);
    let command_for_wait = command.clone();
    let hpc_for_wait = hpc_handle.clone();
    std::thread::spawn(move || {
        let _desktop = desktop;
        let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
        let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
        if wait_res == WAIT_TIMEOUT {
            unsafe {
                if let Ok(guard) = wait_handle.lock()
                    && let Some(handle) = guard.as_ref()
                {
                    let _ = TerminateProcess(*handle, 1);
                }
            }
        }
        if let Some(hpc) = hpc_for_wait
            && let Ok(mut guard) = hpc.lock()
        {
            let _ = guard.take();
        }
        drop(conpty_owner.take());
        unsafe {
            if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {
                CloseHandle(token_handle);
            }
        }
        finalize_exit(
            exit_tx,
            wait_handle,
            pi.hThread,
            output_join,
            common.logs_base_dir.as_deref(),
            command_for_wait,
        );
    });

    let terminator = {
        let process_handle = Arc::clone(&process_handle);
        Some(Box::new(move || {
            if let Ok(guard) = process_handle.lock()
                && let Some(handle) = guard.as_ref()
            {
                unsafe {
                    let _ = TerminateProcess(*handle, 1);
                }
            }
        }) as Box<dyn FnMut() + Send + Sync>)
    };

    let driver = ProcessDriver {
        writer_tx,
        stdout_rx,
        stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
        exit_rx,
        terminator,
        writer_handle: Some(writer_handle),
        resizer: hpc_handle.map(|hpc| {
            Box::new(move |size| resize_conpty_handle(&hpc, size))
                as Box<dyn FnMut(TerminalSize) -> Result<()> + Send>
        }),
    };

    Ok(finish_driver_spawn(driver, stdin_open))
}
