use std::collections::BTreeMap;
use std::collections::HashMap;

use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionPhase;
use codex_analytics::CompactionReason;
use codex_analytics::CompactionStrategy;
use codex_analytics::CompactionTrigger;
use codex_protocol::ThreadId;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_string::to_ascii_json_string;
use http::HeaderMap as ApiHeaderMap;
use http::HeaderValue;
use serde::Serialize;
use serde_json::Value;

use crate::client::X_CODEX_INSTALLATION_ID_HEADER;
use crate::client::X_CODEX_PARENT_THREAD_ID_HEADER;
use crate::client::X_CODEX_TURN_METADATA_HEADER;
use crate::client::X_CODEX_WINDOW_ID_HEADER;
use crate::client::X_OPENAI_SUBAGENT_HEADER;

pub(crate) const INSTALLATION_ID_KEY: &str = "installation_id";
pub(crate) const SESSION_ID_KEY: &str = "session_id";
pub(crate) const THREAD_ID_KEY: &str = "thread_id";
pub(crate) const TURN_ID_KEY: &str = "turn_id";
pub(crate) const WINDOW_ID_KEY: &str = "window_id";
pub(crate) const REQUEST_KIND_KEY: &str = "request_kind";
pub(crate) const COMPACTION_KEY: &str = "compaction";
pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";

pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id";
pub(crate) const PARENT_THREAD_ID_KEY: &str = "parent_thread_id";
pub(crate) const SUBAGENT_KIND_KEY: &str = "subagent_kind";
pub(crate) const SANDBOX_KEY: &str = "sandbox";
pub(crate) const WORKSPACES_KEY: &str = "workspaces";

// App-server clients can specify additional metadata in the `responsesapi_client_metadata` param
// when submitting a turn, but they must not override fields owned by core.
const RESERVED_METADATA_KEYS: &[&str] = &[
    INSTALLATION_ID_KEY,
    X_CODEX_INSTALLATION_ID_HEADER,
    SESSION_ID_KEY,
    THREAD_ID_KEY,
    TURN_ID_KEY,
    WINDOW_ID_KEY,
    X_CODEX_WINDOW_ID_HEADER,
    X_CODEX_TURN_METADATA_HEADER,
    X_CODEX_PARENT_THREAD_ID_HEADER,
    X_OPENAI_SUBAGENT_HEADER,
    REQUEST_KIND_KEY,
    COMPACTION_KEY,
    TURN_STARTED_AT_UNIX_MS_KEY,
    FORKED_FROM_THREAD_ID_KEY,
    PARENT_THREAD_ID_KEY,
    SUBAGENT_KIND_KEY,
    SANDBOX_KEY,
    WORKSPACES_KEY,
];

/// Metadata attached to model requests whose purpose is conversation compaction.
///
/// This covers both local compaction requests sent through the normal `/responses` path and remote
/// compaction requests sent through `/responses/compact`. These fields describe the operation at
/// dispatch time. Post-response outcomes such as status, error, duration, and token deltas remain
/// in compaction analytics events.
#[derive(Clone, Copy, Debug, Serialize)]
pub(crate) struct CompactionTurnMetadata {
    trigger: CompactionTrigger,
    reason: CompactionReason,
    implementation: CompactionImplementation,
    phase: CompactionPhase,
    strategy: CompactionStrategy,
}

impl CompactionTurnMetadata {
    pub(crate) fn new(
        trigger: CompactionTrigger,
        reason: CompactionReason,
        implementation: CompactionImplementation,
        phase: CompactionPhase,
    ) -> Self {
        Self {
            trigger,
            reason,
            implementation,
            phase,
            strategy: CompactionStrategy::Memento,
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub(crate) enum CodexResponsesRequestKind {
    Turn,
    Prewarm,
    Compaction(CompactionTurnMetadata),
    Memory,
}

impl CodexResponsesRequestKind {
    fn metadata(self) -> (&'static str, Option<CompactionTurnMetadata>) {
        match self {
            CodexResponsesRequestKind::Turn => ("turn", None),
            CodexResponsesRequestKind::Prewarm => ("prewarm", None),
            CodexResponsesRequestKind::Compaction(metadata) => ("compaction", Some(metadata)),
            CodexResponsesRequestKind::Memory => ("memory", None),
        }
    }

    fn has_turn_identity(self) -> bool {
        !matches!(self, CodexResponsesRequestKind::Memory)
    }
}

#[derive(Clone, Debug, Serialize, Default)]
pub(crate) struct TurnMetadataWorkspace {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) associated_remote_urls: Option<BTreeMap<String, String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) latest_git_commit_hash: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) has_changes: Option<bool>,
}

