//! Audit event logging

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::PgPool;
use std::net::IpAddr;
use uuid::Uuid;

/// Session event record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct SessionEvent {
    pub id: i64,
    pub session_id: Uuid,
    pub event_type: String,
    pub timestamp: DateTime<Utc>,
    pub viewer_id: Option<String>,
    pub viewer_name: Option<String>,
    pub details: Option<JsonValue>,
    pub ip_address: Option<String>,
    /// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
    pub tenant_id: Option<Uuid>,
}

/// Event types for session audit logging
pub struct EventTypes;

impl EventTypes {
    pub const SESSION_STARTED: &'static str = "session_started";
    pub const SESSION_ENDED: &'static str = "session_ended";
    #[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
    pub const SESSION_TIMEOUT: &'static str = "session_timeout";
    pub const VIEWER_JOINED: &'static str = "viewer_joined";
    pub const VIEWER_LEFT: &'static str = "viewer_left";
    #[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
    pub const STREAMING_STARTED: &'static str = "streaming_started";
    #[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
    pub const STREAMING_STOPPED: &'static str = "streaming_stopped";

    // Failed connection events (security audit trail)
    pub const CONNECTION_REJECTED_NO_AUTH: &'static str = "connection_rejected_no_auth";
    pub const CONNECTION_REJECTED_INVALID_CODE: &'static str = "connection_rejected_invalid_code";
    #[allow(dead_code)] // TODO(audit-events): emit on expired-code bind rejection
    pub const CONNECTION_REJECTED_EXPIRED_CODE: &'static str = "connection_rejected_expired_code";
    pub const CONNECTION_REJECTED_INVALID_API_KEY: &'static str =
        "connection_rejected_invalid_api_key";
    #[allow(dead_code)] // TODO(audit-events): emit on cancelled-code bind rejection
    pub const CONNECTION_REJECTED_CANCELLED_CODE: &'static str =
        "connection_rejected_cancelled_code";

    // Attended-mode consent decisions (Task 5). Audited with the session id and
    // the agent id in `details` so the consent decision is fully traceable.
    /// End user accepted the attended-session consent prompt.
    pub const CONSENT_GRANTED: &'static str = "consent_granted";
    /// End user declined, the prompt timed out, or the agent disconnected
    /// before answering — the attended session was torn down.
    pub const CONSENT_DENIED: &'static str = "consent_denied";
    /// A `ConsentRequest` was sent to the agent for an attended session (the
    /// prompt is now awaiting the end user's decision).
    pub const CONSENT_REQUESTED: &'static str = "consent_requested";
}

/// Log a session event
pub async fn log_event(
    pool: &PgPool,
    session_id: Uuid,
    event_type: &str,
    viewer_id: Option<&str>,
    viewer_name: Option<&str>,
    details: Option<JsonValue>,
    ip_address: Option<IpAddr>,
) -> Result<i64, sqlx::Error> {
    let ip_str = ip_address.map(|ip| ip.to_string());

    let result = sqlx::query_scalar::<_, i64>(
        r#"
        INSERT INTO connect_session_events
            (session_id, event_type, viewer_id, viewer_name, details, ip_address)
        VALUES ($1, $2, $3, $4, $5, $6::inet)
        RETURNING id
        "#,
    )
    .bind(session_id)
    .bind(event_type)
    .bind(viewer_id)
    .bind(viewer_name)
    .bind(details)
    .bind(ip_str)
    .fetch_one(pool)
    .await?;

    Ok(result)
}

/// Get events for a session
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_session_events(
    pool: &PgPool,
    session_id: Uuid,
) -> Result<Vec<SessionEvent>, sqlx::Error> {
    sqlx::query_as::<_, SessionEvent>(
        "SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events WHERE session_id = $1 ORDER BY timestamp"
    )
    .bind(session_id)
    .fetch_all(pool)
    .await
}

/// Get recent events (for dashboard)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_recent_events(
    pool: &PgPool,
    limit: i64,
) -> Result<Vec<SessionEvent>, sqlx::Error> {
    sqlx::query_as::<_, SessionEvent>(
        "SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events ORDER BY timestamp DESC LIMIT $1"
    )
    .bind(limit)
    .fetch_all(pool)
    .await
}

/// Get events by type
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_events_by_type(
    pool: &PgPool,
    event_type: &str,
    limit: i64,
) -> Result<Vec<SessionEvent>, sqlx::Error> {
    sqlx::query_as::<_, SessionEvent>(
        "SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events WHERE event_type = $1 ORDER BY timestamp DESC LIMIT $2"
    )
    .bind(event_type)
    .bind(limit)
    .fetch_all(pool)
    .await
}

/// Get all events for a machine (by joining through sessions)
pub async fn get_events_for_machine(
    pool: &PgPool,
    machine_id: Uuid,
) -> Result<Vec<SessionEvent>, sqlx::Error> {
    sqlx::query_as::<_, SessionEvent>(
        r#"
        SELECT e.id, e.session_id, e.event_type, e.timestamp, e.viewer_id, e.viewer_name, e.details, e.ip_address::text as ip_address, e.tenant_id
        FROM connect_session_events e
        JOIN connect_sessions s ON e.session_id = s.id
        WHERE s.machine_id = $1
        ORDER BY e.timestamp DESC
        "#
    )
    .bind(machine_id)
    .fetch_all(pool)
    .await
}
