//! Connector fingerprint computation
//!
//! Computes deterministic fingerprints for connectors based on their kind and
//! connection parameters. Fingerprints are used by the archivist to re-associate
//! archived data with connectors across restarts, even when connector IDs change.
//!
//! # Fingerprint Format
//!
//! Each connector kind produces fingerprints in a specific format:
//! - ACP stdio: `acp/stdio:<resolved_command_path>`
//! - ACP HTTP: `acp/http:<base_url>`
//! - OpenCode: `opencode/http:<base_url>`
//! - Gateway: `gateway:<title>`
//! - Mock / Acceptor: no fingerprint (returns `None`)

use crate::types::ConnectorKind;

/// Compute a deterministic fingerprint for a connector based on its kind and connection params.
///
/// The fingerprint captures the essential identity of a connector -- the parameters that
/// make it "the same connector" across restarts. For example, an ACP stdio connector
/// pointing to `/usr/bin/claude` will always produce the same fingerprint regardless
/// of what connector ID is assigned.
///
/// Returns `None` for Mock and Acceptor connectors, which do not have meaningful
/// persistent identity.
pub fn compute_fingerprint(kind: &ConnectorKind, params: &serde_json::Value) -> Option<String> {
    match kind {
        ConnectorKind::Acp => compute_acp_fingerprint(params),
        ConnectorKind::OpenCode => compute_opencode_fingerprint(params),
        ConnectorKind::Gateway => compute_gateway_fingerprint(params),
        ConnectorKind::Mock | ConnectorKind::Acceptor => None,
    }
}

fn compute_acp_fingerprint(params: &serde_json::Value) -> Option<String> {
    let transport = params.get("transport")?;
    let transport_type = transport.get("type")?.as_str()?;
    match transport_type {
        "stdio" => {
            let command = transport.get("command")?.as_str()?;
            let resolved = resolve_command_path(command);
            Some(format!("acp/stdio:{}", resolved))
        }
        "http" => {
            let base_url = transport.get("base_url")?.as_str()?;
            Some(format!("acp/http:{}", base_url))
        }
        _ => None,
    }
}

fn compute_opencode_fingerprint(params: &serde_json::Value) -> Option<String> {
    let base_url = params.get("base_url")?.as_str()?;
    Some(format!("opencode/http:{}", base_url))
}

fn compute_gateway_fingerprint(params: &serde_json::Value) -> Option<String> {
    let title = params
        .get("title")
        .and_then(|v| v.as_str())
        .unwrap_or("Gateway");
    Some(format!("gateway:{}", title))
}

/// Resolve a command name to its absolute path.
///
/// If the command is already an absolute path, it is returned as-is.
/// Otherwise, uses platform-specific lookup (`which` on Unix, `where` on Windows)
/// to find the full path. Falls back to the raw command string if resolution fails.
fn resolve_command_path(command: &str) -> String {
    let path = std::path::Path::new(command);
    if path.is_absolute() {
        return command.to_string();
    }

    #[cfg(unix)]
    let result = std::process::Command::new("which").arg(command).output();

    #[cfg(windows)]
    let result = std::process::Command::new("where").arg(command).output();

    match result {
        Ok(output) if output.status.success() => String::from_utf8(output.stdout)
            .ok()
            .and_then(|s| s.lines().next().map(|l| l.trim().to_string()))
            .unwrap_or_else(|| command.to_string()),
        _ => command.to_string(),
    }
}

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

    #[test]
    fn test_acp_stdio_fingerprint() {
        let params = serde_json::json!({
            "transport": {
                "type": "stdio",
                "command": "/usr/bin/claude",
                "args": ["--acp"]
            }
        });
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Acp, &params),
            Some("acp/stdio:/usr/bin/claude".to_string())
        );
    }

    #[test]
    fn test_acp_http_fingerprint() {
        let params = serde_json::json!({
            "transport": {
                "type": "http",
                "base_url": "http://localhost:3000"
            }
        });
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Acp, &params),
            Some("acp/http:http://localhost:3000".to_string())
        );
    }

    #[test]
    fn test_opencode_fingerprint() {
        let params = serde_json::json!({"base_url": "http://localhost:12225"});
        assert_eq!(
            compute_fingerprint(&ConnectorKind::OpenCode, &params),
            Some("opencode/http:http://localhost:12225".to_string())
        );
    }

    #[test]
    fn test_gateway_fingerprint_with_title() {
        let params = serde_json::json!({"title": "My Gateway"});
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Gateway, &params),
            Some("gateway:My Gateway".to_string())
        );
    }

    #[test]
    fn test_gateway_fingerprint_default_title() {
        let params = serde_json::json!({});
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Gateway, &params),
            Some("gateway:Gateway".to_string())
        );
    }

    #[test]
    fn test_mock_no_fingerprint() {
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Mock, &serde_json::json!({})),
            None
        );
    }

    #[test]
    fn test_acceptor_no_fingerprint() {
        assert_eq!(
            compute_fingerprint(&ConnectorKind::Acceptor, &serde_json::json!({})),
            None
        );
    }

    #[test]
    fn test_acp_unknown_transport_type() {
        let params = serde_json::json!({
            "transport": {
                "type": "websocket",
                "url": "ws://localhost:8080"
            }
        });
        assert_eq!(compute_fingerprint(&ConnectorKind::Acp, &params), None);
    }

    #[test]
    fn test_acp_missing_transport() {
        let params = serde_json::json!({"base_url": "http://localhost:3000"});
        assert_eq!(compute_fingerprint(&ConnectorKind::Acp, &params), None);
    }

    #[test]
    fn test_opencode_missing_base_url() {
        let params = serde_json::json!({"title": "My OpenCode"});
        assert_eq!(compute_fingerprint(&ConnectorKind::OpenCode, &params), None);
    }

    #[test]
    fn test_resolve_command_path_absolute() {
        // Absolute paths should be returned as-is
        #[cfg(unix)]
        assert_eq!(resolve_command_path("/usr/bin/claude"), "/usr/bin/claude");

        #[cfg(windows)]
        assert_eq!(
            resolve_command_path("C:\\Program Files\\claude.exe"),
            "C:\\Program Files\\claude.exe"
        );
    }

    #[test]
    fn test_resolve_command_path_nonexistent() {
        // A command that definitely doesn't exist should fall back to raw string
        let result = resolve_command_path("definitely_not_a_real_command_12345");
        assert_eq!(result, "definitely_not_a_real_command_12345");
    }
}
