use std::ffi::OsString;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use crate::codex_config::validate_config_toml;
use crate::error::AppError;
use crate::provider::Provider;
use serde_json::Value;

#[derive(Debug, Clone)]
pub(crate) struct PreparedCodexLaunch {
    pub(crate) executable: PathBuf,
    pub(crate) cc_switch_executable: PathBuf,
    pub(crate) provider_id: String,
    pub(crate) codex_home: PathBuf,
}

impl PreparedCodexLaunch {
    pub(crate) fn cleanup_home_dir(&self) -> Result<(), AppError> {
        cleanup_temp_codex_home(&self.codex_home)
    }
}

pub(crate) fn prepare_launch(
    provider: &Provider,
    temp_dir: &Path,
) -> Result<PreparedCodexLaunch, AppError> {
    prepare_launch_with(provider, temp_dir, resolve_codex_binary)
}

pub(crate) fn prepare_launch_with<Resolve>(
    provider: &Provider,
    temp_dir: &Path,
    resolve_codex_binary: Resolve,
) -> Result<PreparedCodexLaunch, AppError>
where
    Resolve: FnOnce() -> Result<PathBuf, AppError>,
{
    let executable = resolve_codex_binary()?;
    let cc_switch_executable = std::env::current_exe().map_err(|err| {
        AppError::localized(
            "codex.temp_launch_current_exe_failed",
            format!("解析当前 cc-switch 可执行文件路径失败: {err}"),
            format!("Failed to resolve current cc-switch executable path: {err}"),
        )
    })?;
    let codex_home = write_temp_codex_home(temp_dir, provider)?;
    Ok(PreparedCodexLaunch {
        executable,
        cc_switch_executable,
        provider_id: provider.id.clone(),
        codex_home,
    })
}

pub(crate) fn resolve_codex_binary() -> Result<PathBuf, AppError> {
    which::which("codex").map_err(|_| {
        AppError::localized(
            "codex.temp_launch_missing_binary",
            "未找到 codex 命令，请先安装 Codex CLI。".to_string(),
            "Could not find `codex` in PATH. Install Codex CLI first.".to_string(),
        )
    })
}

#[cfg(unix)]
pub(crate) fn ensure_temp_launch_supported() -> Result<(), AppError> {
    Ok(())
}

#[cfg(not(unix))]
pub(crate) fn ensure_temp_launch_supported() -> Result<(), AppError> {
    Err(AppError::localized(
        "codex.temp_launch_unsupported_platform",
        "当前平台暂不支持在当前终端临时启动 Codex。".to_string(),
        "Temporary Codex launch in the current terminal is not supported on this platform."
            .to_string(),
    ))
}

#[cfg(unix)]
pub(crate) fn build_handoff_command(
    prepared: &PreparedCodexLaunch,
    native_args: &[OsString],
) -> std::process::Command {
    let mut command = std::process::Command::new("/bin/sh");
    command.arg("-c").arg(
        "codex_home=\"$1\"; codex_bin=\"$2\"; cc_switch_bin=\"$3\"; provider_id=\"$4\"; shift 4; exit_status=0; persist() { \"$cc_switch_bin\" internal capture-codex-temp \"$provider_id\" \"$codex_home\"; persist_status=$?; if [ \"$persist_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: 持久化供应商 $provider_id 的临时 Codex 登录状态失败（failed to persist temporary Codex login state）\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$persist_status; fi; fi; }; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; persist; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; persist; cleanup; exit \"$exit_status\"",
    );
    command.arg("cc-switch-codex-handoff");
    command.arg(&prepared.codex_home);
    command.arg(&prepared.executable);
    command.arg(&prepared.cc_switch_executable);
    command.arg(&prepared.provider_id);
    command.args(native_args);
    command
}

#[cfg(unix)]
pub(crate) fn exec_prepared_codex(
    prepared: &PreparedCodexLaunch,
    native_args: &[OsString],
) -> Result<(), AppError> {
    use std::os::unix::process::CommandExt;

    let exec_err = build_handoff_command(prepared, native_args).exec();
    Err(AppError::localized(
        "codex.temp_launch_exec_failed",
        format!("启动 Codex 失败: {exec_err}"),
        format!("Failed to launch Codex: {exec_err}"),
    ))
}

#[cfg(not(unix))]
pub(crate) fn exec_prepared_codex(
    _prepared: &PreparedCodexLaunch,
    _native_args: &[OsString],
) -> Result<(), AppError> {
    Err(AppError::localized(
        "codex.temp_launch_unsupported_platform",
        "当前平台暂不支持在当前终端临时启动 Codex。".to_string(),
        "Temporary Codex launch in the current terminal is not supported on this platform."
            .to_string(),
    ))
}