/// Caller-owned snapshot of Codex metadata sent to ResponsesAPI.
///
/// The full Codex turn metadata blob is transported canonically as
/// `client_metadata["x-codex-turn-metadata"]`. Flat `client_metadata` keys and direct HTTP/ws
/// headers are generated compatibility projections of this snapshot, not separate sources of
/// truth.
#[derive(Clone, Debug)]
pub struct CodexResponsesMetadata {
    pub(crate) installation_id: String,
    pub(crate) session_id: String,
    pub(crate) thread_id: String,
    pub(crate) turn_id: Option<String>,
    pub(crate) window_id: String,
    pub(crate) request_kind: Option<CodexResponsesRequestKind>,
    pub(crate) forked_from_thread_id: Option<ThreadId>,
    pub(crate) parent_thread_id: Option<ThreadId>,
    pub(crate) subagent_header: Option<String>,
    pub(crate) subagent_kind: Option<String>,
    pub(crate) sandbox: Option<String>,
    pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
    pub(crate) turn_started_at_unix_ms: Option<i64>,
    pub(crate) extra: BTreeMap<String, String>,
}

impl CodexResponsesMetadata {
    pub(crate) fn new(
        installation_id: String,
        session_id: String,
        thread_id: String,
        window_id: String,
    ) -> Self {
        Self {
            installation_id,
            session_id,
            thread_id,
            turn_id: None,
            window_id,
            request_kind: None,
            forked_from_thread_id: None,
            parent_thread_id: None,
            subagent_header: None,
            subagent_kind: None,
            sandbox: None,
            workspaces: BTreeMap::new(),
            turn_started_at_unix_ms: None,
            extra: BTreeMap::new(),
        }
    }

    pub(crate) fn has_turn_metadata(&self) -> bool {
        self.request_kind.is_some()
    }

    pub(crate) fn turn_metadata_json(&self) -> Option<String> {
        to_ascii_json_string(&self.turn_metadata_payload()).ok()
    }

    pub(crate) fn turn_metadata_value(&self) -> Option<Value> {
        serde_json::to_value(self.turn_metadata_payload()).ok()
    }

    pub(crate) fn client_metadata(&self) -> HashMap<String, String> {
        let mut client_metadata = HashMap::from([
            (
                X_CODEX_INSTALLATION_ID_HEADER.to_string(),
                self.installation_id.clone(),
            ),
            (SESSION_ID_KEY.to_string(), self.session_id.clone()),
            (THREAD_ID_KEY.to_string(), self.thread_id.clone()),
            (X_CODEX_WINDOW_ID_HEADER.to_string(), self.window_id.clone()),
        ]);
        if let Some(turn_id) = &self.turn_id {
            client_metadata.insert(TURN_ID_KEY.to_string(), turn_id.clone());
        }
        if let Some(subagent_header) = &self.subagent_header {
            client_metadata.insert(
                X_OPENAI_SUBAGENT_HEADER.to_string(),
                subagent_header.clone(),
            );
        }
        if let Some(parent_thread_id) = self.parent_thread_id {
            client_metadata.insert(
                X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
                parent_thread_id.to_string(),
            );
        }
        if self.has_turn_metadata()
            && let Some(turn_metadata_json) = self.turn_metadata_json()
        {
            client_metadata.insert(X_CODEX_TURN_METADATA_HEADER.to_string(), turn_metadata_json);
        }
        client_metadata
    }

    pub(crate) fn compatibility_headers(&self) -> ApiHeaderMap {
        let mut headers = ApiHeaderMap::new();
        insert_header(&mut headers, X_CODEX_WINDOW_ID_HEADER, &self.window_id);
        // Direct x-codex-turn-metadata is compatibility output. New per-request consumers should
        // prefer client_metadata["x-codex-turn-metadata"], which is rendered from this same object.
        if self.has_turn_metadata()
            && let Some(turn_metadata_json) = self.turn_metadata_json()
        {
            insert_header(
                &mut headers,
                X_CODEX_TURN_METADATA_HEADER,
                &turn_metadata_json,
            );
        }
        if let Some(parent_thread_id) = self.parent_thread_id {
            insert_header(
                &mut headers,
                X_CODEX_PARENT_THREAD_ID_HEADER,
                &parent_thread_id.to_string(),
            );
        }
        if let Some(subagent_header) = &self.subagent_header {
            insert_header(&mut headers, X_OPENAI_SUBAGENT_HEADER, subagent_header);
        }
        headers
    }

