//! §9.4 Service-interceptor markers — the `interceptors` table (0073).
//!
//! A marker `(owner, service, op) -> interceptor_script` declares a before-op
//! allow/deny hook. Pure declaration (like `extension_points`, 0051); the
//! interceptor's behaviour is a normal script resolved + run through `invoke()`
//! re-entry. This module holds the read + transactional-write helpers (free
//! functions over `&PgPool` / `&mut Transaction`, keyed by [`ScriptOwner`]),
//! plus the runtime [`resolve_before`] chain walk (nearest-owner-wins).

use chrono::{DateTime, Utc};
use picloud_shared::{AppId, ScriptOwner};
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

use crate::config_resolver::CHAIN_LEVELS_CTE;

/// One interceptor marker at an owner: which `(service, op)` it guards and the
/// interceptor script name.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterceptorMarker {
    pub service: String,
    pub op: String,
    pub script: String,
}

/// List the markers declared **directly at** `owner` (not inherited), ordered
/// deterministically. Used by `load_current` (apply diff) and `interceptors ls`.
pub async fn list_for_owner(
    pool: &PgPool,
    owner: ScriptOwner,
) -> Result<Vec<InterceptorMarker>, sqlx::Error> {
    let rows: Vec<(String, String, String)> = match owner {
        ScriptOwner::App(a) => {
            sqlx::query_as(
                "SELECT service, op, interceptor_script FROM interceptors \
                 WHERE app_id = $1 ORDER BY service, op",
            )
            .bind(a.into_inner())
            .fetch_all(pool)
            .await?
        }
        ScriptOwner::Group(g) => {
            sqlx::query_as(
                "SELECT service, op, interceptor_script FROM interceptors \
                 WHERE group_id = $1 ORDER BY service, op",
            )
            .bind(g.into_inner())
            .fetch_all(pool)
            .await?
        }
    };
    Ok(rows
        .into_iter()
        .map(|(service, op, script)| InterceptorMarker {
            service,
            op,
            script,
        })
        .collect())
}

/// All markers **visible to an app** — declared at the app or any ancestor
/// group (each `(service, op)` resolved nearest-owner-wins). Used by
/// `interceptors ls --app` and by the resolver's chain view.
pub async fn list_on_app_chain(
    pool: &PgPool,
    app_id: AppId,
) -> Result<Vec<InterceptorMarker>, sqlx::Error> {
    let rows: Vec<(String, String, String)> = sqlx::query_as(&format!(
        "{CHAIN_LEVELS_CTE} \
         SELECT DISTINCT ON (i.service, i.op) i.service, i.op, i.interceptor_script \
         FROM chain c \
         JOIN interceptors i ON (i.app_id = c.app_owner OR i.group_id = c.group_owner) \
         ORDER BY i.service, i.op, c.depth ASC",
    ))
    .bind(app_id.into_inner())
    .fetch_all(pool)
    .await?;
    Ok(rows
        .into_iter()
        .map(|(service, op, script)| InterceptorMarker {
            service,
            op,
            script,
        })
        .collect())
}

/// The nearest interceptor marker on an app's chain, with its script resolved
/// **at the declaring owner** (the seal). `script` is `None` when the marker
/// names a script that is missing or disabled at that owner — a dangling hook
/// the caller must fail closed on.
#[derive(Debug, Clone)]
pub struct SealedInterceptor {
    pub marker_app: Option<Uuid>,
    pub marker_group: Option<Uuid>,
    pub script_name: String,
    /// `(script_id, source, updated_at)` of the sealed script, or `None` if it
    /// is missing/disabled at the declaring owner.
    pub script: Option<(Uuid, String, DateTime<Utc>)>,
}

impl SealedInterceptor {
    /// The owner that DECLARED the marker (and therefore owns the resolved
    /// script) — the lexical origin for the interceptor's own `import`s (§5.5).
    #[must_use]
    pub fn sealing_owner(&self) -> ScriptOwner {
        if let Some(g) = self.marker_group {
            ScriptOwner::Group(g.into())
        } else {
            ScriptOwner::App(
                self.marker_app
                    .expect("marker XOR: app when not group")
                    .into(),
            )
        }
    }
}