fn write_temp_codex_home(temp_dir: &Path, provider: &Provider) -> Result<PathBuf, AppError> {
    write_temp_codex_home_with(temp_dir, provider, finalize_temp_codex_home)
}

fn write_temp_codex_home_with<Finalize>(
    temp_dir: &Path,
    provider: &Provider,
    finalize: Finalize,
) -> Result<PathBuf, AppError>
where
    Finalize: FnOnce(&Path) -> Result<(), AppError>,
{
    let settings = provider.settings_config.as_object().ok_or_else(|| {
        AppError::localized(
            "codex.temp_launch_settings_not_object",
            format!("供应商 {} 的 Codex 配置必须是 JSON 对象。", provider.id),
            format!(
                "Provider {} Codex configuration must be a JSON object.",
                provider.id
            ),
        )
    })?;

    let config_text = match settings.get("config") {
        Some(Value::String(text)) => text.as_str(),
        Some(Value::Null) | None => "",
        Some(_) => {
            return Err(AppError::localized(
                "codex.temp_launch_config_invalid_type",
                format!("供应商 {} 的 config 必须是字符串。", provider.id),
                format!("Provider {} config must be a string.", provider.id),
            ))
        }
    };
    validate_config_toml(config_text)?;

    let auth = match settings.get("auth") {
        Some(Value::Object(auth)) if !auth.is_empty() => Some(Value::Object(auth.clone())),
        Some(Value::Object(_)) | Some(Value::Null) | None => None,
        Some(_) => {
            return Err(AppError::localized(
                "codex.temp_launch_auth_invalid_type",
                format!("供应商 {} 的 auth 必须是 JSON 对象。", provider.id),
                format!("Provider {} auth must be a JSON object.", provider.id),
            ))
        }
    };

    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let dir_name = format!(
        "cc-switch-codex-{}-{}-{timestamp}",
        sanitize_filename_fragment(&provider.id),
        std::process::id()
    );
    let codex_home = temp_dir.join(dir_name);

    let write_result = (|| {
        fs::create_dir_all(&codex_home).map_err(|err| AppError::io(&codex_home, err))?;
        finalize(&codex_home)?;

        let config_path = codex_home.join("config.toml");
        write_secret_file(&config_path, config_text.as_bytes())?;

        if let Some(auth) = auth {
            let auth_path = codex_home.join("auth.json");
            let auth_text = serde_json::to_vec_pretty(&auth)
                .map_err(|source| AppError::JsonSerialize { source })?;
            write_secret_file(&auth_path, &auth_text)?;
        }

        Ok(())
    })();

    match write_result {
        Ok(()) => Ok(codex_home),
        Err(err) => match cleanup_temp_codex_home(&codex_home) {
            Ok(()) => Err(err),
            Err(cleanup_err) => Err(AppError::localized(
                "codex.temp_launch_tempdir_cleanup_failed",
                format!("写入临时 Codex 配置目录失败: {err}；同时清理失败: {cleanup_err}"),
                format!(
                    "Failed to write the temporary Codex home: {err}; also failed to clean it up: {cleanup_err}"
                ),
            )),
        },
    }
}

#[cfg(unix)]
fn finalize_temp_codex_home(path: &Path) -> Result<(), AppError> {
    use std::os::unix::fs::PermissionsExt;

    fs::set_permissions(path, fs::Permissions::from_mode(0o700))
        .map_err(|err| AppError::io(path, err))
}

#[cfg(not(unix))]
fn finalize_temp_codex_home(_path: &Path) -> Result<(), AppError> {
    Ok(())
}

fn write_secret_file(path: &Path, content: &[u8]) -> Result<(), AppError> {
    let mut file = create_secret_temp_file(path)?;
    file.write_all(content)
        .and_then(|()| file.flush())
        .map_err(|err| AppError::io(path, err))
}

#[cfg(unix)]
fn create_secret_temp_file(path: &Path) -> Result<File, AppError> {
    use std::os::unix::fs::OpenOptionsExt;

    OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o600)
        .open(path)
        .map_err(|err| AppError::io(path, err))
}

#[cfg(not(unix))]
fn create_secret_temp_file(path: &Path) -> Result<File, AppError> {
    OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
        .map_err(|err| AppError::io(path, err))
}

fn cleanup_temp_codex_home(path: &Path) -> Result<(), AppError> {
    match fs::remove_dir_all(path) {
        Ok(()) => Ok(()),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(err) => Err(AppError::io(path, err)),
    }
}

