use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VigthoriaAuthStatus {
    pub cli_installed: bool,
    pub has_token: bool,
    pub authenticated: bool,
    pub email: Option<String>,
    pub user_id: Option<String>,
    pub plan: Option<String>,
    pub is_admin: bool,
    pub is_master_admin: bool,
    pub api_url: String,
    pub models_api_url: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BundledCliStatus {
    pub version: String,
    pub available: bool,
    pub installed: bool,
    pub package_path: Option<String>,
}

const BUNDLED_CLI_VERSION: &str = "1.13.13";
const BUNDLED_CLI_SHA256: &str =
    "3cb03c1f4ffc50cf107f6da7fb130461125c1674e5161830af1e99d9cbcdf8f9";

/// Live plan/admin data parsed from Coder's own `/api/user/subscription`
/// response body, as returned by probe_token(). Preferred over the locally
/// cached CLI config value (which can be stale after a plan change/
/// cancellation) whenever a live probe succeeds.
#[derive(Debug, Clone, Default)]
struct RemoteAuthProbe {
    /// Session is valid (not a 401/403 -- distinct from having plan data).
    ok: bool,
    plan: Option<String>,
    is_admin: bool,
    is_master_admin: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VigthoriaHttpResponse {
    pub ok: bool,
    pub status: u16,
    pub body: String,
}

fn cli_config_paths() -> Vec<PathBuf> {
    let mut paths = Vec::new();

    if let Some(home) = dirs::home_dir() {
        paths.push(home.join(".vigthoria").join("config.json"));
        paths.push(home.join(".vigthoria").join("auth.json"));
        paths.push(home.join(".vigthoria").join("session.json"));
    }

    if let Some(config_dir) = dirs::config_dir() {
        paths.push(config_dir.join("vigthoria-cli").join("config.json"));
        paths.push(
            config_dir
                .join("vigthoria-cli-nodejs")
                .join("Config")
                .join("config.json"),
        );
        paths.push(config_dir.join("vigthoria-cli-nodejs").join("config.json"));
    }

    if let Ok(appdata) = std::env::var("APPDATA") {
        let appdata = PathBuf::from(appdata);
        paths.push(
            appdata
                .join("vigthoria-cli-nodejs")
                .join("Config")
                .join("config.json"),
        );
        paths.push(appdata.join("vigthoria-cli").join("config.json"));
    }

    paths
}

fn read_cli_config() -> Option<Value> {
    for path in cli_config_paths() {
        let Ok(raw) = std::fs::read_to_string(path) else {
            continue;
        };
        if let Ok(parsed) = serde_json::from_str(&raw) {
            return Some(parsed);
        }
    }
    None
}

fn string_field<'a>(config: &'a Value, keys: &[&str]) -> Option<&'a str> {
    for key in keys {
        if let Some(value) = config.get(*key).and_then(|value| value.as_str()) {
            return Some(value);
        }
    }
    None
}

fn nested_string_field<'a>(config: &'a Value, object_key: &str, keys: &[&str]) -> Option<&'a str> {
    let object = config.get(object_key)?;
    string_field(object, keys)
}

fn legacy_plan(config: &Value) -> Option<String> {
    config
        .get("subscription")
        .and_then(|s| s.get("plan"))
        .and_then(|v| v.as_str())
        .or_else(|| config.get("plan").and_then(|v| v.as_str()))
        .map(str::to_string)
}

pub fn detect_package_script(root_path: &str) -> Result<Option<String>, String> {
    let package_path = Path::new(root_path.trim()).join("package.json");
    let raw = match std::fs::read_to_string(&package_path) {
        Ok(raw) => raw,
        Err(_) => return Ok(None),
    };
    let parsed: Value =
        serde_json::from_str(&raw).map_err(|e| format!("Invalid package.json: {e}"))?;
    let Some(scripts) = parsed.get("scripts").and_then(|value| value.as_object()) else {
        return Ok(None);
    };

    for name in ["dev", "start", "serve", "preview"] {
        if scripts.contains_key(name) {
            return Ok(Some(match name {
                "start" => "npm start".into(),
                other => format!("npm run {other}"),
            }));
        }
    }

    Ok(None)
}