    fn turn_metadata_payload(&self) -> CodexTurnMetadataPayload<'_> {
        let request_kind = self.request_kind;
        let (request_kind_value, compaction) = request_kind.map_or((None, None), |request_kind| {
            let (request_kind, compaction) = request_kind.metadata();
            (Some(request_kind), compaction)
        });
        let has_turn_identity =
            request_kind.is_none_or(CodexResponsesRequestKind::has_turn_identity);
        let has_request_identity =
            request_kind.is_some_and(CodexResponsesRequestKind::has_turn_identity);
        CodexTurnMetadataPayload {
            installation_id: has_request_identity.then_some(self.installation_id.as_str()),
            session_id: has_turn_identity.then_some(self.session_id.as_str()),
            thread_id: has_turn_identity.then_some(self.thread_id.as_str()),
            turn_id: has_turn_identity
                .then_some(self.turn_id.as_deref())
                .flatten(),
            window_id: has_request_identity.then_some(self.window_id.as_str()),
            request_kind: request_kind_value,
            forked_from_thread_id: self.forked_from_thread_id,
            parent_thread_id: self.parent_thread_id,
            subagent_kind: self.subagent_kind.as_deref(),
            sandbox: self.sandbox.as_deref(),
            workspaces: non_empty_workspaces(&self.workspaces),
            turn_started_at_unix_ms: self.turn_started_at_unix_ms,
            compaction,
            // responsesapi_client_metadata enriches the Codex turn metadata blob, not literal
            // top-level Responses client_metadata. Reserved Codex-owned keys are filtered when
            // these extras enter turn state.
            extra: &self.extra,
        }
    }
}

pub(crate) fn subagent_header_value(session_source: &SessionSource) -> Option<String> {
    match session_source {
        SessionSource::SubAgent(subagent_source) => match subagent_source {
            SubAgentSource::Review => Some("review".to_string()),
            SubAgentSource::Compact => Some("compact".to_string()),
            SubAgentSource::MemoryConsolidation => Some("memory_consolidation".to_string()),
            SubAgentSource::ThreadSpawn { .. } => Some("collab_spawn".to_string()),
            SubAgentSource::Other(label) => Some(label.clone()),
        },
        SessionSource::Internal(InternalSessionSource::MemoryConsolidation) => {
            Some("memory_consolidation".to_string())
        }
        SessionSource::Cli
        | SessionSource::VSCode
        | SessionSource::Exec
        | SessionSource::Mcp
        | SessionSource::Custom(_)
        | SessionSource::Unknown => None,
    }
}

pub(crate) fn subagent_metadata_kind(session_source: &SessionSource) -> Option<String> {
    match session_source {
        SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),
        SessionSource::Cli
        | SessionSource::VSCode
        | SessionSource::Exec
        | SessionSource::Mcp
        | SessionSource::Custom(_)
        | SessionSource::Internal(_)
        | SessionSource::Unknown => None,
    }
}

fn insert_header(headers: &mut ApiHeaderMap, name: &'static str, value: &str) {
    if let Ok(header_value) = HeaderValue::from_str(value) {
        headers.insert(name, header_value);
    }
}

pub(crate) fn filter_extra_metadata(extra: HashMap<String, String>) -> BTreeMap<String, String> {
    extra
        .into_iter()
        .filter(|(key, _)| !RESERVED_METADATA_KEYS.contains(&key.as_str()))
        .collect()
}

fn non_empty_workspaces(
    workspaces: &BTreeMap<String, TurnMetadataWorkspace>,
) -> Option<&BTreeMap<String, TurnMetadataWorkspace>> {
    (!workspaces.is_empty()).then_some(workspaces)
}

#[derive(Serialize)]
struct CodexTurnMetadataPayload<'a> {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    installation_id: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    session_id: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    thread_id: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    turn_id: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    window_id: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    request_kind: Option<&'static str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    forked_from_thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    parent_thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    subagent_kind: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    sandbox: Option<&'a str>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    workspaces: Option<&'a BTreeMap<String, TurnMetadataWorkspace>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    turn_started_at_unix_ms: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    compaction: Option<CompactionTurnMetadata>,
    #[serde(flatten)]
    extra: &'a BTreeMap<String, String>,
}
