use std::future::Future;
use std::sync::Arc;
use std::time::Duration;

use codex_analytics::CompactionTrigger;
use codex_analytics::HookRunFact;
use codex_analytics::build_track_events_context;
use codex_hooks::PermissionRequestDecision;
use codex_hooks::PermissionRequestOutcome;
use codex_hooks::PermissionRequestRequest;
use codex_hooks::PostToolUseOutcome;
use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
use codex_hooks::PreToolUseRequest;
use codex_hooks::SessionStartOutcome;
use codex_hooks::StartHookTarget;
use codex_hooks::StopHookTarget;
use codex_hooks::StopOutcome;
use codex_hooks::SubagentHookContext;
use codex_hooks::UserPromptSubmitOutcome;
use codex_hooks::UserPromptSubmitRequest;
use codex_otel::HOOK_RUN_DURATION_METRIC;
use codex_otel::HOOK_RUN_METRIC;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::HookCompletedEvent;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::protocol::HookSource;
use codex_protocol::protocol::HookStartedEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_thread_store::ReadThreadParams;
use serde_json::Value;
use tracing::instrument;

use crate::context::ContextualUserFragment;
use crate::context::HookAdditionalContext;
use crate::event_mapping::parse_turn_item;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::hook_names::HookToolName;
use crate::tools::sandboxing::PermissionRequestPayload;

pub(crate) struct HookRuntimeOutcome {
    pub should_stop: bool,
    pub additional_contexts: Vec<String>,
}

pub(crate) enum PreToolUseHookResult {
    Continue { updated_input: Option<Value> },
    Blocked(String),
}

struct ContextInjectingHookOutcome {
    hook_events: Vec<HookCompletedEvent>,
    outcome: HookRuntimeOutcome,
}

impl From<SessionStartOutcome> for ContextInjectingHookOutcome {
    fn from(value: SessionStartOutcome) -> Self {
        let SessionStartOutcome {
            hook_events,
            should_stop,
            stop_reason: _,
            additional_contexts,
        } = value;
        Self {
            hook_events,
            outcome: HookRuntimeOutcome {
                should_stop,
                additional_contexts,
            },
        }
    }
}

impl From<UserPromptSubmitOutcome> for ContextInjectingHookOutcome {
    fn from(value: UserPromptSubmitOutcome) -> Self {
        let UserPromptSubmitOutcome {
            hook_events,
            should_stop,
            stop_reason: _,
            additional_contexts,
        } = value;
        Self {
            hook_events,
            outcome: HookRuntimeOutcome {
                should_stop,
                additional_contexts,
            },
        }
    }
}

#[instrument(level = "trace", skip_all)]
pub(crate) async fn run_pending_session_start_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
) -> bool {
    while let Some(session_start_source) = sess.take_pending_session_start_source().await {
        // Pending session-start hooks are reused to dispatch thread-spawn subagent
        // starts. Other subagent sessions are internal/system work and do not run
        // start hooks.
        let target = match &turn_context.session_source {
            SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. })
                if matches!(
                    session_start_source,
                    codex_hooks::SessionStartSource::Startup
                ) =>
            {
                let context = subagent_hook_context(sess, agent_role);
                StartHookTarget::SubagentStart {
                    turn_id: turn_context.sub_id.clone(),
                    agent_id: context.agent_id,
                    agent_type: context.agent_type,
                }
            }
            SessionSource::SubAgent(_) => return false,
            _ => StartHookTarget::SessionStart {
                source: session_start_source,
            },
        };
        let request = codex_hooks::SessionStartRequest {
            session_id: sess.session_id().into(),
            #[allow(deprecated)]
            cwd: turn_context.cwd.clone(),
            transcript_path: sess.hook_transcript_path().await,
            model: turn_context.model_info.slug.clone(),
            permission_mode: hook_permission_mode(turn_context),
            target,
        };
        let hooks = sess.hooks();
        let preview_runs = hooks.preview_session_start(&request);
        if run_context_injecting_hook(
            sess,
            turn_context,
            preview_runs,
            hooks.run_session_start(request, Some(turn_context.sub_id.clone())),
        )
        .await
        .record_additional_contexts(sess, turn_context)
        .await
        {
            return true;
        }
    }

    false
}