pub fn detect_stabilize_command(root_path: &str) -> Result<Option<String>, String> {
    let package_path = Path::new(root_path.trim()).join("package.json");
    let raw = match std::fs::read_to_string(&package_path) {
        Ok(raw) => raw,
        Err(_) => return Ok(None),
    };
    let parsed: Value =
        serde_json::from_str(&raw).map_err(|e| format!("Invalid package.json: {e}"))?;
    let Some(scripts) = parsed.get("scripts").and_then(|value| value.as_object()) else {
        return Ok(None);
    };

    for name in ["lint", "typecheck", "check", "build"] {
        if scripts.contains_key(name) {
            return Ok(Some(format!("npm run {name}")));
        }
    }

    Ok(None)
}

pub fn run_workspace_stabilize(root_path: &str) -> Result<(bool, String), String> {
    run_stabilize_in_dir(root_path, false)
}

pub fn run_workspace_stabilize_sandbox(root_path: &str) -> Result<(bool, String), String> {
    run_stabilize_in_dir(root_path, true)
}

fn run_stabilize_in_dir(root_path: &str, sandbox: bool) -> Result<(bool, String), String> {
    let root = root_path.trim();
    if root.is_empty() {
        return Err("Workspace root is empty".into());
    }

    let work_dir = if sandbox {
        prepare_sandbox(root)?
    } else {
        PathBuf::from(root)
    };

    let cmd = detect_stabilize_command(work_dir.to_string_lossy().as_ref())?;
    let Some(command) = cmd else {
        if sandbox {
            let _ = std::fs::remove_dir_all(&work_dir);
        }
        return Ok((true, "No lint/typecheck script in package.json — skipped".into()));
    };

    let Some(script_name) = command.strip_prefix("npm run ") else {
        if sandbox {
            let _ = std::fs::remove_dir_all(&work_dir);
        }
        return Err(format!("Unsupported stabilize command: {command}"));
    };

    let mut stabilize_command = Command::new(npm_executable());
    stabilize_command.args(["run", script_name]).current_dir(&work_dir);
    suppress_console_window(&mut stabilize_command);
    let output = stabilize_command
        .output()
        .map_err(|e| format!("Failed to run stabilize: {e}"))?;

    if sandbox {
        let _ = std::fs::remove_dir_all(&work_dir);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    let prefix = if sandbox { "[sandbox] " } else { "" };
    let combined = format!("{prefix}$ {command}\n{stdout}{stderr}");
    Ok((output.status.success(), combined))
}

fn prepare_sandbox(root: &str) -> Result<PathBuf, String> {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    use std::time::{SystemTime, UNIX_EPOCH};

    let root_path = PathBuf::from(root);
    let mut hasher = DefaultHasher::new();
    root.hash(&mut hasher);
    let _ = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos().hash(&mut hasher));
    let id = hasher.finish();

    let sandbox = std::env::temp_dir().join(format!("vigthoria-goa-{id:016x}"));
    if sandbox.exists() {
        std::fs::remove_dir_all(&sandbox).map_err(|e| e.to_string())?;
    }
    std::fs::create_dir_all(&sandbox).map_err(|e| e.to_string())?;

    copy_sandbox_tree(&root_path, &sandbox, &root_path, 0)?;

    let src_node_modules = root_path.join("node_modules");
    if src_node_modules.is_dir() {
        #[cfg(unix)]
        {
            use std::os::unix::fs::symlink;
            let _ = symlink(&src_node_modules, sandbox.join("node_modules"));
        }
        #[cfg(windows)]
        {
            std::os::windows::fs::symlink_dir(&src_node_modules, sandbox.join("node_modules"))
                .map_err(|e| e.to_string())?;
        }
    }

    Ok(sandbox)
}

