use super::*;
use rusqlite::OptionalExtension;

pub(in crate::context) fn pre_render_context_gate(
    conn: &rusqlite::Connection,
    invocation: &ContextInvocation,
    request: &ContextRequest,
    policy: &ContextPolicy,
    debug_enabled: bool,
) -> ContextGatePrecheckResult {
    if !host_is_gated(invocation.host) {
        return ContextGatePrecheckResult::off();
    }
    if resolve_gate_mode(invocation.gate_mode.as_deref()) != ContextGateMode::Strict {
        return ContextGatePrecheckResult::off();
    }
    if invocation.force || source_requires_fresh_emission(invocation.source.as_deref()) {
        return ContextGatePrecheckResult::off();
    }
    if !has_trusted_gate_identity(invocation) {
        return ContextGatePrecheckResult::off();
    }
    if debug_enabled {
        return ContextGatePrecheckResult::off();
    }
    let has_data_version = match data_version::context_injections_has_data_version(conn) {
        Ok(value) => value,
        Err(error) => {
            crate::log::warn(
                "context-gate",
                &format!("pre_render_skip reason=schema_check error={}", error),
            );
            return ContextGatePrecheckResult::miss(None);
        }
    };
    if !has_data_version {
        return ContextGatePrecheckResult::miss(None);
    }
    let current_data_version =
        match data_version::compute_data_version(conn, request, invocation, policy) {
            Ok(value) => value,
            Err(error) => {
                crate::log::warn(
                    "context-gate",
                    &format!("pre_render_skip reason=data_version error={}", error),
                );
                return ContextGatePrecheckResult::miss(None);
            }
        };

    let data_version = Some(current_data_version);
    let data_version_ref = data_version.as_deref();

    let key = injection_key(invocation);
    let now = chrono::Utc::now().timestamp();
    let row = match load_retained_gate_row(
        conn,
        invocation.host.as_env_value(),
        &key,
        retention_cutoff_epoch(now),
    ) {
        Ok(row) => row,
        Err(error) => {
            crate::log::warn(
                "context-gate",
                &format!("pre_render_skip reason=gate_read error={}", error),
            );
            return ContextGatePrecheckResult::miss(data_version);
        }
    };

    let Some(row) = row else {
        return ContextGatePrecheckResult::miss(data_version);
    };
    if row.data_version.as_deref() != data_version_ref {
        return ContextGatePrecheckResult::miss(data_version);
    }
    suppress_matching_row(conn, invocation, row, data_version)
}

pub(in crate::context) struct ContextGatePrecheckResult {
    pub(in crate::context) decision: Option<ContextGateDecision>,
    pub(in crate::context) precheck: ContextGatePrecheck,
    pub(in crate::context) data_version: Option<String>,
}

impl ContextGatePrecheckResult {
    fn off() -> Self {
        Self {
            decision: None,
            precheck: ContextGatePrecheck::Off,
            data_version: None,
        }
    }

    fn miss(data_version: Option<String>) -> Self {
        Self {
            decision: None,
            precheck: ContextGatePrecheck::Miss,
            data_version,
        }
    }

    fn hit(decision: ContextGateDecision, data_version: String) -> Self {
        Self {
            decision: Some(decision),
            precheck: ContextGatePrecheck::Hit,
            data_version: Some(data_version),
        }
    }
}

fn suppress_matching_row(
    conn: &rusqlite::Connection,
    invocation: &ContextInvocation,
    row: GateRow,
    data_version: Option<String>,
) -> ContextGatePrecheckResult {
    let Some(data_version) = data_version else {
        return ContextGatePrecheckResult::miss(None);
    };
    let key = injection_key(invocation);
    let now = chrono::Utc::now().timestamp();
    if !fallback_cooldown_allows_suppression(invocation, &row, now) {
        return ContextGatePrecheckResult::miss(Some(data_version));
    }
    if let Err(error) = record_suppression(conn, invocation, &key, Some(&data_version), now) {
        crate::log::warn(
            "context-gate",
            &format!("pre_render_skip reason=gate_write error={}", error),
        );
        return ContextGatePrecheckResult::miss(Some(data_version));
    }
    crate::log::info(
        "context-gate",
        &format!(
            "pre_render_suppress host={} key={} reason=data_version hash={} project={}",
            invocation.host.as_env_value(),
            key,
            row.context_hash,
            invocation.project
        ),
    );
    ContextGatePrecheckResult::hit(
        gate_decision(
            String::new(),
            ContextGateAction::Suppressed,
            "suppressed_data_version",
            &key,
            &row.context_hash,
            "suppressed",
        ),
        data_version,
    )
}

