use std::collections::VecDeque;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;

use chrono::Utc;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use worker_runtime::auth::{RuntimeIdentityMaterial, decode_public_key};
use yoi_workspace_server::hosts::{RemoteRuntimeAuthConfig, RemoteRuntimeConfig};
use yoi_workspace_server::store::{SqliteWorkspaceStore, TrustedRuntimeRecord};
use yoi_workspace_server::{
    BackendRuntimesConfigFile, ControlPlaneStore, InitialRepositoryIntent, ServerConfig,
    WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, WorkspaceCatalogService,
    WorkspaceCreateRequest, WorkspaceIdentity, WorkspaceRecord, serve_workspace_catalog,
};

#[derive(Debug)]
enum Command {
    Serve(ServeOptions),
    Init(InitOptions),
    ConfigDefault,
    ConfigDiff(WorkspacePathOptions),
    Identity(Vec<String>),
    TrustRuntime(Vec<String>),
    MigrateDryRun { database: Option<PathBuf> },
    Skills(SkillsCommand),
    Help,
}

#[derive(Debug)]
struct ServeOptions {
    listen: Option<SocketAddr>,
}

#[derive(Debug)]
struct InitOptions {
    workspace: PathBuf,
}

#[derive(Debug)]
struct WorkspacePathOptions {
    workspace: PathBuf,
}

#[derive(Debug)]
struct SkillWorkspaceOptions {
    workspace_id: String,
}

#[derive(Debug)]
enum SkillsCommand {
    List(SkillWorkspaceOptions),
    Lint(SkillWorkspaceOptions),
    Show { workspace_id: String, name: String },
}

#[derive(Debug)]
struct CliError(String);

impl std::fmt::Display for CliError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl std::error::Error for CliError {}

#[tokio::main]
async fn main() -> ExitCode {
    match run().await {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("yoi-server: {error}");
            ExitCode::FAILURE
        }
    }
}

async fn run() -> Result<(), Box<dyn std::error::Error>> {
    let args = std::env::args().skip(1).collect::<Vec<_>>();
    match parse_command(&args)? {
        Command::Serve(options) => run_serve(options).await,
        Command::Init(options) => run_init(options).await,
        Command::ConfigDefault => run_config_default(),
        Command::ConfigDiff(options) => run_config_diff(options),
        Command::Identity(args) => run_identity_command(args),
        Command::TrustRuntime(args) => run_trust_runtime_command(args),
        Command::MigrateDryRun { database } => {
            let database = database.unwrap_or_else(ServerConfig::default_server_database_path);
            let plan = SqliteWorkspaceStore::migration_plan(&database).map_err(|error| {
                CliError(format!(
                    "migration dry-run failed for {}: {error}",
                    database.display()
                ))
            })?;
            println!("{}", serde_json::to_string_pretty(&plan)?);
            Ok(())
        }
        Command::Skills(command) => run_skills(command),
        Command::Help => Ok(()),
    }
}

fn parse_command(args: &[String]) -> Result<Command, CliError> {
    let Some((command, rest)) = args.split_first() else {
        print_help();
        return Ok(Command::Help);
    };

    match command.as_str() {
        "init" => {
            if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
                print_init_help();
                return Ok(Command::Help);
            }
            Ok(Command::Init(parse_init_options(rest)?))
        }
        "config" => parse_config_command(rest),
        "identity" => Ok(Command::Identity(rest.to_vec())),
        "trust-runtime" => Ok(Command::TrustRuntime(rest.to_vec())),
        "migrate" => parse_migrate_command(rest),
        "skills" => parse_skills_command(rest),
        "serve" => {
            if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
                print_serve_help();
                return Ok(Command::Help);
            }
            Ok(Command::Serve(parse_serve_options(rest)?))
        }
        "--help" | "-h" => {
            print_help();
            Ok(Command::Help)
        }
        other => Err(CliError(format!(
            "unknown command `{other}`; expected `init`, `config`, `identity`, `trust-runtime`, `migrate`, `skills`, or `serve`"
        ))),
    }
}

async fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
    run_init_with_database_path(options, ServerConfig::default_server_database_path()).await
}

async fn run_init_with_database_path(
    options: InitOptions,
    database_path: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
    let identity = WorkspaceIdentity::load_or_init(&options.workspace)?;
    WorkspaceBackendConfigFile::ensure_local_config_for_workspace(&options.workspace)?;

    if let Some(parent) = database_path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }
    let store = Arc::new(SqliteWorkspaceStore::open(&database_path)?);
    let service = WorkspaceCatalogService::new(store);
    service.create_with_workspace_id(
        WorkspaceCreateRequest {
            operation_key: format!("cli-init:{}", identity.workspace_id),
            display_name: identity.display_name.clone(),
            repository: InitialRepositoryIntent {
                uri: options.workspace.display().to_string(),
                display_name: Some("Main repository".to_string()),
                default_ref: Some("HEAD".to_string()),
            },
        },
        None,
        Some(identity.workspace_id.clone()),
    )?;

    eprintln!(
        "yoi-server: initialized workspace `{}` ({}) in server DB `{}`",
        options.workspace.display(),
        identity.workspace_id,
        database_path.display()
    );
    Ok(())
}

