//! Per-agent key database operations.
//!
//! Backs the `connect_agent_keys` table introduced in v2 (migration 004). A
//! per-agent key replaces the shared `AGENT_API_KEY`: each managed machine has
//! one or more revocable keys, stored ONLY as a hash.
//!
//! This module is hash-agnostic persistence. Computing the hash and minting the
//! plaintext `cak_` token is Task 2 - every function here takes an
//! already-hashed value. All queries use runtime `sqlx::query()` /
//! `sqlx::query_as()` per the v2 convention (no compile-time macros).

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;

/// Per-agent key record from the database.
///
/// `key_hash` is the only representation of the key the server ever stores; the
/// plaintext is shown once at issuance (Task 2) and never persisted.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
#[allow(dead_code)] // Consumed by agent auth + key issuance endpoints in Task 2.
pub struct AgentKey {
    pub id: Uuid,
    pub machine_id: Uuid,
    pub key_hash: String,
    pub tenant_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

/// Insert a new per-agent key (already hashed) and return its id.
///
/// `tenant_id` is `None`-tolerant; pass `db::tenancy::current_tenant_id()` at
/// the call site once issuance lands in Task 2.
#[allow(dead_code)] // Wired by the key-issuance endpoint in Task 2.
pub async fn insert_agent_key(
    pool: &PgPool,
    machine_id: Uuid,
    key_hash: &str,
    tenant_id: Option<Uuid>,
) -> Result<Uuid, sqlx::Error> {
    let id = sqlx::query_scalar::<_, Uuid>(
        r#"
        INSERT INTO connect_agent_keys (machine_id, key_hash, tenant_id)
        VALUES ($1, $2, $3)
        RETURNING id
        "#,
    )
    .bind(machine_id)
    .bind(key_hash)
    .bind(tenant_id)
    .fetch_one(pool)
    .await?;
    Ok(id)
}

/// Find an active (non-revoked) key by its hash.
///
/// Returns `None` if no row matches or the matching key has been revoked. Agent
/// authentication (Task 2) hashes the presented `cak_` token and looks it up
/// here, rejecting on `None`.
#[allow(dead_code)] // Wired by agent WS auth in Task 2/3.
pub async fn find_active_by_hash(
    pool: &PgPool,
    key_hash: &str,
) -> Result<Option<AgentKey>, sqlx::Error> {
    sqlx::query_as::<_, AgentKey>(
        r#"
        SELECT id, machine_id, key_hash, tenant_id, created_at, last_used_at, revoked_at
        FROM connect_agent_keys
        WHERE key_hash = $1 AND revoked_at IS NULL
        "#,
    )
    .bind(key_hash)
    .fetch_optional(pool)
    .await
}

/// List all keys for a machine (newest first), including revoked ones.
///
/// Returns full rows; the API layer projects these to metadata-only responses
/// and never serializes `key_hash`.
pub async fn list_for_machine(
    pool: &PgPool,
    machine_id: Uuid,
) -> Result<Vec<AgentKey>, sqlx::Error> {
    sqlx::query_as::<_, AgentKey>(
        r#"
        SELECT id, machine_id, key_hash, tenant_id, created_at, last_used_at, revoked_at
        FROM connect_agent_keys
        WHERE machine_id = $1
        ORDER BY created_at DESC
        "#,
    )
    .bind(machine_id)
    .fetch_all(pool)
    .await
}

/// Check whether a key id belongs to the given machine.
///
/// Used to scope revocation to the machine in the request path, so a key id
/// cannot be revoked via a mismatched machine URL.
pub async fn key_belongs_to_machine(
    pool: &PgPool,
    key_id: Uuid,
    machine_id: Uuid,
) -> Result<bool, sqlx::Error> {
    let exists = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS(SELECT 1 FROM connect_agent_keys WHERE id = $1 AND machine_id = $2)",
    )
    .bind(key_id)
    .bind(machine_id)
    .fetch_one(pool)
    .await?;
    Ok(exists)
}

/// Revoke a key by id (idempotent: re-revoking keeps the original timestamp).
pub async fn revoke_agent_key(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE connect_agent_keys
        SET revoked_at = NOW()
        WHERE id = $1 AND revoked_at IS NULL
        "#,
    )
    .bind(id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Stamp `last_used_at` on a successful authentication.
#[allow(dead_code)] // Wired by agent WS auth in Task 2/3.
pub async fn touch_last_used(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE connect_agent_keys SET last_used_at = NOW() WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}
