a3s 0.8.2

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use std::sync::Arc;

use a3s_boot::{BootError, Result as BootResult};
use a3s_code_core::config::OsConfig;
use serde_json::{json, Value};

use super::controller::OsTokenLoginRequest;
use crate::api::code_web::session_runtime::{code_web_os_status, rebuild_code_web_sessions};
use crate::api::code_web::state::CodeWebState;

pub(in crate::api::code_web) struct OsService {
    state: Arc<CodeWebState>,
}

impl OsService {
    pub(in crate::api::code_web) fn new(state: Arc<CodeWebState>) -> Self {
        Self { state }
    }

    pub(in crate::api::code_web) async fn account(&self) -> BootResult<Value> {
        code_web_os_status(self.state.as_ref()).await
    }

    pub(in crate::api::code_web) async fn login_with_token(
        &self,
        request: OsTokenLoginRequest,
    ) -> BootResult<Value> {
        let config = self.os_config()?;
        let token = request.token.trim();
        if token.is_empty() {
            return Err(BootError::BadRequest("token is required".to_string()));
        }
        let session = crate::a3s_os::login_with_token(&config, token)
            .await
            .map_err(|error| BootError::BadRequest(error.to_string()))?;
        self.finish_login(&config, session, "token").await
    }

    pub(in crate::api::code_web) async fn login_with_browser(&self) -> BootResult<Value> {
        let config = self.os_config()?;
        let session = crate::a3s_os::login_via_browser(config.clone())
            .await
            .map_err(|error| BootError::BadRequest(error.to_string()))?;
        self.finish_login(&config, session, "browser").await
    }

    pub(in crate::api::code_web) async fn logout(&self) -> BootResult<Value> {
        let config = self.os_config()?;
        let removed = crate::a3s_os::logout(&config)
            .map_err(|error| BootError::Internal(error.to_string()))?;
        crate::a3s_os::clear_os_env();
        crate::a3s_os::remove_capability_skill_dir();
        let rebuilt_sessions = rebuild_code_web_sessions(self.state.as_ref()).await?;
        let mut response = code_web_os_status(self.state.as_ref()).await?;
        insert_extra(
            &mut response,
            json!({
                "operation": "logout",
                "removed": removed,
                "rebuiltSessions": rebuilt_sessions,
            }),
        );
        Ok(response)
    }

    async fn finish_login(
        &self,
        config: &OsConfig,
        session: crate::a3s_os::StoredOsSession,
        method: &str,
    ) -> BootResult<Value> {
        crate::a3s_os::export_os_env(&session);
        let capability_skill_active = crate::a3s_os::ensure_capability_skill_dir(config).is_some();
        let ssh_key = ssh_key_outcome_json(crate::a3s_os::sync_ssh_key(session.clone()).await);
        let rebuilt_sessions = rebuild_code_web_sessions(self.state.as_ref()).await?;
        let mut response = code_web_os_status(self.state.as_ref()).await?;
        insert_extra(
            &mut response,
            json!({
                "operation": "login",
                "method": method,
                "capabilitySkillPrepared": capability_skill_active,
                "sshKey": ssh_key,
                "rebuiltSessions": rebuilt_sessions,
            }),
        );
        Ok(response)
    }

    fn os_config(&self) -> BootResult<OsConfig> {
        self.state
            .code_config_snapshot()
            .os
            .ok_or_else(|| BootError::BadRequest("OS endpoint is not configured".to_string()))
    }
}

fn insert_extra(target: &mut Value, extra: Value) {
    let Some(target) = target.as_object_mut() else {
        return;
    };
    let Some(extra) = extra.as_object() else {
        return;
    };
    for (key, value) in extra {
        target.insert(key.clone(), value.clone());
    }
}

fn ssh_key_outcome_json(outcome: crate::a3s_os::SshKeyOutcome) -> Value {
    match outcome {
        crate::a3s_os::SshKeyOutcome::Registered(fingerprint) => json!({
            "status": "registered",
            "fingerprint": fingerprint,
        }),
        crate::a3s_os::SshKeyOutcome::AlreadyRegistered => json!({
            "status": "alreadyRegistered",
        }),
        crate::a3s_os::SshKeyOutcome::NoLocalKey => json!({
            "status": "noLocalKey",
        }),
        crate::a3s_os::SshKeyOutcome::Failed(error) => json!({
            "status": "failed",
            "error": error,
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ssh_key_outcome_status_is_stable() {
        assert_eq!(
            ssh_key_outcome_json(crate::a3s_os::SshKeyOutcome::AlreadyRegistered)["status"],
            "alreadyRegistered"
        );
        assert_eq!(
            ssh_key_outcome_json(crate::a3s_os::SshKeyOutcome::NoLocalKey)["status"],
            "noLocalKey"
        );
    }
}