fn run_config_default() -> Result<(), Box<dyn std::error::Error>> {
    print!("{WORKSPACE_BACKEND_CONFIG_TEMPLATE}");
    Ok(())
}

fn run_config_diff(options: WorkspacePathOptions) -> Result<(), Box<dyn std::error::Error>> {
    let diff = WorkspaceBackendConfigFile::local_config_diff_for_workspace(&options.workspace)?;
    print!("{}", diff.text);
    Ok(())
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct ServerIdentityFile {
    identity: RuntimeIdentityMaterial,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct PublicIdentityView {
    identity_id: String,
    public_key: String,
}

fn server_identity_path() -> PathBuf {
    ServerConfig::default_server_data_root().join("identity.toml")
}

fn read_server_identity_file(
    path: &Path,
) -> Result<Option<ServerIdentityFile>, Box<dyn std::error::Error>> {
    if !path.exists() {
        return Ok(None);
    }
    let contents = std::fs::read_to_string(path)?;
    Ok(Some(toml::from_str(&contents)?))
}

fn write_server_identity_file(
    path: &Path,
    identity: &ServerIdentityFile,
) -> Result<(), Box<dyn std::error::Error>> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let contents = toml::to_string_pretty(identity)?;
    write_secret_file(path, contents.as_bytes())?;
    Ok(())
}

fn write_secret_file(path: &Path, contents: &[u8]) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .mode(0o600)
            .open(path)?;
        use std::io::Write as _;
        file.write_all(contents)?;
    }
    #[cfg(not(unix))]
    {
        std::fs::write(path, contents)?;
    }
    Ok(())
}

fn public_identity_view(identity: &RuntimeIdentityMaterial) -> PublicIdentityView {
    PublicIdentityView {
        identity_id: identity.identity_id.clone(),
        public_key: identity.public_key.clone(),
    }
}

fn run_identity_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    let mut args = VecDeque::from(args);
    let subcommand = args
        .pop_front()
        .ok_or_else(|| CliError("identity requires `init` or `show`".to_string()))?;
    match subcommand.as_str() {
        "init" => {
            let mut server_id = None;
            let mut replace = false;
            while let Some(arg) = args.pop_front() {
                let (flag, inline_value) = split_flag_value(arg)?;
                match flag.as_str() {
                    "--server-id" => server_id = Some(take_value(&flag, inline_value, &mut args)?),
                    "--replace" => {
                        ensure_no_inline_value(&flag, inline_value.as_deref())?;
                        replace = true;
                    }
                    _ => {
                        return Err(Box::new(CliError(format!(
                            "unknown identity init argument `{flag}`"
                        ))));
                    }
                }
            }
            let server_id = server_id
                .ok_or_else(|| CliError("identity init requires --server-id".to_string()))?;
            let path = server_identity_path();
            if read_server_identity_file(&path)?.is_some() && !replace {
                return Err(Box::new(CliError(format!(
                    "server identity already exists at {}; pass --replace to rotate it",
                    path.display()
                ))));
            }
            let identity = RuntimeIdentityMaterial::generate(server_id)?;
            write_server_identity_file(
                &path,
                &ServerIdentityFile {
                    identity: identity.clone(),
                },
            )?;
            println!("server_id={}", identity.identity_id);
            println!("public_key={}", identity.public_key);
            println!("identity_file={}", path.display());
            Ok(())
        }
        "show" => {
            let mut json = false;
            while let Some(arg) = args.pop_front() {
                let (flag, inline_value) = split_flag_value(arg)?;
                match flag.as_str() {
                    "--json" => {
                        ensure_no_inline_value(&flag, inline_value.as_deref())?;
                        json = true;
                    }
                    _ => {
                        return Err(Box::new(CliError(format!(
                            "unknown identity show argument `{flag}`"
                        ))));
                    }
                }
            }
            let path = server_identity_path();
            let identity = read_server_identity_file(&path)?.ok_or_else(|| {
                CliError(format!(
                    "server identity is not initialized at {}",
                    path.display()
                ))
            })?;
            let view = public_identity_view(&identity.identity);
            if json {
                println!("{}", serde_json::to_string_pretty(&view)?);
            } else {
                println!("server_id={}", view.identity_id);
                println!("public_key={}", view.public_key);
                println!("identity_file={}", path.display());
            }
            Ok(())
        }
        _ => Err(Box::new(CliError(format!(
            "unknown identity subcommand `{subcommand}`"
        )))),
    }
}

fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    let mut args = VecDeque::from(args);
    let subcommand = args
        .pop_front()
        .ok_or_else(|| CliError("trust-runtime requires `add`, `list`, or `revoke`".to_string()))?;
    let database_path = ServerConfig::default_server_database_path();
    if let Some(parent) = database_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let store = SqliteWorkspaceStore::open(&database_path)?;
    match subcommand.as_str() {
        "add" => {
            let mut runtime_id = None;
            let mut workspace_id = None;
            let mut base_url = None;
            let mut public_key = None;
            let mut display_name = None;
            let mut replace = false;
            while let Some(arg) = args.pop_front() {
                let (flag, inline_value) = split_flag_value(arg)?;
                match flag.as_str() {
                    "--runtime-id" => {
                        runtime_id = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    "--workspace-id" => {
                        workspace_id = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    "--base-url" | "--endpoint" => {
                        base_url = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    "--public-key" => {
                        public_key = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    "--display-name" => {
                        display_name = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    "--replace" => {
                        ensure_no_inline_value(&flag, inline_value.as_deref())?;
                        replace = true;
                    }
                    _ => {
                        return Err(Box::new(CliError(format!(
                            "unknown trust-runtime add argument `{flag}`"
                        ))));
                    }
                }
            }
            let runtime_id = runtime_id
                .ok_or_else(|| CliError("trust-runtime add requires --runtime-id".to_string()))?;
            let workspace_id = workspace_id
                .ok_or_else(|| CliError("trust-runtime add requires --workspace-id".to_string()))?;
            if !store
                .list_workspaces()?
                .iter()
                .any(|workspace| workspace.workspace_id == workspace_id)
            {
                return Err(Box::new(CliError(format!(
                    "Workspace `{workspace_id}` is not registered"
                ))));
            }
            let base_url = base_url
                .ok_or_else(|| CliError("trust-runtime add requires --base-url".to_string()))?;
            let public_key = public_key
                .ok_or_else(|| CliError("trust-runtime add requires --public-key".to_string()))?;
            decode_public_key(&public_key)?;
            ensure_trusted_runtime_replace_allowed(&store, &runtime_id, replace)?;
            if let Some(existing) = store
                .list_trusted_runtimes(true)?
                .into_iter()
                .find(|runtime| runtime.runtime_id == runtime_id)
            {
                if existing.workspace_id.as_deref() != Some(workspace_id.as_str()) {
                    return Err(Box::new(CliError(format!(
                        "runtime `{runtime_id}` is already assigned to Workspace `{}` and cannot be reparented",
                        existing.workspace_id.as_deref().unwrap_or("unassigned")
                    ))));
                }
            }
            let now = Utc::now().to_rfc3339();
            store.upsert_trusted_runtime(&TrustedRuntimeRecord {
                runtime_id: runtime_id.clone(),
                workspace_id: Some(workspace_id.clone()),
                display_name: display_name.unwrap_or_else(|| runtime_id.clone()),
                base_url,
                public_key,
                created_at: now.clone(),
                updated_at: now,
                revoked_at: None,
            })?;
            println!("trusted_runtime_id={runtime_id}");
            println!("server_db={}", database_path.display());
            Ok(())
        }
        "list" => {
            let mut json = false;
            let mut include_revoked = false;
            while let Some(arg) = args.pop_front() {
                let (flag, inline_value) = split_flag_value(arg)?;
                match flag.as_str() {
                    "--json" => {
                        ensure_no_inline_value(&flag, inline_value.as_deref())?;
                        json = true;
                    }
                    "--include-revoked" => {
                        ensure_no_inline_value(&flag, inline_value.as_deref())?;
                        include_revoked = true;
                    }
                    _ => {
                        return Err(Box::new(CliError(format!(
                            "unknown trust-runtime list argument `{flag}`"
                        ))));
                    }
                }
            }
            let records = store.list_trusted_runtimes(include_revoked)?;
            if json {
                println!("{}", serde_json::to_string_pretty(&records)?);
            } else {
                for runtime in records {
                    println!(
                        "runtime_id={} workspace_id={} base_url={} public_key={} revoked_at={}",
                        runtime.runtime_id,
                        runtime.workspace_id.unwrap_or_default(),
                        runtime.base_url,
                        runtime.public_key,
                        runtime.revoked_at.unwrap_or_default()
                    );
                }
            }
            Ok(())
        }
        "revoke" => {
            let mut runtime_id = None;
            while let Some(arg) = args.pop_front() {
                let (flag, inline_value) = split_flag_value(arg)?;
                match flag.as_str() {
                    "--runtime-id" => {
                        runtime_id = Some(take_value(&flag, inline_value, &mut args)?)
                    }
                    _ => {
                        return Err(Box::new(CliError(format!(
                            "unknown trust-runtime revoke argument `{flag}`"
                        ))));
                    }
                }
            }
            let runtime_id = runtime_id.ok_or_else(|| {
                CliError("trust-runtime revoke requires --runtime-id".to_string())
            })?;
            let now = Utc::now().to_rfc3339();
            if !store.revoke_trusted_runtime(&runtime_id, &now)? {
                return Err(Box::new(CliError(format!(
                    "trusted runtime `{runtime_id}` is not registered or is already revoked"
                ))));
            }
            println!("revoked_runtime_id={runtime_id}");
            Ok(())
        }
        _ => Err(Box::new(CliError(format!(
            "unknown trust-runtime subcommand `{subcommand}`"
        )))),
    }
}

fn ensure_trusted_runtime_replace_allowed(
    store: &SqliteWorkspaceStore,
    runtime_id: &str,
    replace: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    if store
        .list_trusted_runtimes(true)?
        .iter()
        .any(|runtime| runtime.runtime_id == runtime_id)
        && !replace
    {
        return Err(Box::new(CliError(format!(
            "trusted runtime `{runtime_id}` already exists; pass --replace to update it"
        ))));
    }
    Ok(())
}

fn split_flag_value(arg: String) -> Result<(String, Option<String>), CliError> {
    if let Some((flag, value)) = arg.split_once('=') {
        if flag.is_empty() {
            return Err(CliError("empty flag name".to_string()));
        }
        Ok((flag.to_string(), Some(value.to_string())))
    } else {
        Ok((arg, None))
    }
}

fn take_value(
    flag: &str,
    inline_value: Option<String>,
    args: &mut VecDeque<String>,
) -> Result<String, CliError> {
    if let Some(value) = inline_value {
        return Ok(value);
    }
    args.pop_front()
        .ok_or_else(|| CliError(format!("{flag} requires a value")))
}

fn ensure_no_inline_value(flag: &str, inline_value: Option<&str>) -> Result<(), CliError> {
    if inline_value.is_some() {
        return Err(CliError(format!("{flag} does not accept a value")));
    }
    Ok(())
}

fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        SkillsCommand::List(options) => {
            let state = load_skill_workspace_config(&options.workspace_id)?;
            let catalog = yoi_workspace_server::skills::catalog(&state)?;
            println!("{}", serde_json::to_string_pretty(&catalog)?);
        }
        SkillsCommand::Lint(options) => {
            let state = load_skill_workspace_config(&options.workspace_id)?;
            let catalog = yoi_workspace_server::skills::lint(&state)?;
            println!("{}", serde_json::to_string_pretty(&catalog)?);
            if catalog
                .diagnostics
                .iter()
                .chain(
                    catalog
                        .entries
                        .iter()
                        .flat_map(|entry| entry.diagnostics.iter()),
                )
                .any(|diagnostic| {
                    diagnostic.severity == worker::skill::SkillDiagnosticSeverity::Error
                })
            {
                return Err(Box::new(CliError("Skill lint found errors".to_string())));
            }
        }
        SkillsCommand::Show { workspace_id, name } => {
            let state = load_skill_workspace_config(&workspace_id)?;
            let detail = yoi_workspace_server::skills::detail(&state, &name)?;
            println!("{}", serde_json::to_string_pretty(&detail)?);
        }
    }
    Ok(())
}

fn load_skill_workspace_config(
    workspace_id: &str,
) -> Result<yoi_workspace_server::config_source::WorkspaceConfigState, Box<dyn std::error::Error>> {
    let store = SqliteWorkspaceStore::open(ServerConfig::default_server_database_path())?;
    store.load_workspace_config(workspace_id)?.ok_or_else(|| {
        Box::new(CliError(format!(
            "Workspace `{workspace_id}` has no active config revision"
        ))) as Box<dyn std::error::Error>
    })
}

async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Error>> {
    let database_path = ServerConfig::default_server_database_path();
    if let Some(parent) = database_path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }

    let store = Arc::new(SqliteWorkspaceStore::open(&database_path)?);
    let workspaces = store.list_workspaces()?;
    let (identity, workspace_root) = if let Some(workspace) = workspaces.first() {
        (
            WorkspaceIdentity {
                workspace_id: workspace.workspace_id.clone(),
                created_at: workspace.created_at.clone(),
                display_name: workspace.display_name.clone(),
            },
            infer_workspace_root_from_repositories(store.as_ref(), workspace)?,
        )
    } else {
        (
            WorkspaceIdentity {
                workspace_id: "00000000-0000-0000-0000-000000000000".to_string(),
                created_at: Utc::now().to_rfc3339(),
                display_name: "Server bootstrap".to_string(),
            },
            database_path
                .parent()
                .ok_or_else(|| CliError("server database path has no parent".to_string()))?
                .to_path_buf(),
        )
    };
    let runtime_config = BackendRuntimesConfigFile::load_default()?;
    let mut resolved = WorkspaceBackendConfigFile::default().resolve_with_runtime_config(
        &workspace_root,
        identity,
        &runtime_config,
    )?;
    resolved.database_path = database_path.clone();
    resolved.server.database_path = database_path.clone();
    append_trusted_runtime_sources(store.as_ref(), &mut resolved.server.remote_runtime_sources)?;
    if let Some(listen) = options.listen {
        resolved = resolved.with_listen(listen);
    }
    resolved.server.allow_local_workspace_bootstrap = resolved.listen.ip().is_loopback();

    let listener = TcpListener::bind(resolved.listen).await?;
    let local_addr = listener.local_addr()?;
    if resolved.server.backend_base_url.is_none() {
        resolved = resolved.with_backend_base_url(format!("http://{local_addr}"));
    }
    eprintln!(
        "yoi-server: serving {} workspace(s) from server DB `{}` on http://{}",
        workspaces.len(),
        database_path.display(),
        local_addr
    );
    serve_workspace_catalog(resolved.server, store, listener).await?;
    Ok(())
}

fn append_trusted_runtime_sources(
    store: &SqliteWorkspaceStore,
    remote_runtime_sources: &mut Vec<RemoteRuntimeConfig>,
) -> Result<(), Box<dyn std::error::Error>> {
    let Some(server_identity) = read_server_identity_file(&server_identity_path())? else {
        if !store.list_trusted_runtimes(false)?.is_empty() {
            return Err(Box::new(CliError(
                "trusted runtimes are registered but server identity is not initialized; run `yoi-server identity init`".to_string(),
            )));
        }
        return Ok(());
    };
    for runtime in store.list_trusted_runtimes(false)? {
        let Some(workspace_id) = runtime.workspace_id.clone() else {
            continue;
        };
        let auth = RemoteRuntimeAuthConfig {
            server_id: server_identity.identity.identity_id.clone(),
            server_private_key: server_identity.identity.private_key.clone(),
        };
        let remote = RemoteRuntimeConfig::new(
            runtime.runtime_id.clone(),
            runtime.display_name,
            runtime.base_url,
            None,
        )
        .with_workspace_id(workspace_id)
        .with_auth(auth);
        remote_runtime_sources.retain(|existing| existing.runtime_id != runtime.runtime_id);
        remote_runtime_sources.push(remote);
    }
    Ok(())
}

fn infer_workspace_root_from_repositories(
    store: &SqliteWorkspaceStore,
    workspace: &WorkspaceRecord,
) -> Result<PathBuf, CliError> {
    let repositories = store
        .list_repositories(&workspace.workspace_id)
        .map_err(|error| {
            CliError(format!(
                "failed to list repositories from server DB: {error}"
            ))
        })?;
    let Some(repository) = repositories
        .iter()
        .find(|repository| repository.repository_id == "main")
        .or_else(|| repositories.first())
    else {
        return Err(CliError(format!(
            "workspace `{}` has no repository records; cannot derive a workspace root",
            workspace.workspace_id
        )));
    };

    if repository.source.kind == workspace_api::RepositorySourceKind::Invalid {
        return Err(CliError(format!(
            "repository `{}` has an invalid migrated source and cannot be used by serve",
            repository.repository_id
        )));
    }
    Ok(ServerConfig::default_workspace_backend_data_root(
        &workspace.workspace_id,
    ))
}

fn parse_config_command(args: &[String]) -> Result<Command, CliError> {
    let Some((subcommand, rest)) = args.split_first() else {
        print_config_help();
        return Ok(Command::Help);
    };
    match subcommand.as_str() {
        "default" => {
            if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
                print_config_help();
                return Ok(Command::Help);
            }
            if !rest.is_empty() {
                return Err(CliError(
                    "config default does not accept options".to_string(),
                ));
            }
            Ok(Command::ConfigDefault)
        }
        "diff" => {
            if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
                print_config_help();
                return Ok(Command::Help);
            }
            Ok(Command::ConfigDiff(parse_workspace_path_options(rest)?))
        }
        "--help" | "-h" => {
            print_config_help();
            Ok(Command::Help)
        }
        other => Err(CliError(format!(
            "unknown config subcommand `{other}`; expected `default` or `diff`"
        ))),
    }
}