fn copy_sandbox_tree(src_root: &Path, dst_root: &Path, current: &Path, depth: usize) -> Result<(), String> {
    if depth > 6 {
        return Ok(());
    }
    let read_dir = std::fs::read_dir(current).map_err(|e| e.to_string())?;
    for entry in read_dir {
        let entry = entry.map_err(|e| e.to_string())?;
        let name = entry.file_name().to_string_lossy().into_owned();
        if name.starts_with('.') && name != ".env" {
            continue;
        }
        if ["node_modules", "target", "dist", "build", ".git", ".next"].contains(&name.as_str()) {
            continue;
        }
        let src_path = entry.path();
        let rel = src_path
            .strip_prefix(src_root)
            .unwrap_or(&src_path)
            .to_path_buf();
        let dst_path = dst_root.join(rel);
        if entry.file_type().map_err(|e| e.to_string())?.is_dir() {
            std::fs::create_dir_all(&dst_path).map_err(|e| e.to_string())?;
            copy_sandbox_tree(src_root, dst_root, &src_path, depth + 1)?;
        } else {
            if let Some(parent) = dst_path.parent() {
                std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
            }
            std::fs::copy(&src_path, &dst_path).map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}

pub fn resolve_cli_command(cli_path: Option<&str>) -> String {
    cli_path
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .unwrap_or_else(|| "vigthoria".into())
}

/// Build a `Command` that can actually execute the resolved CLI entry point.
///
/// npm global installs on Windows create `.cmd`/`.bat` shims (not PE
/// executables). `std::process::Command::new()` uses `CreateProcessW`
/// directly and cannot launch those shims - it fails with
/// "program not found" (or "%1 is not a valid Win32 application") even
/// when the resolved path is correct. Route through `cmd.exe /C` on
/// Windows so the shell resolves and executes the shim correctly; on
/// other platforms spawn the binary directly.
pub fn build_cli_command(cli: &str, args: &[&str]) -> Command {
    if cfg!(windows) {
        let comspec = std::env::var("COMSPEC").unwrap_or_else(|_| r"C:\Windows\System32\cmd.exe".into());
        let mut full = if cli.contains(' ') {
            format!("\"{cli}\"")
        } else {
            cli.to_string()
        };
        for arg in args {
            full.push(' ');
            if arg.contains(' ') {
                full.push('"');
                full.push_str(arg);
                full.push('"');
            } else {
                full.push_str(arg);
            }
        }
        let mut command = Command::new(&comspec);
        command.args(["/C", &full]);
        suppress_console_window(&mut command);
        return command;
    }

    let mut command = Command::new(cli);
    command.args(args);
    command
}

/// Prevent Windows from flashing a visible console window when a GUI app
/// spawns a console-subsystem child (cmd.exe, npm.cmd, etc.) via `Command`.
/// Without this, every background `vigthoria`/`npm` invocation (update
/// checks, lint/build stabilize runs, ...) pops up a black terminal window.
#[cfg(windows)]
fn suppress_console_window(command: &mut Command) {
    use std::os::windows::process::CommandExt;
    const CREATE_NO_WINDOW: u32 = 0x0800_0000;
    command.creation_flags(CREATE_NO_WINDOW);
}

#[cfg(not(windows))]
fn suppress_console_window(_command: &mut Command) {}

const NPM_INSTALL_TIMEOUT_SECS: u64 = 180;
const LOGIN_TIMEOUT_SECS: u64 = 45;

/// Best-effort kill of a possibly still-running child process by pid. Used
/// when a wall-clock timeout fires so an abandoned npm/login process doesn't
/// keep running forever in the background.
fn kill_pid_best_effort(pid: u32) {
    #[cfg(windows)]
    {
        let _ = Command::new("taskkill")
            .args(["/F", "/T", "/PID", &pid.to_string()])
            .output();
    }
    #[cfg(not(windows))]
    {
        let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
    }
}

/// Run a child process with piped stdio and a hard wall-clock timeout so a
/// hung/slow subprocess (npm registry stalls, network issues, etc.) can never
/// block the Workbench UI thread forever. The child is waited on from a
/// background thread; on timeout, the child is killed best-effort and an
/// error is returned instead of waiting indefinitely.
fn run_command_with_timeout(mut command: Command, timeout: Duration) -> Result<String, String> {
    command.stdout(Stdio::piped()).stderr(Stdio::piped());
    let child = command
        .spawn()
        .map_err(|e| format!("Failed to start process: {e}"))?;
    let pid = child.id();
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let result = child.wait_with_output();
        let _ = tx.send(result);
    });

    match rx.recv_timeout(timeout) {
        Ok(Ok(output)) => {
            let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
            let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
            if output.status.success() {
                Ok(format!("{stdout}{stderr}"))
            } else {
                Err(format!("{stdout}{stderr}"))
            }
        }
        Ok(Err(e)) => Err(format!("Process error: {e}")),
        Err(_) => {
            kill_pid_best_effort(pid);
            Err(format!(
                "Timed out after {}s waiting for the process to finish. It has been terminated.",
                timeout.as_secs()
            ))
        }
    }
}