/// Runs matching `PreToolUse` hooks before a tool executes.
///
/// `tool_name` is the canonical name serialized to hook stdin. Matcher aliases
/// are internal compatibility names used only for selecting configured hook
/// handlers.
pub(crate) async fn run_pre_tool_use_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    tool_use_id: String,
    tool_name: &HookToolName,
    tool_input: &Value,
) -> PreToolUseHookResult {
    let request = PreToolUseRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        subagent: thread_spawn_subagent_hook_context(sess, turn_context),
        #[allow(deprecated)]
        cwd: turn_context.cwd.clone(),
        transcript_path: sess.hook_transcript_path().await,
        model: turn_context.model_info.slug.clone(),
        permission_mode: hook_permission_mode(turn_context),
        tool_name: tool_name.name().to_string(),
        matcher_aliases: tool_name.matcher_aliases().to_vec(),
        tool_use_id,
        tool_input: tool_input.clone(),
    };
    let hooks = sess.hooks();
    let preview_runs = hooks.preview_pre_tool_use(&request);
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let PreToolUseOutcome {
        hook_events,
        should_block,
        block_reason,
        additional_contexts,
        updated_input,
    } = hooks.run_pre_tool_use(request).await;
    emit_hook_completed_events(sess, turn_context, hook_events).await;
    record_additional_contexts(sess, turn_context, additional_contexts).await;

    if !should_block {
        return PreToolUseHookResult::Continue { updated_input };
    }

    let Some(reason) = block_reason else {
        return PreToolUseHookResult::Continue {
            updated_input: None,
        };
    };

    if (tool_name.name() == "Bash" || tool_name.name() == "apply_patch")
        && let Some(command) = tool_input.get("command").and_then(Value::as_str)
    {
        PreToolUseHookResult::Blocked(format!(
            "Command blocked by PreToolUse hook: {reason}. Command: {command}"
        ))
    } else {
        PreToolUseHookResult::Blocked(format!(
            "Tool call blocked by PreToolUse hook: {reason}. Tool: {}",
            tool_name.name()
        ))
    }
}

// PermissionRequest hooks share the same preview/start/completed event flow as
// other hook types, but they return an optional decision instead of mutating
// tool input or post-run state.
pub(crate) async fn run_permission_request_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    run_id_suffix: &str,
    payload: PermissionRequestPayload,
) -> Option<PermissionRequestDecision> {
    let request = PermissionRequestRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        subagent: thread_spawn_subagent_hook_context(sess, turn_context),
        #[allow(deprecated)]
        cwd: turn_context.cwd.to_path_buf(),
        transcript_path: sess.hook_transcript_path().await,
        model: turn_context.model_info.slug.clone(),
        permission_mode: hook_permission_mode(turn_context),
        tool_name: payload.tool_name.name().to_string(),
        matcher_aliases: payload.tool_name.matcher_aliases().to_vec(),
        run_id_suffix: run_id_suffix.to_string(),
        tool_input: payload.tool_input,
    };
    let hooks = sess.hooks();
    let preview_runs = hooks.preview_permission_request(&request);
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let PermissionRequestOutcome {
        hook_events,
        decision,
    } = hooks.run_permission_request(request).await;
    emit_hook_completed_events(sess, turn_context, hook_events).await;

    decision
}

