//! The §3 configuration-resolution engine: env-filtered, proximity-first
//! inheritance down the group tree.
//!
//! To resolve a key for app A in environment E (docs/design §3):
//!   1. **Env-filter first, per level** — a value scoped `@E` is eligible;
//!      `*` (env-agnostic) is the fallback. Within one level `@E` beats `*`.
//!      Env is *eligibility*, not a precedence tier.
//!   2. **Nearest level wins** — walk A → parent group → … → root; the
//!      closest level that defines the (filtered) key wins. Proximity beats
//!      farther-level env-specificity (a leaf's `*` beats an ancestor's `@E`).
//!   3. **Maps deep-merge per key; scalars/arrays replace; deletion is an
//!      explicit tombstone** that suppresses the inherited key.
//!
//! The recursive CTE that walks `apps.group_id → groups.parent_id → root`
//! mirrors `app_members_repo::effective_app_role`. The env-eligibility
//! filter happens in SQL; the §3 merge/replace/tombstone semantics — which
//! a window-function pick can't express — happen in the pure `resolve`
//! function below, so they're unit-tested without Postgres.

use std::collections::BTreeMap;

use serde_json::{Map, Value};
use sqlx::PgPool;
use uuid::Uuid;

use picloud_shared::AppId;

/// Shared chain-walk CTE: emits one row per owner-level for an app —
/// depth 0 = the app itself (`app_owner` set), then ancestor groups
/// nearest-first (`group_owner` set), each carrying the app's environment.
/// Depth-bounded `< 64` (the group cycle guard already forbids cycles; this
/// is a runaway guard). Bind `$1 = app_id`. Reused by the vars and secret
/// resolvers, which each append their own owner-keyed JOIN.
pub(crate) const CHAIN_LEVELS_CTE: &str = "\
    WITH RECURSIVE chain AS ( \
        SELECT a.id AS app_owner, NULL::uuid AS group_owner, \
               a.group_id AS next_group, 0 AS depth, a.environment AS app_env \
        FROM apps a WHERE a.id = $1 \
      UNION ALL \
        SELECT NULL::uuid, g.id, g.parent_id, c.depth + 1, c.app_env \
        FROM groups g JOIN chain c ON g.id = c.next_group \
        WHERE c.depth < 64 \
    )";

/// Group-rooted variant of [`CHAIN_LEVELS_CTE`]: the chain starts at a
/// **group** (depth 0 = the group itself, `group_owner` set), then walks
/// its ancestor groups nearest-first. There is no `app_owner` level — a
/// group origin never sees app-owned rows below it (the §5.5 trust
/// boundary). Bind `$1 = group_id`. Used for the lexical module resolver
/// when the importing script's defining node is a group.
pub(crate) const GROUP_CHAIN_LEVELS_CTE: &str = "\
    WITH RECURSIVE chain AS ( \
        SELECT g.id AS group_owner, g.parent_id AS next_group, 0 AS depth \
        FROM groups g WHERE g.id = $1 \
      UNION ALL \
        SELECT g.id, g.parent_id, c.depth + 1 \
        FROM groups g JOIN chain c ON g.id = c.next_group \
        WHERE c.depth < 64 \
    )";

/// Owner kind of a resolved value, for `--explain` provenance.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnerKind {
    App,
    Group,
}

impl OwnerKind {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::App => "app",
            Self::Group => "group",
        }
    }
}

/// One eligible (env-filtered) candidate row pulled by the resolver, before
/// §3 proximity/merge resolution.
#[derive(Debug, Clone)]
pub struct Candidate {
    /// 0 = the app itself; 1 = its parent group; … (nearest-first).
    pub depth: i32,
    pub owner_kind: OwnerKind,
    pub owner_id: Uuid,
    /// `*` (env-agnostic) or a concrete environment name.
    pub scope: String,
    pub key: String,
    pub value: Value,
    pub is_tombstone: bool,
}

/// Where a resolved key came from (for `config --effective --explain`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provenance {
    pub owner_kind: OwnerKind,
    pub owner_id: Uuid,
    pub depth: i32,
    pub scope: String,
    /// For a deep-merged map, the `(depth, scope)` of every layer that
    /// contributed, nearest-first. Empty for a plain scalar/array winner.
    pub merged_from: Vec<(i32, String)>,
}

/// `@E`-scoped values outrank `*` *within the same level* (§3 step 1).
fn env_priority(scope: &str) -> u8 {
    u8::from(scope != "*")
}