pub fn bundled_cli_status(package_path: Option<&Path>) -> BundledCliStatus {
    BundledCliStatus {
        version: BUNDLED_CLI_VERSION.into(),
        available: package_path.is_some_and(Path::is_file),
        installed: which::which("vigthoria").is_ok(),
        package_path: package_path.map(|path| path.to_string_lossy().into_owned()),
    }
}

fn sha256_file(path: &Path) -> Result<String, String> {
    let mut file = std::fs::File::open(path).map_err(|e| e.to_string())?;
    let mut digest = Sha256::new();
    let mut buffer = [0u8; 64 * 1024];
    loop {
        let read = file.read(&mut buffer).map_err(|e| e.to_string())?;
        if read == 0 {
            break;
        }
        digest.update(&buffer[..read]);
    }
    Ok(format!("{:x}", digest.finalize()))
}

pub fn install_bundled_cli(package_path: &Path) -> Result<String, String> {
    if !package_path.is_file() {
        return Err("Bundled Vigthoria CLI package was not found.".into());
    }
    let actual = sha256_file(package_path)?;
    if !actual.eq_ignore_ascii_case(BUNDLED_CLI_SHA256) {
        return Err(format!(
            "Bundled CLI checksum mismatch (expected {BUNDLED_CLI_SHA256}, got {actual})"
        ));
    }
    if which::which(npm_executable()).is_err() {
        return Err("Node.js/npm is required to install the bundled CLI.".into());
    }
    let mut command = Command::new(npm_executable());
    command
        .arg("install")
        .arg("--global")
        .arg(package_path.as_os_str());
    let output = run_command_with_timeout(
        command,
        Duration::from_secs(NPM_INSTALL_TIMEOUT_SECS),
    )?;
    Ok(format!(
        "Installed Vigthoria CLI {BUNDLED_CLI_VERSION} from the verified Workbench resource.\n{output}"
    ))
}

/// Run `vigthoria update` with a hard timeout so a stalled npm registry or
/// network issue can never hang the Workbench UI (previously this used a
/// plain blocking `.output()` call with no timeout at all).
pub fn run_update(cli_path: Option<&str>) -> Result<String, String> {
    let cli = resolve_cli_command(cli_path);
    let command = build_cli_command(&cli, &["update"]);
    run_command_with_timeout(command, Duration::from_secs(NPM_INSTALL_TIMEOUT_SECS))
}

pub fn run_community_repo_command(
    cli_path: Option<&str>,
    action: &str,
    root_path: &str,
    repository_name: Option<&str>,
    visibility: Option<&str>,
) -> Result<String, String> {
    let root = Path::new(root_path.trim())
        .canonicalize()
        .map_err(|e| format!("Invalid project directory: {e}"))?;
    if !root.is_dir() {
        return Err("Project path must be an existing directory.".into());
    }

    let mut args = vec!["repo".to_string()];
    match action {
        "list" => args.push("list".into()),
        "status" => args.push("status".into()),
        "push" => {
            let visibility = visibility.unwrap_or("private");
            if !matches!(visibility, "private" | "restricted" | "public") {
                return Err("Visibility must be private, restricted, or public.".into());
            }
            args.extend([
                "push".into(),
                root.to_string_lossy().into_owned(),
                "--yes".into(),
                "--visibility".into(),
                visibility.into(),
            ]);
            if let Some(name) = repository_name.map(str::trim).filter(|name| !name.is_empty()) {
                args.extend(["--name".into(), name.into()]);
            }
        }
        "pull" => {
            let name = repository_name
                .map(str::trim)
                .filter(|name| !name.is_empty())
                .ok_or_else(|| "Repository name is required for pull.".to_string())?;
            args.extend([
                "pull".into(),
                name.into(),
                "--output".into(),
                root.to_string_lossy().into_owned(),
            ]);
        }
        _ => return Err("Unsupported Community repository action.".into()),
    }

    let cli = resolve_cli_command(cli_path);
    let refs: Vec<&str> = args.iter().map(String::as_str).collect();
    let mut command = build_cli_command(&cli, &refs);
    command.current_dir(&root);
    run_command_with_timeout(command, Duration::from_secs(300))
}