fn parse_migrate_command(args: &[String]) -> Result<Command, CliError> {
    let mut dry_run = false;
    let mut database = None;
    let mut index = 0;
    while index < args.len() {
        match args[index].as_str() {
            "--dry-run" => dry_run = true,
            "--database" => {
                index += 1;
                database =
                    Some(PathBuf::from(args.get(index).ok_or_else(|| {
                        CliError("--database requires a path".to_string())
                    })?));
            }
            value => {
                return Err(CliError(format!("unknown migrate option: {value}")));
            }
        }
        index += 1;
    }
    if !dry_run {
        return Err(CliError("migrate currently requires --dry-run".to_string()));
    }
    Ok(Command::MigrateDryRun { database })
}

fn parse_skills_command(args: &[String]) -> Result<Command, CliError> {
    let Some((subcommand, rest)) = args.split_first() else {
        print_skills_help();
        return Ok(Command::Help);
    };
    match subcommand.as_str() {
        "list" => Ok(Command::Skills(SkillsCommand::List(
            parse_skill_workspace_options(rest)?,
        ))),
        "lint" => Ok(Command::Skills(SkillsCommand::Lint(
            parse_skill_workspace_options(rest)?,
        ))),
        "show" => {
            let Some((name, rest)) = rest.split_first() else {
                return Err(CliError("skills show requires a Skill name".to_string()));
            };
            Ok(Command::Skills(SkillsCommand::Show {
                workspace_id: parse_skill_workspace_options(rest)?.workspace_id,
                name: name.to_string(),
            }))
        }
        "--help" | "-h" => {
            print_skills_help();
            Ok(Command::Help)
        }
        other => Err(CliError(format!(
            "unknown skills subcommand `{other}`; expected `list`, `lint`, or `show`"
        ))),
    }
}