fn sanitize_filename_fragment(value: &str) -> String {
    let sanitized: String = value
        .chars()
        .map(|ch| match ch {
            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
            _ => '-',
        })
        .collect();
    if sanitized.is_empty() {
        "provider".to_string()
    } else {
        sanitized
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(unix)]
    use std::ffi::OsString;
    #[cfg(unix)]
    use std::os::unix::{fs::PermissionsExt, process::CommandExt};
    #[cfg(unix)]
    use std::process::Stdio;
    #[cfg(unix)]
    use std::time::Duration;
    use tempfile::TempDir;

    #[cfg(unix)]
    fn write_test_executable(temp_dir: &TempDir, name: &str, body: &str) -> PathBuf {
        let path = temp_dir.path().join(name);
        std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).expect("write stub executable");
        let mut permissions = std::fs::metadata(&path)
            .expect("stat stub executable")
            .permissions();
        permissions.set_mode(0o755);
        std::fs::set_permissions(&path, permissions).expect("chmod stub executable");
        path
    }

    fn provider_with(config: &str, auth: Option<Value>) -> Provider {
        let mut settings = serde_json::Map::new();
        settings.insert("config".to_string(), Value::String(config.to_string()));
        if let Some(auth) = auth {
            settings.insert("auth".to_string(), auth);
        }
        Provider::with_id(
            "demo".to_string(),
            "Demo".to_string(),
            Value::Object(settings),
            None,
        )
    }

    fn official_provider_with_auth(config: &str) -> Provider {
        let mut provider = provider_with(
            config,
            Some(serde_json::json!({ "OPENAI_API_KEY": "stale-key" })),
        );
        provider.website_url = Some("https://chatgpt.com/codex".to_string());
        provider
    }

    #[cfg(unix)]
    #[test]
    fn unix_handoff_command_exports_codex_home_and_cleans_up_temp_dir() {
        let prepared = PreparedCodexLaunch {
            executable: PathBuf::from("/usr/local/bin/codex"),
            cc_switch_executable: PathBuf::from("/usr/local/bin/cc-switch"),
            provider_id: "demo".to_string(),
            codex_home: PathBuf::from("/tmp/cc-switch-codex-home"),
        };
        let native_args = vec![OsString::from("--model"), OsString::from("gpt-5.4")];

        let command = build_handoff_command(&prepared, &native_args);
        let args: Vec<OsString> = command.get_args().map(|arg| arg.to_os_string()).collect();

        assert_eq!(command.get_program(), std::path::Path::new("/bin/sh"));
        assert_eq!(
            args,
            vec![
                OsString::from("-c"),
                OsString::from(
                    "codex_home=\"$1\"; codex_bin=\"$2\"; cc_switch_bin=\"$3\"; provider_id=\"$4\"; shift 4; exit_status=0; persist() { \"$cc_switch_bin\" internal capture-codex-temp \"$provider_id\" \"$codex_home\"; persist_status=$?; if [ \"$persist_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: 持久化供应商 $provider_id 的临时 Codex 登录状态失败（failed to persist temporary Codex login state）\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$persist_status; fi; fi; }; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; persist; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; persist; cleanup; exit \"$exit_status\""
                ),
                OsString::from("cc-switch-codex-handoff"),
                OsString::from("/tmp/cc-switch-codex-home"),
                OsString::from("/usr/local/bin/codex"),
                OsString::from("/usr/local/bin/cc-switch"),
                OsString::from("demo"),
                OsString::from("--model"),
                OsString::from("gpt-5.4"),
            ]
        );
    }

    #[cfg(unix)]
    #[test]
    fn interrupting_handoff_still_cleans_up_temp_codex_home() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let executable = write_test_executable(
            &temp_dir,
            "codex-stub.sh",
            "trap 'exit 130' INT TERM HUP\nwhile :; do sleep 1; done",
        );
        let codex_home = temp_dir.path().join("cc-switch-codex-home");
        std::fs::create_dir_all(&codex_home).expect("create temp codex home");
        std::fs::write(codex_home.join("config.toml"), "model = \"gpt-5.4\"\n")
            .expect("seed config");

        let prepared = PreparedCodexLaunch {
            executable,
            cc_switch_executable: PathBuf::from("/bin/true"),
            provider_id: "demo".to_string(),
            codex_home: codex_home.clone(),
        };
        let mut command = build_handoff_command(&prepared, &[]);
        command.stdout(Stdio::null()).stderr(Stdio::null());
        unsafe {
            command.pre_exec(|| {
                if libc::setsid() == -1 {
                    return Err(std::io::Error::last_os_error());
                }
                Ok(())
            });
        }

        let mut child = command.spawn().expect("spawn handoff");
        std::thread::sleep(Duration::from_millis(150));
        let kill_result = unsafe { libc::kill(-(child.id() as i32), libc::SIGINT) };
        assert_eq!(kill_result, 0, "send SIGINT to handoff process group");

        let status = child.wait().expect("wait for handoff");
        assert_eq!(status.code(), Some(130));
        assert!(
            !codex_home.exists(),
            "temporary Codex home should be removed after interrupt"
        );
    }

    #[cfg(unix)]
    #[test]
    fn cleanup_failure_after_successful_handoff_surfaces_nonzero_exit() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let executable = write_test_executable(&temp_dir, "codex-stub.sh", "exit 0");
        let prepared = PreparedCodexLaunch {
            executable,
            cc_switch_executable: PathBuf::from("/bin/true"),
            provider_id: "demo".to_string(),
            codex_home: PathBuf::from("."),
        };

        let mut command = build_handoff_command(&prepared, &[]);
        command.current_dir(temp_dir.path());
        let output = command.output().expect("run handoff");

        assert!(
            !output.status.success(),
            "cleanup failure should not look like a successful handoff"
        );
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(stderr.contains("failed to remove temporary Codex home"));
    }

    #[test]
    fn temp_codex_home_is_removed_when_finalize_step_fails() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let provider = provider_with(
            "model_provider = \"demo\"\n",
            Some(serde_json::json!({ "OPENAI_API_KEY": "sk-demo" })),
        );

        let err = write_temp_codex_home_with(temp_dir.path(), &provider, |_| {
            Err(AppError::Message("simulated finalize failure".to_string()))
        })
        .expect_err("finalize failure should bubble up");

        assert!(err.to_string().contains("simulated finalize failure"));
        assert!(
            std::fs::read_dir(temp_dir.path())
                .expect("read temp dir")
                .next()
                .is_none(),
            "temporary Codex home should be removed on failure"
        );
    }

    #[test]
    fn prepare_launch_writes_codex_home_files() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let provider = provider_with(
            "model_provider = \"demo\"\nmodel = \"gpt-5.2-codex\"\n",
            Some(serde_json::json!({ "OPENAI_API_KEY": "sk-demo" })),
        );

        let prepared = prepare_launch_with(&provider, temp_dir.path(), || {
            Ok(PathBuf::from("/usr/bin/codex"))
        })
        .expect("prepare launch");

        assert_eq!(prepared.executable, PathBuf::from("/usr/bin/codex"));
        assert_eq!(
            std::fs::read_to_string(prepared.codex_home.join("config.toml"))
                .expect("read config.toml"),
            "model_provider = \"demo\"\nmodel = \"gpt-5.2-codex\"\n"
        );
        let auth: Value = serde_json::from_str(
            &std::fs::read_to_string(prepared.codex_home.join("auth.json"))
                .expect("read auth.json"),
        )
        .expect("parse auth.json");
        assert_eq!(auth, serde_json::json!({ "OPENAI_API_KEY": "sk-demo" }));

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;

            let dir_mode = std::fs::metadata(&prepared.codex_home)
                .expect("stat codex home")
                .permissions()
                .mode()
                & 0o777;
            assert_eq!(dir_mode, 0o700);

            let auth_mode = std::fs::metadata(prepared.codex_home.join("auth.json"))
                .expect("stat auth.json")
                .permissions()
                .mode()
                & 0o777;
            assert_eq!(auth_mode, 0o600);
        }
    }

    #[test]
    fn prepare_launch_allows_missing_auth_for_official_style_providers() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let provider = provider_with("model_provider = \"openai\"\n", None);

        let prepared = prepare_launch_with(&provider, temp_dir.path(), || {
            Ok(PathBuf::from("/usr/bin/codex"))
        })
        .expect("prepare launch");

        assert!(!prepared.codex_home.join("auth.json").exists());
    }

    #[test]
    fn prepare_launch_writes_auth_file_for_official_provider() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let provider = official_provider_with_auth("model_provider = \"openai\"\n");

        let prepared = prepare_launch_with(&provider, temp_dir.path(), || {
            Ok(PathBuf::from("/usr/bin/codex"))
        })
        .expect("prepare launch");

        assert!(prepared.codex_home.join("auth.json").exists());
    }

    #[test]
    fn missing_codex_binary_reports_an_error() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let provider = provider_with("model_provider = \"demo\"\n", None);

        let err = prepare_launch_with(&provider, temp_dir.path(), || {
            Err(AppError::Message("codex binary is missing".to_string()))
        })
        .expect_err("missing binary should fail");

        assert!(err.to_string().contains("codex"));
    }
}