/// Runs matching `PostToolUse` hooks after a tool has produced a successful output.
///
/// The `tool_name`, matcher aliases, `tool_input`, and `tool_response` values are
/// already adapted by the tool handler into the stable hook contract. Passing
/// raw internal tool data here would leak implementation details into user hook
/// matchers and hook logs.
pub(crate) async fn run_post_tool_use_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    tool_use_id: String,
    tool_name: String,
    matcher_aliases: Vec<String>,
    tool_input: Value,
    tool_response: Value,
) -> PostToolUseOutcome {
    let request = PostToolUseRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        subagent: thread_spawn_subagent_hook_context(sess, turn_context),
        #[allow(deprecated)]
        cwd: turn_context.cwd.clone(),
        transcript_path: sess.hook_transcript_path().await,
        model: turn_context.model_info.slug.clone(),
        permission_mode: hook_permission_mode(turn_context),
        tool_name,
        matcher_aliases,
        tool_use_id,
        tool_input,
        tool_response,
    };
    let hooks = sess.hooks();
    let preview_runs = hooks.preview_post_tool_use(&request);
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let outcome = hooks.run_post_tool_use(request).await;
    emit_hook_completed_events(sess, turn_context, outcome.hook_events.clone()).await;
    outcome
}

#[instrument(level = "trace", skip_all)]
pub(crate) async fn run_turn_stop_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    stop_hook_active: bool,
    last_assistant_message: Option<String>,
) -> StopOutcome {
    // Resolve the stop hook kind from the session source before building the
    // request. Root turns run Stop; thread-spawned child turns run SubagentStop.
    let (target, transcript_path) = match &turn_context.session_source {
        SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
            agent_role,
            parent_thread_id,
            ..
        }) => {
            let context = subagent_hook_context(sess, agent_role);
            let agent_transcript_path = sess.hook_transcript_path().await;
            let parent_transcript_path = match sess
                .services
                .thread_store
                .read_thread(ReadThreadParams {
                    thread_id: *parent_thread_id,
                    include_archived: true,
                    include_history: false,
                })
                .await
            {
                Ok(thread) => thread.rollout_path,
                Err(error) => {
                    tracing::warn!(
                        parent_thread_id = %parent_thread_id,
                        error = %error,
                        "failed to resolve parent transcript path for subagent hook"
                    );
                    None
                }
            };
            (
                StopHookTarget::SubagentStop {
                    agent_id: context.agent_id,
                    agent_type: context.agent_type,
                    agent_transcript_path,
                },
                parent_transcript_path,
            )
        }
        // Internal/synthetic subagents do not expose user-configured lifecycle
        // hooks, so there is no Stop or SubagentStop request to dispatch.
        SessionSource::SubAgent(_) => return StopOutcome::default(),
        _ => (StopHookTarget::Stop, sess.hook_transcript_path().await),
    };
    let request = codex_hooks::StopRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        #[allow(deprecated)]
        cwd: turn_context.cwd.clone(),
        transcript_path,
        model: turn_context.model_info.slug.clone(),
        permission_mode: hook_permission_mode(turn_context),
        stop_hook_active,
        last_assistant_message,
        target,
    };
    let hooks = sess.hooks();
    emit_hook_started_events(sess, turn_context, hooks.preview_stop(&request)).await;

    let mut outcome = hooks.run_stop(request).await;
    emit_hook_completed_events(sess, turn_context, std::mem::take(&mut outcome.hook_events)).await;
    outcome
}

pub(crate) async fn run_pre_compact_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    trigger: CompactionTrigger,
) -> PreCompactHookOutcome {
    let request = codex_hooks::PreCompactRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        subagent: thread_spawn_subagent_hook_context(sess, turn_context),
        #[allow(deprecated)]
        cwd: turn_context.cwd.clone(),
        transcript_path: sess.hook_transcript_path().await,
        model: turn_context.model_info.slug.clone(),
        trigger: compaction_trigger_label(trigger).to_string(),
    };
    let preview_runs = sess.hooks().preview_pre_compact(&request);
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let outcome = sess.hooks().run_pre_compact(request).await;
    emit_hook_completed_events(sess, turn_context, outcome.hook_events).await;
    if outcome.should_stop {
        PreCompactHookOutcome::Stopped
    } else {
        PreCompactHookOutcome::Continue
    }
}

pub(crate) enum PreCompactHookOutcome {
    Continue,
    Stopped,
}

pub(crate) enum PostCompactHookOutcome {
    Continue,
    Stopped,
}