/// Resolve the interceptor guarding `(service, op)` for a calling app — the
/// nearest MARKER on the app's chain (app, then nearest ancestor group), with
/// its script **sealed to the owner that declared the marker** (a descendant
/// app cannot shadow it with a same-named local script). `None` when un-hooked.
/// **This chain walk is the resolution boundary** — a sibling-subtree app never
/// sees another subtree's interceptor. The script join is `LEFT` so a marker
/// whose script is missing/disabled still returns a row (with `script: None`)
/// rather than silently falling through to a farther, weaker marker.
pub async fn resolve_before(
    pool: &PgPool,
    app_id: AppId,
    service: &str,
    op: &str,
) -> Result<Option<SealedInterceptor>, sqlx::Error> {
    #[allow(clippy::type_complexity)]
    let row: Option<(
        Option<Uuid>,
        Option<Uuid>,
        String,
        Option<Uuid>,
        Option<String>,
        Option<DateTime<Utc>>,
    )> = sqlx::query_as(&format!(
        "{CHAIN_LEVELS_CTE}, \
         nearest AS ( \
             SELECT i.interceptor_script AS script_name, \
                    i.app_id AS marker_app, i.group_id AS marker_group, c.depth AS depth \
             FROM chain c \
             JOIN interceptors i ON (i.app_id = c.app_owner OR i.group_id = c.group_owner) \
             WHERE i.service = $2 AND i.op = $3 \
             ORDER BY c.depth ASC LIMIT 1 \
         ) \
         SELECT n.marker_app, n.marker_group, n.script_name, \
                s.id, s.source, s.updated_at \
         FROM nearest n \
         LEFT JOIN scripts s ON ( \
             (n.marker_app IS NOT NULL AND s.app_id = n.marker_app \
              AND s.name = n.script_name AND s.enabled) \
             OR (n.marker_group IS NOT NULL AND s.group_id = n.marker_group \
              AND s.name = n.script_name AND s.enabled) \
         )",
    ))
    .bind(app_id.into_inner())
    .bind(service)
    .bind(op)
    .fetch_optional(pool)
    .await?;
    Ok(row.map(
        |(marker_app, marker_group, script_name, sid, src, upd)| SealedInterceptor {
            marker_app,
            marker_group,
            script_name,
            script: match (sid, src, upd) {
                (Some(id), Some(source), Some(updated_at)) => Some((id, source, updated_at)),
                _ => None,
            },
        },
    ))
}

/// Upsert a marker at `owner` in the apply transaction. A re-apply that changes
/// only the interceptor script updates it in place (no version churn on the
/// `(service, op)` identity).
pub async fn insert_interceptor_tx(
    tx: &mut Transaction<'_, Postgres>,
    owner: ScriptOwner,
    service: &str,
    op: &str,
    script: &str,
) -> Result<(), sqlx::Error> {
    match owner {
        ScriptOwner::App(a) => {
            sqlx::query(
                "INSERT INTO interceptors (app_id, service, op, interceptor_script) \
                 VALUES ($1, $2, $3, $4) \
                 ON CONFLICT (app_id, service, op) WHERE app_id IS NOT NULL \
                 DO UPDATE SET interceptor_script = EXCLUDED.interceptor_script, updated_at = NOW()",
            )
            .bind(a.into_inner())
            .bind(service)
            .bind(op)
            .bind(script)
            .execute(&mut **tx)
            .await?;
        }
        ScriptOwner::Group(g) => {
            sqlx::query(
                "INSERT INTO interceptors (group_id, service, op, interceptor_script) \
                 VALUES ($1, $2, $3, $4) \
                 ON CONFLICT (group_id, service, op) WHERE group_id IS NOT NULL \
                 DO UPDATE SET interceptor_script = EXCLUDED.interceptor_script, updated_at = NOW()",
            )
            .bind(g.into_inner())
            .bind(service)
            .bind(op)
            .bind(script)
            .execute(&mut **tx)
            .await?;
        }
    }
    Ok(())
}

/// Delete a marker at `owner` (by `(service, op)`), in the apply transaction.
/// Used by `--prune` when the manifest stops declaring it.
pub async fn delete_interceptor_tx(
    tx: &mut Transaction<'_, Postgres>,
    owner: ScriptOwner,
    service: &str,
    op: &str,
) -> Result<(), sqlx::Error> {
    match owner {
        ScriptOwner::App(a) => {
            sqlx::query("DELETE FROM interceptors WHERE app_id = $1 AND service = $2 AND op = $3")
                .bind(a.into_inner())
                .bind(service)
                .bind(op)
                .execute(&mut **tx)
                .await?;
        }
        ScriptOwner::Group(g) => {
            sqlx::query(
                "DELETE FROM interceptors WHERE group_id = $1 AND service = $2 AND op = $3",
            )
            .bind(g.into_inner())
            .bind(service)
            .bind(op)
            .execute(&mut **tx)
            .await?;
        }
    }
    Ok(())
}