/// Deep-merge `src` into `dst` per key: nested objects merge recursively,
/// everything else is replaced by `src`. Caller merges farthest→nearest so
/// nearer layers overwrite.
fn deep_merge(dst: &mut Map<String, Value>, src: &Map<String, Value>) {
    for (k, sv) in src {
        match (dst.get_mut(k), sv) {
            (Some(Value::Object(dm)), Value::Object(sm)) => deep_merge(dm, sm),
            _ => {
                dst.insert(k.clone(), sv.clone());
            }
        }
    }
}

/// Resolve one key's candidate rows (any order in) to its effective value,
/// plus the provenance. Returns `None` when a tombstone suppresses the key.
fn resolve_one(mut rows: Vec<Candidate>) -> Option<(Value, Provenance)> {
    // Nearest-first; within a level, `@E` before `*` (§3 steps 1-2).
    rows.sort_by(|a, b| {
        a.depth
            .cmp(&b.depth)
            .then(env_priority(&b.scope).cmp(&env_priority(&a.scope)))
    });

    // §3 step 1: env is eligibility, not a merge tier — within a single level
    // `@E` *suppresses* `*` (it does not layer on top of it). Each level is one
    // owner (single-parent tree) with at most one `@E` and one `*` row per key
    // after env-filtering; keep only the level's winner (the `@E` row sorts
    // first). Without this, a level holding both an `@E` map and a `*` map would
    // deep-merge them instead of the `@E` shadowing the `*`.
    rows.dedup_by_key(|r| r.depth);

    // Collect the contiguous run of map-valued rows from the nearest. A
    // tombstone or scalar/array boundary stops the run (and, if it's the
    // nearest row, decides the result outright).
    let mut maps: Vec<&Candidate> = Vec::new();
    let mut boundary: Option<&Candidate> = None;
    for r in &rows {
        if r.is_tombstone {
            boundary = Some(r);
            break;
        }
        if r.value.is_object() {
            maps.push(r);
        } else {
            boundary = Some(r);
            break;
        }
    }

    if let Some(first) = maps.first() {
        // Map run wins; deep-merge farthest→nearest so nearest keys win.
        let mut acc = Map::new();
        for m in maps.iter().rev() {
            if let Value::Object(obj) = &m.value {
                deep_merge(&mut acc, obj);
            }
        }
        let prov = Provenance {
            owner_kind: first.owner_kind,
            owner_id: first.owner_id,
            depth: first.depth,
            scope: first.scope.clone(),
            merged_from: maps.iter().map(|m| (m.depth, m.scope.clone())).collect(),
        };
        return Some((Value::Object(acc), prov));
    }

    // No leading map run: the nearest row is the boundary.
    match boundary {
        // Nearest is a tombstone → key deleted.
        Some(b) if b.is_tombstone => None,
        // Nearest is a scalar/array → take it verbatim.
        Some(b) => Some((
            b.value.clone(),
            Provenance {
                owner_kind: b.owner_kind,
                owner_id: b.owner_id,
                depth: b.depth,
                scope: b.scope.clone(),
                merged_from: Vec::new(),
            },
        )),
        // No rows at all (caller only passes non-empty groups).
        None => None,
    }
}

/// Resolve a full candidate set (mixed keys, any order) into the effective
/// config map + per-key provenance. Pure — unit-tested without Postgres.
#[must_use]
pub fn resolve(
    candidates: Vec<Candidate>,
) -> (BTreeMap<String, Value>, BTreeMap<String, Provenance>) {
    let mut by_key: BTreeMap<String, Vec<Candidate>> = BTreeMap::new();
    for c in candidates {
        by_key.entry(c.key.clone()).or_default().push(c);
    }
    let mut values = BTreeMap::new();
    let mut provenance = BTreeMap::new();
    for (key, rows) in by_key {
        if let Some((v, p)) = resolve_one(rows) {
            values.insert(key.clone(), v);
            provenance.insert(key, p);
        }
    }
    (values, provenance)
}