pub(crate) async fn run_post_compact_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    trigger: CompactionTrigger,
) -> PostCompactHookOutcome {
    let request = codex_hooks::PostCompactRequest {
        session_id: sess.session_id().into(),
        turn_id: turn_context.sub_id.clone(),
        subagent: thread_spawn_subagent_hook_context(sess, turn_context),
        #[allow(deprecated)]
        cwd: turn_context.cwd.clone(),
        transcript_path: sess.hook_transcript_path().await,
        model: turn_context.model_info.slug.clone(),
        trigger: compaction_trigger_label(trigger).to_string(),
    };
    let preview_runs = sess.hooks().preview_post_compact(&request);
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let outcome = sess.hooks().run_post_compact(request).await;
    emit_hook_completed_events(sess, turn_context, outcome.hook_events).await;
    if outcome.should_stop {
        PostCompactHookOutcome::Stopped
    } else {
        PostCompactHookOutcome::Continue
    }
}

#[instrument(level = "trace", skip_all)]
pub(crate) async fn run_legacy_after_agent_hook(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    input: &[ResponseItem],
    last_assistant_message: Option<String>,
) -> bool {
    let mut abort_message = None;
    let input_messages = input
        .iter()
        .filter_map(|item| match parse_turn_item(item) {
            Some(TurnItem::UserMessage(user_message)) => Some(user_message.message()),
            _ => None,
        })
        .collect();
    let hooks = sess.hooks();
    for hook_outcome in hooks
        .dispatch(codex_hooks::HookPayload {
            session_id: sess.session_id().into(),
            #[allow(deprecated)]
            cwd: turn_context.cwd.clone(),
            client: turn_context.app_server_client_name.clone(),
            triggered_at: chrono::Utc::now(),
            hook_event: codex_hooks::HookEvent::AfterAgent {
                event: codex_hooks::HookEventAfterAgent {
                    thread_id: sess.thread_id,
                    turn_id: turn_context.sub_id.clone(),
                    input_messages,
                    last_assistant_message,
                },
            },
        })
        .await
    {
        let hook_name = hook_outcome.hook_name;
        let (error, should_abort) = match hook_outcome.result {
            codex_hooks::HookResult::Success => continue,
            codex_hooks::HookResult::FailedContinue(error) => (error, false),
            codex_hooks::HookResult::FailedAbort(error) => (error, true),
        };
        let action = if should_abort {
            "aborting operation"
        } else {
            "continuing"
        };
        tracing::warn!(
            turn_id = %turn_context.sub_id,
            hook_name = %hook_name,
            error = %error,
            "after_agent hook failed; {action}"
        );
        if should_abort && abort_message.is_none() {
            abort_message = Some(format!(
                "after_agent hook '{hook_name}' failed and aborted turn completion: {error}"
            ));
        }
    }
    let Some(message) = abort_message else {
        return false;
    };
    let event = EventMsg::Error(codex_protocol::protocol::ErrorEvent {
        message,
        codex_error_info: None,
    });
    sess.send_event(turn_context, event).await;
    true
}

pub(crate) async fn inspect_pending_input(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    pending_input_item: &TurnInput,
) -> HookRuntimeOutcome {
    match pending_input_item {
        TurnInput::UserInput { content, .. } => {
            let request = UserPromptSubmitRequest {
                session_id: sess.session_id().into(),
                turn_id: turn_context.sub_id.clone(),
                subagent: thread_spawn_subagent_hook_context(sess, turn_context),
                #[allow(deprecated)]
                cwd: turn_context.cwd.clone(),
                transcript_path: sess.hook_transcript_path().await,
                model: turn_context.model_info.slug.clone(),
                permission_mode: hook_permission_mode(turn_context),
                prompt: UserMessageItem::new(content).message(),
            };
            let hooks = sess.hooks();
            let preview_runs = hooks.preview_user_prompt_submit(&request);
            run_context_injecting_hook(
                sess,
                turn_context,
                preview_runs,
                hooks.run_user_prompt_submit(request),
            )
            .await
        }
        TurnInput::ResponseItem(_) => HookRuntimeOutcome {
            should_stop: false,
            additional_contexts: Vec::new(),
        },
        TurnInput::InterAgentCommunication(_) => HookRuntimeOutcome {
            should_stop: false,
            additional_contexts: Vec::new(),
        },
    }
}