fn parse_skill_workspace_options(args: &[String]) -> Result<SkillWorkspaceOptions, CliError> {
    let mut workspace_id = None;
    let mut iter = args.iter();
    while let Some(arg) = iter.next() {
        match arg.as_str() {
            "--workspace" => {
                let value = iter
                    .next()
                    .ok_or_else(|| CliError("--workspace requires a Workspace id".to_string()))?;
                workspace_id = Some(value.clone());
            }
            value if value.starts_with("--workspace=") => {
                workspace_id = Some(value_after_equals(arg, "--workspace")?.to_string());
            }
            other => return Err(CliError(format!("unknown skills option `{other}`"))),
        }
    }
    let workspace_id = workspace_id.ok_or_else(|| {
        CliError("skills commands require --workspace <workspace-id>".to_string())
    })?;
    if workspace_id.trim().is_empty() {
        return Err(CliError("--workspace must not be empty".to_string()));
    }
    Ok(SkillWorkspaceOptions { workspace_id })
}

fn parse_workspace_path_options(args: &[String]) -> Result<WorkspacePathOptions, CliError> {
    let mut workspace = std::env::current_dir()
        .map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
    let mut iter = args.iter();
    while let Some(arg) = iter.next() {
        match arg.as_str() {
            "--workspace" => {
                let value = iter
                    .next()
                    .ok_or_else(|| CliError("--workspace requires a path".to_string()))?;
                workspace = PathBuf::from(value);
            }
            value if value.starts_with("--workspace=") => {
                workspace = PathBuf::from(value_after_equals(arg, "--workspace")?);
            }
            other => return Err(CliError(format!("unknown workspace option `{other}`"))),
        }
    }
    let workspace = workspace
        .canonicalize()
        .map_err(|error| CliError(format!("failed to canonicalize workspace: {error}")))?;
    Ok(WorkspacePathOptions { workspace })
}

fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
    let mut workspace = std::env::current_dir()
        .map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
    let mut iter = args.iter();
    while let Some(arg) = iter.next() {
        match arg.as_str() {
            "--workspace" => {
                let value = iter
                    .next()
                    .ok_or_else(|| CliError("--workspace requires a path".to_string()))?;
                workspace = PathBuf::from(value);
            }
            value if value.starts_with("--workspace=") => {
                workspace = PathBuf::from(value_after_equals(arg, "--workspace")?);
            }
            other => return Err(CliError(format!("unknown init option `{other}`"))),
        }
    }

    let workspace = workspace
        .canonicalize()
        .map_err(|error| CliError(format!("failed to canonicalize workspace: {error}")))?;
    Ok(InitOptions { workspace })
}

fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
    let mut listen = None;

    let mut index = 0;
    while index < args.len() {
        let arg = &args[index];
        match arg.as_str() {
            "--listen" => {
                index += 1;
                let value = args
                    .get(index)
                    .ok_or_else(|| CliError("--listen requires a value".to_string()))?;
                listen = Some(parse_listen(value)?);
            }
            _ if arg.starts_with("--listen=") => {
                listen = Some(parse_listen(value_after_equals(arg, "--listen")?)?);
            }
            _ if arg.starts_with('-') => {
                return Err(CliError(format!("unknown serve option `{arg}`")));
            }
            _ => {
                return Err(CliError(format!(
                    "unexpected positional argument `{arg}`; serve reads the workspace from the server DB"
                )));
            }
        }
        index += 1;
    }

    Ok(ServeOptions { listen })
}