/// Run `vigthoria logout` to clear the CLI's stored auth token, then remove
/// any locally-cached CLI config files as a fallback so the Workbench never
/// reports a stale "authenticated" state even if the CLI's own logout command
/// is unavailable or fails for some reason (e.g. a corrupted config file).
pub fn run_logout(cli_path: Option<&str>) -> Result<String, String> {
    let cli = resolve_cli_command(cli_path);
    let cli_result = if cli == "vigthoria" && which::which("vigthoria").is_err() {
        Err("Vigthoria CLI not found on PATH.".to_string())
    } else {
        let command = build_cli_command(&cli, &["logout"]);
        run_command_with_timeout(command, Duration::from_secs(LOGIN_TIMEOUT_SECS))
    };

    let mut removed_any = false;
    for path in cli_config_paths() {
        if path.exists() {
            if std::fs::remove_file(&path).is_ok() {
                removed_any = true;
            }
        }
    }

    match cli_result {
        Ok(output) => Ok(if output.trim().is_empty() {
            "Logged out.".to_string()
        } else {
            output
        }),
        Err(cli_err) => {
            if removed_any {
                Ok("Logged out (cleared local session; CLI logout reported: {cli_err}).".replace("{cli_err}", &cli_err))
            } else {
                Err(cli_err)
            }
        }
    }
}

pub fn launch_login(cli_path: Option<&str>) -> Result<String, String> {
    let cli = resolve_cli_command(cli_path);
    if cli == "vigthoria" && which::which("vigthoria").is_err() {
        return Err(
            "Vigthoria CLI not found. Install it first with: npm install -g vigthoria-cli".into(),
        );
    }

    if cfg!(windows) {
        let comspec =
            std::env::var("COMSPEC").unwrap_or_else(|_| r"C:\Windows\System32\cmd.exe".into());
        let quoted_cli = if cli.contains(' ') {
            format!("\"{cli}\"")
        } else {
            cli.to_string()
        };
        let shell_command = format!(
            "{quoted_cli} login && echo. && echo When login is complete, return to Vigthoria Workbench and click Refresh status. && pause"
        );
        Command::new(&comspec)
            .args(["/C", "start", "", "cmd", "/K", &shell_command])
            .spawn()
            .map_err(|e| format!("Failed to open login terminal: {e}"))?;
        return Ok("Login window opened. Complete login there, then click Refresh status.".into());
    }

    if cfg!(target_os = "macos") {
        let escaped = cli.replace('\\', "\\\\").replace('"', "\\\"");
        let terminal_command = format!(
            "{escaped} login; echo; echo 'Return to Vigthoria Workbench and click Refresh status.'"
        );
        Command::new("osascript")
            .args([
                "-e",
                &format!(
                    "tell application \"Terminal\" to do script \"{}\"",
                    terminal_command.replace('"', "\\\"")
                ),
                "-e",
                "tell application \"Terminal\" to activate",
            ])
            .spawn()
            .map_err(|e| format!("Failed to open Terminal for login: {e}"))?;
        return Ok(
            "Login terminal opened. Complete login there, then click Refresh status.".into(),
        );
    }

    let shell_command = format!(
        "{} login; echo; echo 'Return to Vigthoria Workbench and click Refresh status.'; read -r -p 'Press Enter to close...'",
        shell_escape(&cli)
    );
    let terminal_attempts: Vec<(&str, Vec<&str>)> = vec![
        (
            "x-terminal-emulator",
            vec!["-e", "sh", "-lc", &shell_command],
        ),
        ("gnome-terminal", vec!["--", "sh", "-lc", &shell_command]),
        ("konsole", vec!["-e", "sh", "-lc", &shell_command]),
        ("xterm", vec!["-e", "sh", "-lc", &shell_command]),
    ];
    for (terminal, args) in terminal_attempts {
        if which::which(terminal).is_ok() && Command::new(terminal).args(args).spawn().is_ok() {
            return Ok(
                "Login terminal opened. Complete login there, then click Refresh status.".into(),
            );
        }
    }

    Err("Could not find a desktop terminal to run vigthoria login. Open a terminal and run: vigthoria login".into())
}