pub(crate) async fn record_pending_input(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    pending_input: TurnInput,
    additional_contexts: Vec<String>,
) {
    match pending_input {
        TurnInput::UserInput { content, client_id } => {
            sess.record_user_prompt_and_emit_turn_item(
                turn_context.as_ref(),
                content.as_slice(),
                client_id,
            )
            .await;
        }
        TurnInput::ResponseItem(item) => {
            sess.record_conversation_items(turn_context, std::slice::from_ref(&item))
                .await;
        }
        TurnInput::InterAgentCommunication(communication) => {
            sess.record_inter_agent_communication(turn_context, communication)
                .await;
        }
    }
    record_additional_contexts(sess, turn_context, additional_contexts).await;
}

async fn run_context_injecting_hook<Fut, Outcome>(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    preview_runs: Vec<HookRunSummary>,
    outcome_future: Fut,
) -> HookRuntimeOutcome
where
    Fut: Future<Output = Outcome>,
    Outcome: Into<ContextInjectingHookOutcome>,
{
    emit_hook_started_events(sess, turn_context, preview_runs).await;

    let outcome = outcome_future.await.into();
    emit_hook_completed_events(sess, turn_context, outcome.hook_events).await;
    outcome.outcome
}

impl HookRuntimeOutcome {
    async fn record_additional_contexts(
        self,
        sess: &Arc<Session>,
        turn_context: &Arc<TurnContext>,
    ) -> bool {
        record_additional_contexts(sess, turn_context, self.additional_contexts).await;

        self.should_stop
    }
}

pub(crate) async fn record_additional_contexts(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    additional_contexts: Vec<String>,
) {
    let developer_messages = additional_context_messages(additional_contexts);
    if developer_messages.is_empty() {
        return;
    }

    sess.record_conversation_items(turn_context, developer_messages.as_slice())
        .await;
}

fn additional_context_messages(additional_contexts: Vec<String>) -> Vec<ResponseItem> {
    additional_contexts
        .into_iter()
        .map(HookAdditionalContext::new)
        .map(ContextualUserFragment::into)
        .collect()
}

async fn emit_hook_started_events(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    preview_runs: Vec<HookRunSummary>,
) {
    for run in preview_runs {
        sess.send_event(
            turn_context,
            EventMsg::HookStarted(HookStartedEvent {
                turn_id: Some(turn_context.sub_id.clone()),
                run,
            }),
        )
        .await;
    }
}

pub(crate) async fn emit_hook_completed_events(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    completed_events: Vec<HookCompletedEvent>,
) {
    for completed in completed_events {
        emit_hook_completed_metrics(turn_context, &completed);
        track_hook_completed_analytics(sess, turn_context, &completed);
        sess.send_event(turn_context, EventMsg::HookCompleted(completed))
            .await;
    }
}

fn emit_hook_completed_metrics(turn_context: &TurnContext, completed: &HookCompletedEvent) {
    let tags = hook_run_metric_tags(&completed.run);
    turn_context
        .session_telemetry
        .counter(HOOK_RUN_METRIC, /*inc*/ 1, &tags);
    if let Some(duration_ms) = completed.run.duration_ms
        && let Ok(duration_ms) = u64::try_from(duration_ms)
    {
        turn_context.session_telemetry.record_duration(
            HOOK_RUN_DURATION_METRIC,
            Duration::from_millis(duration_ms),
            &tags,
        );
    }
}

fn track_hook_completed_analytics(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    completed: &HookCompletedEvent,
) {
    let (tracking, hook) =
        hook_run_analytics_payload(sess.thread_id.to_string(), turn_context, completed);
    sess.services
        .analytics_events_client
        .track_hook_run(tracking, hook);
}

