//! Machine/Agent database operations

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

/// Machine record from database
///
/// `FromRow` is implemented manually (not derived) so that every column whose
/// schema definition is *nullable* decodes NULL-tolerantly. The `connect_machines`
/// table was created in migration 001 with only `DEFAULT` clauses (no `NOT NULL`)
/// on `is_elevated`, `is_persistent`, `first_seen`, `last_seen`, `status`,
/// `created_at`, and `updated_at`; `tags` (migration 005) likewise ended up
/// nullable with no default on the production instance. A derived `FromRow` maps
/// those to non-`Option` Rust types and errors at decode time the moment any cell
/// is NULL (`unexpected null; try decoding as an Option`). In production a row with
/// `tags IS NULL` broke the startup reconcile task and would 500 the authenticated
/// Machines list. The manual impl below reads every nullable column as
/// `Option<T>` and falls back to `Default::default()`, so a NULL can never panic or
/// error regardless of how the column was created. Truly non-null columns (`id`,
/// `agent_id`, `hostname`) are decoded directly. Migration 007 additionally pins
/// `tags` to `DEFAULT '{}'` and backfills existing NULLs (defense in depth).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Machine {
    pub id: Uuid,
    pub agent_id: String,
    pub hostname: String,
    pub os_version: Option<String>,
    pub is_elevated: bool,
    pub is_persistent: bool,
    pub first_seen: DateTime<Utc>,
    pub last_seen: DateTime<Utc>,
    pub last_session_id: Option<Uuid>,
    pub status: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    /// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
    pub tenant_id: Option<Uuid>,
    /// Company/organization name reported by the agent (`AgentStatus.organization`).
    /// Column added in migration 005; previously written by `update_machine_metadata`
    /// against a non-existent column (the write silently failed). Now mapped here so
    /// `SELECT *` returns it.
    pub organization: Option<String>,
    /// Site/location name reported by the agent (`AgentStatus.site`). Migration 005.
    pub site: Option<String>,
    /// Free-form tags reported by the agent (`AgentStatus.tags`). Stored as a
    /// Postgres `TEXT[]`; matches the `&[String]` bound by `update_machine_metadata`.
    /// Migration 005. A `NULL` cell decodes to an empty vec (see the manual `FromRow`
    /// impl) so it can never error at decode time.
    #[serde(default)]
    pub tags: Vec<String>,
}

impl<'r> FromRow<'r, PgRow> for Machine {
    fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
        Ok(Self {
            // NOT NULL columns: decode directly.
            id: row.try_get("id")?,
            agent_id: row.try_get("agent_id")?,
            hostname: row.try_get("hostname")?,
            // Schema-nullable in `Option<T>` form already: decode directly.
            os_version: row.try_get("os_version")?,
            last_session_id: row.try_get("last_session_id")?,
            tenant_id: row.try_get("tenant_id")?,
            organization: row.try_get("organization")?,
            site: row.try_get("site")?,
            // Nullable-with-default columns mapped to non-`Option` Rust types: read as
            // `Option<T>` and fall back to the type default so a NULL cell never errors.
            is_elevated: row
                .try_get::<Option<bool>, _>("is_elevated")?
                .unwrap_or_default(),
            is_persistent: row
                .try_get::<Option<bool>, _>("is_persistent")?
                .unwrap_or_default(),
            first_seen: row
                .try_get::<Option<DateTime<Utc>>, _>("first_seen")?
                .unwrap_or_default(),
            last_seen: row
                .try_get::<Option<DateTime<Utc>>, _>("last_seen")?
                .unwrap_or_default(),
            status: row
                .try_get::<Option<String>, _>("status")?
                .unwrap_or_default(),
            created_at: row
                .try_get::<Option<DateTime<Utc>>, _>("created_at")?
                .unwrap_or_default(),
            updated_at: row
                .try_get::<Option<DateTime<Utc>>, _>("updated_at")?
                .unwrap_or_default(),
            // The production bug: `tags` was nullable with no default. A NULL cell
            // decodes to an empty vec here instead of erroring.
            tags: row
                .try_get::<Option<Vec<String>>, _>("tags")?
                .unwrap_or_default(),
        })
    }
}