/// Sign in without ever opening a visible terminal window or putting the
/// password on the command line (so it can't show up in `ps`/Task Manager).
/// Email is passed as a CLI flag (not sensitive); the password is written to
/// the child process's stdin, which is exactly what `vigthoria login`
/// falls back to reading when `--password` is omitted.
pub fn login_with_credentials(
    cli_path: Option<&str>,
    email: &str,
    password: &str,
) -> Result<String, String> {
    let cli = resolve_cli_command(cli_path);
    let email = email.trim();
    if email.is_empty() {
        return Err("Email is required.".into());
    }
    if password.is_empty() {
        return Err("Password is required.".into());
    }
    if cli == "vigthoria" && which::which("vigthoria").is_err() {
        return Err(
            "Vigthoria CLI not found. Install it first with: npm install -g vigthoria-cli".into(),
        );
    }

    let mut command = build_cli_command(&cli, &["login", "-e", email]);
    command
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let mut child = command
        .spawn()
        .map_err(|e| format!("Failed to start vigthoria login: {e}"))?;

    if let Some(mut stdin) = child.stdin.take() {
        stdin
            .write_all(format!("{password}\n").as_bytes())
            .map_err(|e| format!("Failed to send credentials: {e}"))?;
    }

    let pid = child.id();
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let result = child.wait_with_output();
        let _ = tx.send(result);
    });

    let output = match rx.recv_timeout(Duration::from_secs(LOGIN_TIMEOUT_SECS)) {
        Ok(Ok(output)) => output,
        Ok(Err(e)) => return Err(format!("Login process failed: {e}")),
        Err(_) => {
            kill_pid_best_effort(pid);
            return Err(format!(
                "Login timed out after {LOGIN_TIMEOUT_SECS}s. Check your network connection and try again."
            ));
        }
    };
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    let combined = format!("{stdout}{stderr}");

    if output.status.success() && combined.to_lowercase().contains("logged in successfully") {
        Ok(combined.trim().to_string())
    } else {
        let message = combined
            .lines()
            .rev()
            .find(|line| !line.trim().is_empty())
            .unwrap_or("Login failed. Check your email and password and try again.");
        Err(message.trim().to_string())
    }
}

fn shell_escape(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn npm_executable() -> &'static str {
    if cfg!(windows) {
        "npm.cmd"
    } else {
        "npm"
    }
}

fn trim_api_url(value: &str, fallback: &str) -> String {
    let trimmed = value.trim().trim_end_matches('/');
    if trimmed.is_empty() {
        fallback.to_string()
    } else {
        trimmed.to_string()
    }
}

fn canonical_coder_api_url(value: &str) -> String {
    let candidate = trim_api_url(value, "https://coder.vigthoria.io");
    // This URL receives the bearer token during the live subscription probe.
    // Never reuse an agent/model endpoint (or an arbitrary config value) as
    // the identity/billing authority. Besides producing hard-to-diagnose
    // 404s, doing so could disclose the user's CLI token.
    let probe_url = format!("{candidate}/api/user/subscription");
    if token_bearing_url_allowed(&probe_url)
        && probe_url
            .strip_prefix("https://")
            .and_then(|rest| rest.split('/').next())
            .map(|host| host.eq_ignore_ascii_case("coder.vigthoria.io"))
            .unwrap_or(false)
    {
        candidate
    } else {
        "https://coder.vigthoria.io".to_string()
    }
}

fn parse_remote_probe_body(body: &str) -> (Option<String>, bool, bool) {
    let Ok(parsed) = serde_json::from_str::<Value>(body) else {
        return (None, false, false);
    };
    let sub = parsed.get("subscription");
    let plan = sub
        .and_then(|s| s.get("plan"))
        .and_then(|v| v.as_str())
        .map(str::to_string);
    let is_admin = sub
        .and_then(|s| s.get("isAdmin"))
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let is_master_admin = sub
        .and_then(|s| s.get("isMasterAdmin"))
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    (plan, is_admin, is_master_admin)
}

fn probe_token(api_url: &str, token: &str) -> RemoteAuthProbe {
    let url = format!("{api_url}/api/user/subscription");
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(8))
        .build();
    let response = agent
        .get(&url)
        .set("Authorization", &format!("Bearer {token}"))
        .set("Cookie", &format!("vigthoria-auth-token={token}"))
        .set("Accept", "application/json")
        .call();
    match response {
        Ok(resp) => {
            let status = resp.status();
            let ok = status != 401 && status != 403;
            if !ok {
                return RemoteAuthProbe { ok: false, ..Default::default() };
            }
            // Live plan/admin data straight from Coder's own authoritative
            // response -- preferred over the locally cached CLI config value,
            // which can be stale after a plan change/cancellation.
            let body = resp.into_string().unwrap_or_default();
            let (plan, is_admin, is_master_admin) = parse_remote_probe_body(&body);
            RemoteAuthProbe { ok: true, plan, is_admin, is_master_admin }
        }
        // ureq returns Err(Error::Status(code, _)) for any HTTP status >= 400,
        // so a real 401/403 (expired/invalid token) never reaches the Ok(_)
        // branch above -- it must be classified here, or an expired session
        // gets silently treated as still-valid ("fail open"), which is exactly
        // what caused the Workbench top bar to show "logged in" while the
        // actual vigthoria CLI (axios-based, sees the same 401/403 correctly)
        // reported a real gateway authentication failure.
        Err(ureq::Error::Status(code, _)) => RemoteAuthProbe {
            ok: code != 401 && code != 403,
            ..Default::default()
        },
        // Genuine transport/network errors (DNS, connect, timeout) should not
        // be treated as an invalid session -- fail open only for these (plan
        // data simply falls back to the locally cached config value).
        Err(ureq::Error::Transport(_)) => RemoteAuthProbe { ok: true, ..Default::default() },
    }
}