fn hook_run_analytics_payload(
    thread_id: String,
    turn_context: &TurnContext,
    completed: &HookCompletedEvent,
) -> (codex_analytics::TrackEventsContext, HookRunFact) {
    (
        build_track_events_context(
            turn_context.model_info.slug.clone(),
            thread_id,
            completed
                .turn_id
                .clone()
                .unwrap_or_else(|| turn_context.sub_id.clone()),
        ),
        HookRunFact {
            event_name: completed.run.event_name,
            hook_source: completed.run.source,
            status: completed.run.status,
        },
    )
}

fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str); 3] {
    let hook_name = match run.event_name {
        HookEventName::PreToolUse => "PreToolUse",
        HookEventName::PermissionRequest => "PermissionRequest",
        HookEventName::PostToolUse => "PostToolUse",
        HookEventName::PreCompact => "PreCompact",
        HookEventName::PostCompact => "PostCompact",
        HookEventName::SessionStart => "SessionStart",
        HookEventName::UserPromptSubmit => "UserPromptSubmit",
        HookEventName::SubagentStart => "SubagentStart",
        HookEventName::SubagentStop => "SubagentStop",
        HookEventName::Stop => "Stop",
    };
    let hook_source = match run.source {
        HookSource::System => "system",
        HookSource::User => "user",
        HookSource::Project => "project",
        HookSource::Mdm => "mdm",
        HookSource::SessionFlags => "session_flags",
        HookSource::Plugin => "plugin",
        HookSource::CloudRequirements => "cloud_requirements",
        HookSource::CloudManagedConfig => "cloud_managed_config",
        HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
        HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
        HookSource::Unknown => "unknown",
    };
    let status = match run.status {
        HookRunStatus::Running => "running",
        HookRunStatus::Completed => "completed",
        HookRunStatus::Failed => "failed",
        HookRunStatus::Blocked => "blocked",
        HookRunStatus::Stopped => "stopped",
    };

    [
        ("hook_name", hook_name),
        ("source", hook_source),
        ("status", status),
    ]
}

fn hook_permission_mode(turn_context: &TurnContext) -> String {
    match turn_context.approval_policy.value() {
        AskForApproval::Never => "bypassPermissions",
        AskForApproval::UnlessTrusted
        | AskForApproval::OnFailure
        | AskForApproval::OnRequest
        | AskForApproval::Granular(_) => "default",
    }
    .to_string()
}

fn thread_spawn_subagent_hook_context(
    sess: &Arc<Session>,
    turn_context: &TurnContext,
) -> Option<SubagentHookContext> {
    match &turn_context.session_source {
        SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => {
            Some(subagent_hook_context(sess, agent_role))
        }
        _ => None,
    }
}

fn subagent_hook_context(sess: &Arc<Session>, agent_role: &Option<String>) -> SubagentHookContext {
    SubagentHookContext {
        agent_id: sess.thread_id().to_string(),
        agent_type: agent_role
            .clone()
            .unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string()),
    }
}

fn compaction_trigger_label(value: CompactionTrigger) -> &'static str {
    match value {
        CompactionTrigger::Manual => "manual",
        CompactionTrigger::Auto => "auto",
    }
}

#[cfg(test)]
mod tests {
    use codex_protocol::models::ContentItem;
    use codex_protocol::protocol::HookEventName;
    use codex_protocol::protocol::HookExecutionMode;
    use codex_protocol::protocol::HookHandlerType;
    use codex_protocol::protocol::HookRunStatus;
    use codex_protocol::protocol::HookScope;
    use codex_protocol::protocol::HookSource;
    use pretty_assertions::assert_eq;

    use super::additional_context_messages;
    use super::hook_run_analytics_payload;
    use super::hook_run_metric_tags;
    use crate::session::tests::make_session_and_context;
    use codex_protocol::protocol::HookCompletedEvent;
    use codex_protocol::protocol::HookRunSummary;
    use codex_utils_absolute_path::test_support::PathBufExt;
    use codex_utils_absolute_path::test_support::test_path_buf;