fn value_after_equals<'a>(arg: &'a str, flag: &str) -> Result<&'a str, CliError> {
    let value = arg
        .strip_prefix(flag)
        .and_then(|rest| rest.strip_prefix('='))
        .unwrap_or_default();
    if value.is_empty() {
        return Err(CliError(format!("{flag} requires a value")));
    }
    Ok(value)
}

fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
    value
        .parse()
        .map_err(|_| CliError(format!("invalid --listen address `{value}`")))
}

fn print_help() {
    println!(
        "yoi-server\n\nUsage:\n  yoi-server init [OPTIONS]\n  yoi-server config <COMMAND> [OPTIONS]\n  yoi-server identity init --server-id <SERVER_ID> [--replace]\n  yoi-server identity show [--json]\n  yoi-server trust-runtime add --runtime-id <RUNTIME_ID> --workspace-id <WORKSPACE_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n  yoi-server trust-runtime list [--json] [--include-revoked]\n  yoi-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n  yoi-server skills <COMMAND> [OPTIONS]\n  yoi-server migrate --dry-run [--database <PATH>]
  yoi-server serve [OPTIONS]\n\nOptions:\n  -h, --help    Print help"
    );
}

fn print_init_help() {
    println!(
        "yoi-server init\n\nUsage:\n  yoi-server init [OPTIONS]\n\nDescription:\n  Initializes a Workspace identity, copies the packaged Backend config template to .yoi/workspace-backend.local.toml, and registers the Workspace in the Yoi server DB.\n\nOptions:\n      --workspace <PATH>  Workspace root to initialize (defaults to cwd)\n  -h, --help              Print help"
    );
}

fn print_config_help() {
    println!(
        "yoi-server config\n\nUsage:\n  yoi-server config default\n  yoi-server config diff [OPTIONS]\n\nDescription:\n  Prints the packaged Workspace Backend config template or compares it with the workspace-local config.\n\nOptions for diff:\n      --workspace <PATH>  Workspace root (defaults to cwd)\n  -h, --help              Print help"
    );
}

fn print_skills_help() {
    println!(
        "yoi-server skills\n\nUsage:\n  yoi-server skills list --workspace <WORKSPACE_ID>\n  yoi-server skills lint --workspace <WORKSPACE_ID>\n  yoi-server skills show <NAME> --workspace <WORKSPACE_ID>\n\nDescription:\n  Reads the active Server DB virtual-config revision. Catalog output is lightweight and omits imported Markdown content; detail output includes that content. allowed-tools and scripts are diagnostics only.\n\nOptions:\n      --workspace <WORKSPACE_ID>  Workspace id in the Server DB (required)\n  -h, --help                      Print help"
    );
}