fn load_retained_gate_row(
    conn: &rusqlite::Connection,
    host: &str,
    key: &str,
    cutoff_epoch: i64,
) -> Result<Option<GateRow>> {
    conn.query_row(
        "SELECT context_hash, last_emitted_epoch, data_version
         FROM context_injections
         WHERE host = ?1
           AND injection_key = ?2
           AND updated_at_epoch >= ?3",
        rusqlite::params![host, key, cutoff_epoch],
        |row| {
            Ok(GateRow {
                context_hash: row.get(0)?,
                last_emitted_epoch: row.get(1)?,
                data_version: row.get(2)?,
            })
        },
    )
    .optional()
    .map_err(Into::into)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::policy::ContextLimits;
    use rusqlite::params;
    use std::ffi::OsString;

    struct ScopedEnvVar {
        key: &'static str,
        previous: Option<OsString>,
    }

    impl ScopedEnvVar {
        fn set(key: &'static str, value: &str) -> Self {
            let previous = std::env::var_os(key);
            std::env::set_var(key, value);
            Self { key, previous }
        }
    }

    impl Drop for ScopedEnvVar {
        fn drop(&mut self) {
            if let Some(previous) = self.previous.as_ref() {
                std::env::set_var(self.key, previous);
            } else {
                std::env::remove_var(self.key);
            }
        }
    }

    fn gate_invocation(session_id: Option<&str>) -> ContextInvocation {
        ContextInvocation {
            cwd: "/tmp/remem".to_string(),
            project: "/tmp/remem".to_string(),
            session_id: session_id.map(str::to_string),
            transcript_path: Some("/tmp/remem.jsonl".to_string()),
            source: None,
            host: HostKind::CodexCli,
            use_colors: false,
            debug: false,
            force: false,
            gate_mode: None,
        }
    }

    fn test_request(invocation: &ContextInvocation) -> ContextRequest {
        ContextRequest {
            cwd: invocation.cwd.clone(),
            project: invocation.project.clone(),
            session_id: invocation.session_id.clone(),
            hook_source: invocation.source.clone(),
            current_branch: None,
            host: invocation.host,
            use_colors: invocation.use_colors,
        }
    }

    fn test_policy() -> ContextPolicy {
        ContextPolicy::from_limits(ContextLimits::default())
    }

    fn apply_gate_with_current_data_version(
        conn: &rusqlite::Connection,
        invocation: &ContextInvocation,
        output: String,
    ) -> anyhow::Result<ContextGateDecision> {
        let request = test_request(invocation);
        let policy = test_policy();
        let data_version = data_version::compute_data_version(conn, &request, invocation, &policy)?;
        Ok(apply_context_gate_with_data_version(
            conn,
            invocation,
            output,
            Some(&data_version),
        ))
    }

    fn pre_render_for_test(
        conn: &rusqlite::Connection,
        invocation: &ContextInvocation,
    ) -> ContextGatePrecheckResult {
        let request = test_request(invocation);
        let policy = test_policy();
        pre_render_context_gate(conn, invocation, &request, &policy, false)
    }

    #[test]
    fn strict_pre_render_suppresses_existing_session_without_render_output() -> anyhow::Result<()> {
        let _data_dir = crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render");
        let mut invocation = gate_invocation(Some("sess-pre-render"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        let result = pre_render_for_test(&conn, &invocation);
        let Some(decision) = result.decision else {
            anyhow::bail!("existing strict gate row should suppress before render");
        };
        assert_eq!(result.precheck, ContextGatePrecheck::Hit);
        assert_eq!(decision.action, ContextGateAction::Suppressed);
        assert_eq!(decision.reason, "suppressed_data_version");
        assert!(decision.output.is_empty());
        assert_eq!(decision.output_mode, Some("suppressed"));
        assert_eq!(decision.context_hash, first.context_hash);
        Ok(())
    }

    #[test]
    fn strict_pre_render_does_not_suppress_first_session_context() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-first");
        let mut invocation = gate_invocation(Some("sess-pre-render-first"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        Ok(())
    }

    #[test]
    fn strict_pre_render_does_not_suppress_force_or_clear() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-clear");
        let mut invocation = gate_invocation(Some("sess-pre-render-clear"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        invocation.force = true;
        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Off);

        invocation.force = false;
        invocation.source = Some("clear".to_string());
        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Off);
        Ok(())
    }

    #[test]
    fn strict_pre_render_respects_retention_cutoff() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-retention");
        let _retention = ScopedEnvVar::set("REMEM_CONTEXT_GATE_RETENTION_DAYS", "30");
        let mut invocation = gate_invocation(Some("sess-pre-render-retention"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        let old_epoch = chrono::Utc::now().timestamp().saturating_sub(31 * 86_400);
        conn.execute(
            "UPDATE context_injections
             SET updated_at_epoch = ?1, last_emitted_epoch = ?1
             WHERE host = ?2 AND injection_key = ?3",
            params![
                old_epoch,
                invocation.host.as_env_value(),
                injection_key(&invocation)
            ],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        Ok(())
    }

    #[test]
    fn strict_pre_render_respects_fallback_cooldown_expiry() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-fallback");
        let _cooldown = ScopedEnvVar::set("REMEM_CONTEXT_GATE_FALLBACK_COOLDOWN_SECS", "900");
        let mut invocation = gate_invocation(None);
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        let old_last_emitted = chrono::Utc::now().timestamp().saturating_sub(901);
        conn.execute(
            "UPDATE context_injections
             SET last_emitted_epoch = ?1
             WHERE host = ?2 AND injection_key = ?3",
            params![
                old_last_emitted,
                invocation.host.as_env_value(),
                injection_key(&invocation)
            ],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        Ok(())
    }

    #[test]
    fn strict_pre_render_misses_after_project_memory_write() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-data-change");
        let mut invocation = gate_invocation(Some("sess-pre-render-data-change"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        crate::memory::insert_memory_full(
            &conn,
            Some("sess-pre-render-data-change"),
            &invocation.project,
            Some("context-gate-data-change"),
            "New context memory",
            "A relevant project memory changed.",
            "decision",
            None,
            None,
            "project",
            None,
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_some());
        Ok(())
    }

    #[test]
    fn strict_pre_render_misses_after_same_second_memory_content_change() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-memory-row");
        let mut invocation = gate_invocation(Some("sess-pre-render-memory-row"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;
        let memory_id = crate::memory::insert_memory_full(
            &conn,
            Some("sess-pre-render-memory-row"),
            &invocation.project,
            Some("context-gate-memory-row"),
            "Context gate row hash",
            "Original render-relevant memory text.",
            "decision",
            None,
            None,
            "project",
            None,
        )?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        conn.execute(
            "UPDATE memories
             SET content = 'Changed render-relevant memory text.'
             WHERE id = ?1",
            params![memory_id],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_some());
        Ok(())
    }

    #[test]
    fn strict_pre_render_misses_after_unknown_memory_type_content_change() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-custom-row");
        let mut invocation = gate_invocation(Some("sess-pre-render-custom-row"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;
        let memory_id = crate::memory::insert_memory_full(
            &conn,
            Some("sess-pre-render-custom-row"),
            &invocation.project,
            Some("custom/context-gate-row"),
            "Custom context row",
            "Original custom row text.",
            "custom_context",
            None,
            None,
            "project",
            None,
        )?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        conn.execute(
            "UPDATE memories
             SET content = 'Changed custom row text.'
             WHERE id = ?1",
            params![memory_id],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_some());
        Ok(())
    }

    #[test]
    fn strict_pre_render_misses_after_preference_content_change() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-pref-row");
        let mut invocation = gate_invocation(Some("sess-pre-render-pref-row"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;
        let memory_id = crate::memory::insert_memory_full(
            &conn,
            Some("sess-pre-render-pref-row"),
            &invocation.project,
            Some("preference/context-gate-row"),
            "Preference: context gate",
            "Original preference text.",
            "preference",
            None,
            None,
            "project",
            None,
        )?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        conn.execute(
            "UPDATE memories
             SET content = 'Changed preference text.'
             WHERE id = ?1",
            params![memory_id],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_some());
        Ok(())
    }

    #[test]
    fn strict_pre_render_misses_after_workstream_next_action_change() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-workstream");
        let mut invocation = gate_invocation(Some("sess-pre-render-workstream"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;
        conn.execute(
            "INSERT INTO workstreams
             (project, title, status, next_action, created_at_epoch, updated_at_epoch,
              source_project, target_project, owner_scope, owner_key, context_class)
             VALUES (?1, 'Context gate workstream', 'active', 'Original next action',
                     ?2, ?2, ?1, ?1, 'repo', ?1, 'startup_core')",
            params![invocation.project, chrono::Utc::now().timestamp()],
        )?;
        let workstream_id = conn.last_insert_rowid();

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        conn.execute(
            "UPDATE workstreams
             SET next_action = 'Changed next action'
             WHERE id = ?1",
            params![workstream_id],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_some());
        Ok(())
    }

    #[test]
    fn strict_pre_render_ignores_workstream_past_render_limit() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-work-limit");
        let mut invocation = gate_invocation(Some("sess-pre-render-work-limit"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;
        for idx in 0..6 {
            conn.execute(
                "INSERT INTO workstreams
                 (project, title, status, next_action, created_at_epoch, updated_at_epoch,
                  source_project, target_project, owner_scope, owner_key, context_class)
                 VALUES (?1, ?2, 'active', ?3, ?4, ?4, ?1, ?1, 'repo', ?1, 'startup_core')",
                params![
                    invocation.project,
                    format!("Context gate workstream {idx}"),
                    format!("Next action {idx}"),
                    2_000_i64 - idx,
                ],
            )?;
        }

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        conn.execute(
            "UPDATE workstreams
             SET next_action = 'Changed offscreen next action'
             WHERE title = 'Context gate workstream 5'",
            [],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        let Some(decision) = result.decision else {
            anyhow::bail!("off-limit workstream should not invalidate rendered context");
        };
        assert_eq!(result.precheck, ContextGatePrecheck::Hit);
        assert_eq!(decision.reason, "suppressed_data_version");
        Ok(())
    }

    #[test]
    fn strict_pre_render_falls_open_when_data_version_query_fails() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-fail-open");
        let mut invocation = gate_invocation(Some("sess-pre-render-fail-open"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);
        conn.execute("DROP TABLE memories", [])?;

        let result = pre_render_for_test(&conn, &invocation);
        assert!(result.decision.is_none());
        assert_eq!(result.precheck, ContextGatePrecheck::Miss);
        assert!(result.data_version.is_none());
        Ok(())
    }

    #[test]
    fn strict_pre_render_records_suppression_refresh() -> anyhow::Result<()> {
        let _data_dir =
            crate::db::test_support::ScopedTestDataDir::new("context-gate-pre-render-refresh");
        let mut invocation = gate_invocation(Some("sess-pre-render-refresh"));
        invocation.gate_mode = Some("strict".to_string());
        let conn = crate::db::test_support::runtime_connection()?;

        let first = apply_gate_with_current_data_version(
            &conn,
            &invocation,
            "# [/tmp/remem] context now\nBody A\n".to_string(),
        )?;
        assert_eq!(first.action, ContextGateAction::EmittedFull);

        let old_epoch = chrono::Utc::now().timestamp().saturating_sub(60);
        conn.execute(
            "UPDATE context_injections
             SET updated_at_epoch = ?1, suppress_count = 0
             WHERE host = ?2 AND injection_key = ?3",
            params![
                old_epoch,
                invocation.host.as_env_value(),
                injection_key(&invocation)
            ],
        )?;

        let result = pre_render_for_test(&conn, &invocation);
        let Some(decision) = result.decision else {
            anyhow::bail!("existing strict gate row should suppress before render");
        };
        assert_eq!(result.precheck, ContextGatePrecheck::Hit);
        assert_eq!(decision.action, ContextGateAction::Suppressed);

        let (updated_at_epoch, suppress_count): (i64, i64) = conn.query_row(
            "SELECT updated_at_epoch, suppress_count
             FROM context_injections
             WHERE host = ?1 AND injection_key = ?2",
            params![invocation.host.as_env_value(), injection_key(&invocation)],
            |row| Ok((row.get(0)?, row.get(1)?)),
        )?;
        assert!(updated_at_epoch > old_epoch);
        assert_eq!(suppress_count, 1);
        Ok(())
    }
}