    #[test]
    fn additional_context_messages_stay_separate_and_ordered() {
        let messages = additional_context_messages(vec![
            "first tide note".to_string(),
            "second tide note".to_string(),
        ]);

        assert_eq!(messages.len(), 2);
        assert_eq!(
            messages
                .iter()
                .map(|message| match message {
                    codex_protocol::models::ResponseItem::Message { role, content, .. } => {
                        let text = content
                            .iter()
                            .map(|item| match item {
                                ContentItem::InputText { text } => text.as_str(),
                                ContentItem::InputImage { .. } | ContentItem::OutputText { .. } => {
                                    panic!("expected input text content, got {item:?}")
                                }
                            })
                            .collect::<String>();
                        (role.as_str(), text)
                    }
                    other => panic!("expected developer message, got {other:?}"),
                })
                .collect::<Vec<_>>(),
            vec![
                ("developer", "first tide note".to_string()),
                ("developer", "second tide note".to_string()),
            ],
        );
    }

    #[tokio::test]
    async fn hook_run_analytics_payload_uses_completed_turn_id() {
        let (_session, turn_context) = make_session_and_context().await;
        let completed = HookCompletedEvent {
            turn_id: Some("turn-from-hook".to_string()),
            run: sample_hook_run(HookRunStatus::Blocked, HookSource::Project),
        };

        let (tracking, hook) =
            hook_run_analytics_payload("thread-123".to_string(), &turn_context, &completed);

        assert_eq!(tracking.thread_id, "thread-123");
        assert_eq!(tracking.turn_id, "turn-from-hook");
        assert_eq!(tracking.model_slug, turn_context.model_info.slug);
        assert_eq!(hook.event_name, HookEventName::Stop);
        assert_eq!(hook.hook_source, HookSource::Project);
        assert_eq!(hook.status, HookRunStatus::Blocked);
    }

    #[tokio::test]
    async fn hook_run_analytics_payload_falls_back_to_turn_context_id() {
        let (_session, turn_context) = make_session_and_context().await;
        let completed = HookCompletedEvent {
            turn_id: None,
            run: sample_hook_run(HookRunStatus::Failed, HookSource::Unknown),
        };

        let (tracking, hook) =
            hook_run_analytics_payload("thread-123".to_string(), &turn_context, &completed);

        assert_eq!(tracking.turn_id, turn_context.sub_id);
        assert_eq!(hook.hook_source, HookSource::Unknown);
        assert_eq!(hook.status, HookRunStatus::Failed);
    }

    #[test]
    fn hook_run_metric_tags_match_analytics_shape() {
        let run = sample_hook_run(HookRunStatus::Blocked, HookSource::Project);

        assert_eq!(
            hook_run_metric_tags(&run),
            [
                ("hook_name", "Stop"),
                ("source", "project"),
                ("status", "blocked"),
            ]
        );

        let cloud_requirements =
            sample_hook_run(HookRunStatus::Blocked, HookSource::CloudRequirements);

        assert_eq!(
            hook_run_metric_tags(&cloud_requirements),
            [
                ("hook_name", "Stop"),
                ("source", "cloud_requirements"),
                ("status", "blocked"),
            ]
        );
    }

    #[test]
    fn hook_run_metric_tags_include_expanded_hook_sources() {
        let run = sample_hook_run(HookRunStatus::Completed, HookSource::LegacyManagedConfigMdm);

        assert_eq!(
            hook_run_metric_tags(&run),
            [
                ("hook_name", "Stop"),
                ("source", "legacy_managed_config_mdm"),
                ("status", "completed"),
            ]
        );
    }

    fn sample_hook_run(status: HookRunStatus, source: HookSource) -> HookRunSummary {
        HookRunSummary {
            id: "stop:0:/tmp/hooks.json".to_string(),
            event_name: HookEventName::Stop,
            handler_type: HookHandlerType::Command,
            execution_mode: HookExecutionMode::Sync,
            scope: HookScope::Turn,
            source_path: test_path_buf("/tmp/hooks.json").abs(),
            source,
            display_order: 0,
            status,
            status_message: None,
            started_at: 10,
            completed_at: Some(37),
            duration_ms: Some(27),
            entries: Vec::new(),
        }
    }
}