/// Pull every env-eligible `vars` candidate for `app_id`: the app's own
/// rows + every ancestor group's rows, filtered to `*` or the app's
/// environment, ordered nearest-first.
///
/// # Errors
/// Propagates sqlx errors.
pub async fn fetch_var_candidates(
    pool: &PgPool,
    app_id: AppId,
) -> Result<Vec<Candidate>, sqlx::Error> {
    let sql = format!(
        "{CHAIN_LEVELS_CTE} \
         SELECT c.depth, \
                CASE WHEN v.app_id IS NOT NULL THEN 'app' ELSE 'group' END AS owner_kind, \
                COALESCE(v.app_id, v.group_id) AS owner_id, \
                v.environment_scope, v.key, v.value, v.is_tombstone \
         FROM chain c \
         JOIN vars v ON (v.app_id = c.app_owner OR v.group_id = c.group_owner) \
         WHERE v.environment_scope = '*' OR v.environment_scope = c.app_env \
         ORDER BY c.depth ASC"
    );
    let rows = sqlx::query_as::<_, VarCandidateRow>(&sql)
        .bind(app_id.into_inner())
        .fetch_all(pool)
        .await?;
    Ok(rows.into_iter().map(Into::into).collect())
}

/// The masked, resolved view of one inherited secret for `config/effective`:
/// which owner/level/scope supplies it — **never** the value.
#[derive(Debug, Clone)]
pub struct EffectiveSecretMeta {
    pub name: String,
    pub owner_kind: OwnerKind,
    pub owner_id: Uuid,
    pub scope: String,
    pub depth: i32,
}

/// Resolve the *names* of every secret an app effectively sees — its own
/// plus every ancestor group's, env-filtered, nearest-wins per name. Returns
/// masked metadata only (owner/level/scope), so it's safe for an app-level
/// principal to read. `DISTINCT ON (name)` with the same ordering as the
/// per-name secret resolver guarantees the same winner.
///
/// # Errors
/// Propagates sqlx errors.
pub async fn fetch_effective_secret_meta(
    pool: &PgPool,
    app_id: AppId,
) -> Result<Vec<EffectiveSecretMeta>, sqlx::Error> {
    let sql = format!(
        "{CHAIN_LEVELS_CTE} \
         SELECT DISTINCT ON (s.name) s.name, \
                CASE WHEN s.app_id IS NOT NULL THEN 'app' ELSE 'group' END AS owner_kind, \
                COALESCE(s.app_id, s.group_id) AS owner_id, \
                s.environment_scope, c.depth \
         FROM chain c \
         JOIN secrets s ON (s.app_id = c.app_owner OR s.group_id = c.group_owner) \
         WHERE s.environment_scope = '*' OR s.environment_scope = c.app_env \
         ORDER BY s.name ASC, c.depth ASC, (s.environment_scope <> '*') DESC"
    );
    let rows: Vec<(String, String, Uuid, String, i32)> = sqlx::query_as(&sql)
        .bind(app_id.into_inner())
        .fetch_all(pool)
        .await?;
    Ok(rows
        .into_iter()
        .map(
            |(name, owner_kind, owner_id, scope, depth)| EffectiveSecretMeta {
                name,
                owner_kind: if owner_kind == "app" {
                    OwnerKind::App
                } else {
                    OwnerKind::Group
                },
                owner_id,
                scope,
                depth,
            },
        )
        .collect())
}

#[derive(sqlx::FromRow)]
struct VarCandidateRow {
    depth: i32,
    owner_kind: String,
    owner_id: Uuid,
    environment_scope: String,
    key: String,
    value: Value,
    is_tombstone: bool,
}