/// Get or create a machine by agent_id (upsert)
pub async fn upsert_machine(
    pool: &PgPool,
    agent_id: &str,
    hostname: &str,
    is_persistent: bool,
) -> Result<Machine, sqlx::Error> {
    sqlx::query_as::<_, Machine>(
        r#"
        INSERT INTO connect_machines (agent_id, hostname, is_persistent, status, last_seen)
        VALUES ($1, $2, $3, 'online', NOW())
        ON CONFLICT (agent_id) DO UPDATE SET
            hostname = EXCLUDED.hostname,
            status = 'online',
            last_seen = NOW()
        RETURNING *
        "#,
    )
    .bind(agent_id)
    .bind(hostname)
    .bind(is_persistent)
    .fetch_one(pool)
    .await
}

/// Update machine status and info
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn update_machine_status(
    pool: &PgPool,
    agent_id: &str,
    status: &str,
    os_version: Option<&str>,
    is_elevated: bool,
    session_id: Option<Uuid>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE connect_machines SET
            status = $1,
            os_version = COALESCE($2, os_version),
            is_elevated = $3,
            last_seen = NOW(),
            last_session_id = COALESCE($4, last_session_id)
        WHERE agent_id = $5
        "#,
    )
    .bind(status)
    .bind(os_version)
    .bind(is_elevated)
    .bind(session_id)
    .bind(agent_id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Get all persistent machines (for restore on startup)
pub async fn get_all_machines(pool: &PgPool) -> Result<Vec<Machine>, sqlx::Error> {
    sqlx::query_as::<_, Machine>(
        "SELECT * FROM connect_machines WHERE is_persistent = true ORDER BY hostname",
    )
    .fetch_all(pool)
    .await
}

/// Get machine by agent_id
pub async fn get_machine_by_agent_id(
    pool: &PgPool,
    agent_id: &str,
) -> Result<Option<Machine>, sqlx::Error> {
    sqlx::query_as::<_, Machine>("SELECT * FROM connect_machines WHERE agent_id = $1")
        .bind(agent_id)
        .fetch_optional(pool)
        .await
}

/// Get machine by its primary-key UUID (`connect_machines.id`).
///
/// Used by the agent WS plane to resolve the trusted `machine_id` returned by
/// `verify_agent_key` back to its canonical `agent_id`, so persistent reattach
/// binds to the authenticated identity rather than a client-supplied query
/// param (Task 3 identity binding).
pub async fn get_machine_by_id(
    pool: &PgPool,
    machine_id: Uuid,
) -> Result<Option<Machine>, sqlx::Error> {
    sqlx::query_as::<_, Machine>("SELECT * FROM connect_machines WHERE id = $1")
        .bind(machine_id)
        .fetch_optional(pool)
        .await
}

/// Mark machine as offline
pub async fn mark_machine_offline(pool: &PgPool, agent_id: &str) -> Result<(), sqlx::Error> {
    sqlx::query(
        "UPDATE connect_machines SET status = 'offline', last_seen = NOW() WHERE agent_id = $1",
    )
    .bind(agent_id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Delete a machine record
pub async fn delete_machine(pool: &PgPool, agent_id: &str) -> Result<(), sqlx::Error> {
    sqlx::query("DELETE FROM connect_machines WHERE agent_id = $1")
        .bind(agent_id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Update machine organization, site, and tags
pub async fn update_machine_metadata(
    pool: &PgPool,
    agent_id: &str,
    organization: Option<&str>,
    site: Option<&str>,
    tags: &[String],
) -> Result<(), sqlx::Error> {
    // Only update if at least one value is provided
    if organization.is_none() && site.is_none() && tags.is_empty() {
        return Ok(());
    }

    sqlx::query(
        r#"
        UPDATE connect_machines SET
            organization = COALESCE($1, organization),
            site = COALESCE($2, site),
            tags = CASE WHEN $3::text[] = '{}' THEN tags ELSE $3 END
        WHERE agent_id = $4
        "#,
    )
    .bind(organization)
    .bind(site)
    .bind(tags)
    .bind(agent_id)
    .execute(pool)
    .await?;
    Ok(())
}