pub fn get_auth_status(verify_remote: bool) -> VigthoriaAuthStatus {
    let cli_installed = which::which("vigthoria").is_ok();
    let config = read_cli_config();

    let api_url = config
        .as_ref()
        .and_then(|c| string_field(c, &["apiUrl", "api_url"]))
        .map(canonical_coder_api_url)
        .unwrap_or_else(|| "https://coder.vigthoria.io".to_string());

    let models_api_url = config
        .as_ref()
        .and_then(|c| string_field(c, &["modelsApiUrl", "models_api_url"]))
        .map(|s| trim_api_url(s, "https://api.vigthoria.io"))
        .unwrap_or_else(|| "https://api.vigthoria.io".to_string());

    let token = config
        .as_ref()
        .and_then(|c| string_field(c, &["authToken", "token", "accessToken"]))
        .map(str::trim)
        .filter(|t| !t.is_empty())
        .map(|t| t.to_string());

    let email = config
        .as_ref()
        .and_then(|c| {
            string_field(c, &["email"]).or_else(|| nested_string_field(c, "user", &["email"]))
        })
        .map(|s| s.to_string());

    let user_id = config
        .as_ref()
        .and_then(|c| {
            string_field(c, &["userId", "user_id", "id"])
                .or_else(|| nested_string_field(c, "user", &["id", "userId", "user_id"]))
        })
        .map(|s| s.to_string());

    let plan = config.as_ref().and_then(legacy_plan);

    let has_token = token.is_some();

    if !cli_installed {
        return VigthoriaAuthStatus {
            cli_installed: false,
            has_token: false,
            authenticated: false,
            email: None,
            user_id: None,
            plan: None,
            is_admin: false,
            is_master_admin: false,
            api_url,
            models_api_url,
            message: "Vigthoria CLI not found. Install with: npm install -g vigthoria-cli".into(),
        };
    }

    if !has_token {
        return VigthoriaAuthStatus {
            cli_installed: true,
            has_token: false,
            authenticated: false,
            email: None,
            user_id: None,
            plan: None,
            is_admin: false,
            is_master_admin: false,
            api_url,
            models_api_url,
            message: "Not logged in. Run vigthoria login in the Main Agent terminal.".into(),
        };
    }

    let token = token.unwrap();
    let remote_probe = if verify_remote {
        probe_token(&api_url, &token)
    } else {
        RemoteAuthProbe { ok: true, ..Default::default() }
    };

    if !remote_probe.ok {
        return VigthoriaAuthStatus {
            cli_installed: true,
            has_token: true,
            authenticated: false,
            email,
            user_id,
            plan,
            is_admin: false,
            is_master_admin: false,
            api_url,
            models_api_url,
            message: "Session expired or invalid. Run vigthoria login again.".into(),
        };
    }

    // Prefer the LIVE plan/admin data from Coder's own response (only
    // available when verify_remote actually made a successful call) over the
    // locally cached CLI config value, which can be stale after a plan
    // change/cancellation.
    let plan = remote_probe.plan.clone().or(plan);
    let is_admin = remote_probe.is_admin;
    let is_master_admin = remote_probe.is_master_admin;

    let who = email
        .clone()
        .unwrap_or_else(|| "Vigthoria user".to_string());
    VigthoriaAuthStatus {
        cli_installed: true,
        has_token: true,
        authenticated: true,
        email,
        user_id,
        plan,
        is_admin,
        is_master_admin,
        api_url,
        models_api_url,
        message: format!("Logged in as {who}"),
    }
}