impl From<VarCandidateRow> for Candidate {
    fn from(r: VarCandidateRow) -> Self {
        Self {
            depth: r.depth,
            owner_kind: if r.owner_kind == "app" {
                OwnerKind::App
            } else {
                OwnerKind::Group
            },
            owner_id: r.owner_id,
            scope: r.environment_scope,
            key: r.key,
            value: r.value,
            is_tombstone: r.is_tombstone,
        }
    }
}

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

    fn cand(depth: i32, scope: &str, key: &str, value: Value) -> Candidate {
        Candidate {
            depth,
            owner_kind: if depth == 0 {
                OwnerKind::App
            } else {
                OwnerKind::Group
            },
            owner_id: Uuid::nil(),
            scope: scope.into(),
            key: key.into(),
            value,
            is_tombstone: false,
        }
    }

    fn tomb(depth: i32, scope: &str, key: &str) -> Candidate {
        Candidate {
            is_tombstone: true,
            value: Value::Null,
            ..cand(depth, scope, key, Value::Null)
        }
    }

    #[test]
    fn nearest_level_wins() {
        // app (depth 0) overrides group (depth 2).
        let (v, p) = resolve(vec![
            cand(2, "*", "region", json!("eu")),
            cand(0, "*", "region", json!("us")),
        ]);
        assert_eq!(v["region"], json!("us"));
        assert_eq!(p["region"].depth, 0);
    }

    #[test]
    fn env_scoped_beats_agnostic_within_a_level() {
        let (v, p) = resolve(vec![
            cand(1, "*", "db_url", json!("default")),
            cand(1, "staging", "db_url", json!("staging-db")),
        ]);
        assert_eq!(v["db_url"], json!("staging-db"));
        assert_eq!(p["db_url"].scope, "staging");
    }

    #[test]
    fn proximity_beats_env_specificity_across_levels() {
        // §3.2 deliberately-novel call: a leaf's `*` beats an ancestor's `@E`.
        let (v, _) = resolve(vec![
            cand(2, "production", "db_url", json!("anc-prod")),
            cand(0, "*", "db_url", json!("leaf-default")),
        ]);
        assert_eq!(v["db_url"], json!("leaf-default"));
    }

    #[test]
    fn maps_deep_merge_nearest_wins() {
        // ancestor sets {title, region}; leaf overrides title, adds locale.
        let (v, p) = resolve(vec![
            cand(2, "*", "ui", json!({"title": "Base", "region": "eu"})),
            cand(0, "*", "ui", json!({"title": "Leaf", "locale": "en"})),
        ]);
        assert_eq!(
            v["ui"],
            json!({"title": "Leaf", "region": "eu", "locale": "en"})
        );
        assert_eq!(p["ui"].merged_from.len(), 2);
    }

    #[test]
    fn same_level_env_map_suppresses_agnostic_map_no_merge() {
        // §3 step 1: within ONE level, `@E` is not a merge layer over `*` — it
        // shadows it. A level holding both an `@staging` map and a `*` map must
        // resolve to the `@staging` map alone, never a deep-merge of the two.
        let (v, p) = resolve(vec![
            cand(1, "*", "cfg", json!({"a": 1, "shared": "default"})),
            cand(1, "staging", "cfg", json!({"shared": "staging"})),
        ]);
        assert_eq!(v["cfg"], json!({"shared": "staging"}));
        assert_eq!(p["cfg"].scope, "staging");
        // Provenance must not list the suppressed `*` layer.
        assert_eq!(p["cfg"].merged_from, vec![(1, "staging".to_string())]);
    }

    #[test]
    fn same_level_env_map_then_ancestor_map_merges_without_agnostic_sibling() {
        // The suppressed same-level `*` must also be invisible to cross-level
        // merge: leaf `@staging` map merges onto the ancestor map, but the
        // leaf's own `*` map (shadowed at its level) never contributes.
        let (v, _) = resolve(vec![
            cand(2, "*", "cfg", json!({"region": "eu", "tier": "base"})),
            cand(0, "*", "cfg", json!({"leak": "should-not-appear"})),
            cand(0, "staging", "cfg", json!({"tier": "leaf"})),
        ]);
        assert_eq!(v["cfg"], json!({"region": "eu", "tier": "leaf"}));
        assert!(!v["cfg"].as_object().unwrap().contains_key("leak"));
    }

    #[test]
    fn nearer_scalar_replaces_whole_inherited_map() {
        let (v, _) = resolve(vec![
            cand(2, "*", "x", json!({"a": 1})),
            cand(0, "*", "x", json!("scalar")),
        ]);
        assert_eq!(v["x"], json!("scalar"));
    }

    #[test]
    fn tombstone_suppresses_inherited_key() {
        let (v, _) = resolve(vec![
            cand(2, "*", "secret_flag", json!(true)),
            tomb(0, "*", "secret_flag"),
        ]);
        assert!(!v.contains_key("secret_flag"));
    }

    #[test]
    fn json_null_is_a_value_not_a_deletion() {
        let (v, _) = resolve(vec![cand(0, "*", "k", Value::Null)]);
        assert!(v.contains_key("k"));
        assert_eq!(v["k"], Value::Null);
    }

    #[test]
    fn map_merge_stops_at_a_nearer_scalar_boundary() {
        // nearest map, then a scalar, then a farther map: only the
        // contiguous top map run merges; the scalar bounds it.
        let (v, _) = resolve(vec![
            cand(3, "*", "m", json!({"deep": 1})),
            cand(2, "*", "m", json!("scalar-boundary")),
            cand(0, "*", "m", json!({"near": 2})),
        ]);
        // depth 0 map is the only one above the depth-2 scalar boundary.
        assert_eq!(v["m"], json!({"near": 2}));
    }
}