fn print_serve_help() {
    println!(
        "yoi-server serve\n\nUsage:\n  yoi-server migrate --dry-run [--database <PATH>]
  yoi-server serve [OPTIONS]\n\nDescription:\n  Serves the Workspace recorded in the Yoi server DB. Workspace records are stored in the XDG/Yoi data directory, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n      --listen <ADDR>     Listen address (default 127.0.0.1:8787)\n  -h, --help              Print help"
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use yoi_workspace_server::{
        WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH, WORKSPACE_BACKEND_CONFIG_TEMPLATE,
        WORKSPACE_IDENTITY_RELATIVE_PATH,
    };

    #[test]
    fn parse_init_defaults_workspace_to_cwd_or_flag() {
        let temp = tempfile::tempdir().unwrap();
        let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
        let options = parse_init_options(&args).unwrap();
        assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
    }

    #[test]
    fn parse_skills_requires_server_workspace_id() {
        let error = parse_skills_command(&["list".to_string()]).unwrap_err();
        assert_eq!(
            error.to_string(),
            "skills commands require --workspace <workspace-id>"
        );
        let command = parse_skills_command(&[
            "show".to_string(),
            "debug-rust".to_string(),
            "--workspace=workspace-a".to_string(),
        ])
        .unwrap();
        let Command::Skills(SkillsCommand::Show { workspace_id, name }) = command else {
            panic!("expected skills show command");
        };
        assert_eq!(workspace_id, "workspace-a");
        assert_eq!(name, "debug-rust");
    }

    #[test]
    fn parse_migrate_requires_dry_run_and_accepts_database_path() {
        let error = parse_migrate_command(&[]).unwrap_err();
        assert_eq!(error.to_string(), "migrate currently requires --dry-run");
        let command = parse_migrate_command(&[
            "--dry-run".to_string(),
            "--database".to_string(),
            "/tmp/server.db".to_string(),
        ])
        .unwrap();
        let Command::MigrateDryRun { database } = command else {
            panic!("expected migration dry-run command");
        };
        assert_eq!(database, Some(PathBuf::from("/tmp/server.db")));
    }

    #[test]
    fn parse_serve_accepts_listen_only() {
        let args = vec!["--listen".to_string(), "127.0.0.1:0".to_string()];
        let options = parse_serve_options(&args).unwrap();
        assert_eq!(options.listen.unwrap(), "127.0.0.1:0".parse().unwrap());
    }

    #[test]
    fn parse_serve_rejects_legacy_workspace_flag() {
        let temp = tempfile::tempdir().unwrap();
        let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
        let error = parse_serve_options(&args).unwrap_err();
        assert_eq!(error.to_string(), "unknown serve option `--workspace`");
    }

    #[test]
    fn parse_serve_rejects_legacy_db_and_frontend_flags() {
        let error = parse_serve_options(&["--db=/tmp/yoi.db".to_string()]).unwrap_err();
        assert_eq!(error.to_string(), "unknown serve option `--db=/tmp/yoi.db`");
        let error = parse_serve_options(&["--frontend=/tmp/web".to_string()]).unwrap_err();
        assert_eq!(
            error.to_string(),
            "unknown serve option `--frontend=/tmp/web`"
        );
    }

    #[test]
    fn server_identity_init_requires_explicit_server_id() {
        let error = run_identity_command(vec!["init".to_string()]).unwrap_err();
        assert_eq!(error.to_string(), "identity init requires --server-id");
    }

    #[test]
    fn trusted_runtime_add_requires_replace_for_existing_record() {
        let temp = tempfile::tempdir().unwrap();
        let store = SqliteWorkspaceStore::open(temp.path().join("server.db")).unwrap();
        let public_key = RuntimeIdentityMaterial::generate("runtime-a")
            .unwrap()
            .public_key;
        store
            .upsert_trusted_runtime(&TrustedRuntimeRecord {
                runtime_id: "runtime-a".to_string(),
                workspace_id: None,
                display_name: "Runtime A".to_string(),
                base_url: "http://127.0.0.1:18080".to_string(),
                public_key,
                created_at: "2026-07-26T00:00:00Z".to_string(),
                updated_at: "2026-07-26T00:00:00Z".to_string(),
                revoked_at: None,
            })
            .unwrap();

        let error = ensure_trusted_runtime_replace_allowed(&store, "runtime-a", false).unwrap_err();
        assert_eq!(
            error.to_string(),
            "trusted runtime `runtime-a` already exists; pass --replace to update it"
        );
        ensure_trusted_runtime_replace_allowed(&store, "runtime-a", true).unwrap();
    }

    #[tokio::test]
    async fn init_creates_identity_local_config_and_server_records() {
        let temp = tempfile::tempdir().unwrap();
        let database_path = temp.path().join("data").join("server").join("server.db");
        std::fs::create_dir(temp.path().join(".git")).unwrap();
        run_init_with_database_path(
            InitOptions {
                workspace: temp.path().canonicalize().unwrap(),
            },
            database_path.clone(),
        )
        .await
        .unwrap();

        assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists());
        let local_config_path = temp.path().join(WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH);
        assert!(local_config_path.exists());
        assert_eq!(
            std::fs::read_to_string(local_config_path).unwrap(),
            WORKSPACE_BACKEND_CONFIG_TEMPLATE
        );
        assert!(
            !temp
                .path()
                .join(".yoi/workspace-backend.default.toml")
                .exists()
        );
        assert!(!temp.path().join(".yoi/workspace.db").exists());
        assert!(!temp.path().join(".yoi/embedded-runtime").exists());
        assert!(database_path.exists());

        let store = SqliteWorkspaceStore::open(&database_path).unwrap();
        let workspaces = store.list_workspaces().unwrap();
        assert_eq!(workspaces.len(), 1);
        let repositories = store
            .list_repositories(&workspaces[0].workspace_id)
            .unwrap();
        assert_eq!(repositories.len(), 1);
        assert_eq!(repositories[0].repository_id, "main");
        assert_eq!(
            repositories[0].uri,
            temp.path().canonicalize().unwrap().display().to_string()
        );
    }
}