pub fn get_auth_token() -> Option<String> {
    let config = read_cli_config()?;
    let token = string_field(&config, &["authToken", "token", "accessToken"])?.trim();
    if token.is_empty() {
        None
    } else {
        Some(token.to_string())
    }
}

fn token_bearing_url_allowed(url: &str) -> bool {
    let trimmed = url.trim();
    let Some(rest) = trimmed.strip_prefix("https://") else {
        return false;
    };
    let host_port = rest.split('/').next().unwrap_or_default();
    if host_port.is_empty() || host_port.contains('@') {
        return false;
    }
    let host = host_port
        .split(':')
        .next()
        .unwrap_or_default()
        .trim_end_matches('.')
        .to_ascii_lowercase();
    matches!(
        host.as_str(),
        "coder.vigthoria.io"
            | "api.vigthoria.io"
            | "hyperloop.vigthoria.io"
            | "gateway.vigthoria.io"
            | "community.vigthoria.io"
    )
}

pub fn vigthoria_http_request(
    method: &str,
    url: &str,
    token: Option<&str>,
    body: Option<&str>,
) -> Result<VigthoriaHttpResponse, String> {
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(120))
        .build();

    let method_upper = method.to_uppercase();
    let mut request = match method_upper.as_str() {
        "GET" => agent.get(url),
        "POST" => agent.post(url),
        "PUT" => agent.put(url),
        "DELETE" => agent.delete(url),
        other => return Err(format!("Unsupported HTTP method: {other}")),
    };

    request = request.set("Accept", "application/json");
    if body.is_some() {
        request = request.set("Content-Type", "application/json");
    }
    if let Some(token) = token.filter(|t| !t.is_empty()) {
        if !token_bearing_url_allowed(url) {
            return Err("Refusing to send Vigthoria auth token to a non-Vigthoria HTTPS host".into());
        }
        request = request.set("Authorization", &format!("Bearer {token}"));
        request = request.set("Cookie", &format!("vigthoria-auth-token={token}"));
    }

    let response = if let Some(body) = body {
        request.send_string(body)
    } else {
        request.call()
    };

    match response {
        Ok(resp) => {
            let status = resp.status();
            let body = resp.into_string().unwrap_or_default();
            Ok(VigthoriaHttpResponse {
                ok: (200..300).contains(&status),
                status,
                body,
            })
        }
        Err(ureq::Error::Status(code, resp)) => {
            let body = resp.into_string().unwrap_or_default();
            Ok(VigthoriaHttpResponse {
                ok: false,
                status: code,
                body,
            })
        }
        Err(err) => Err(format!("Request failed: {err}")),
    }
}

#[cfg(test)]
mod security_tests {
    use super::{
        bundled_cli_status, canonical_coder_api_url, token_bearing_url_allowed,
        BUNDLED_CLI_VERSION,
    };

    #[test]
    fn token_urls_require_vigthoria_https_hosts() {
        assert!(token_bearing_url_allowed("https://coder.vigthoria.io/api/user"));
        assert!(token_bearing_url_allowed("https://hyperloop.vigthoria.io/api"));
        assert!(!token_bearing_url_allowed("http://coder.vigthoria.io/api"));
        assert!(!token_bearing_url_allowed("https://untrusted.vigthoria.io/api"));
        assert!(!token_bearing_url_allowed("https://vigthoria.io.evil.example/api"));
        assert!(!token_bearing_url_allowed("https://user@coder.vigthoria.io/api"));
    }

    #[test]
    fn subscription_probe_never_reuses_agent_or_model_urls() {
        assert_eq!(
            canonical_coder_api_url("https://coder.vigthoria.io/"),
            "https://coder.vigthoria.io"
        );
        assert_eq!(
            canonical_coder_api_url("http://172.19.0.1:8030"),
            "https://coder.vigthoria.io"
        );
        assert_eq!(
            canonical_coder_api_url("https://api.vigthoria.io"),
            "https://coder.vigthoria.io"
        );
        assert_eq!(
            canonical_coder_api_url("https://evil.example"),
            "https://coder.vigthoria.io"
        );
    }

    #[test]
    fn bundled_cli_status_never_claims_a_missing_resource() {
        let status = bundled_cli_status(None);
        assert_eq!(status.version, BUNDLED_CLI_VERSION);
        assert!(!status.available);
        assert!(status.package_path.is_none());
    }
}
