use super::*;

pub(super) async fn not_found() -> Response {
    let problem = ProblemDetails {
        type_: ErrorType::InvalidRequest.as_urn().to_string(),
        title: "Not Found".to_string(),
        status: 404,
        detail: Some("endpoint not found".to_string()),
        instance: None,
        extensions: serde_json::Map::new(),
    };

    (
        StatusCode::NOT_FOUND,
        [(header::CONTENT_TYPE, "application/problem+json")],
        Json(problem),
    )
        .into_response()
}

pub(super) async fn require_token(
    State(state): State<Arc<AppState>>,
    request: Request<axum::body::Body>,
    next: Next,
) -> Result<Response, ApiError> {
    let Some(expected) = state.auth.token.as_ref() else {
        return Ok(next.run(request).await);
    };

    let bearer = request
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "));

    let allow_query_token = request.uri().path().ends_with("/terminal/ws");
    let query_token = if allow_query_token {
        request
            .uri()
            .query()
            .and_then(|query| query_param(query, "access_token"))
    } else {
        None
    };

    if bearer == Some(expected.as_str()) || query_token.as_deref() == Some(expected.as_str()) {
        return Ok(next.run(request).await);
    }

    Err(ApiError::Sandbox(SandboxError::TokenInvalid {
        message: Some("missing or invalid bearer token".to_string()),
    }))
}

fn query_param(query: &str, key: &str) -> Option<String> {
    query
        .split('&')
        .filter_map(|part| part.split_once('='))
        .find_map(|(k, v)| {
            if k == key {
                Some(percent_decode(v))
            } else {
                None
            }
        })
}

fn percent_decode(input: &str) -> String {
    let mut output = Vec::with_capacity(input.len());
    let bytes = input.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'%' && i + 2 < bytes.len() {
            if let (Some(hi), Some(lo)) = (
                hex_nibble(bytes[i + 1]),
                hex_nibble(bytes[i + 2]),
            ) {
                output.push((hi << 4) | lo);
                i += 3;
                continue;
            }
        }
        if bytes[i] == b'+' {
            output.push(b' ');
        } else {
            output.push(bytes[i]);
        }
        i += 1;
    }
    String::from_utf8(output).unwrap_or_else(|_| input.to_string())
}

fn hex_nibble(b: u8) -> Option<u8> {
    match b {
        b'0'..=b'9' => Some(b - b'0'),
        b'a'..=b'f' => Some(b - b'a' + 10),
        b'A'..=b'F' => Some(b - b'A' + 10),
        _ => None,
    }
}

pub(super) type PinBoxSseStream = crate::acp_proxy_runtime::PinBoxSseStream;

pub(super) fn credentials_available_for(
    agent: AgentId,
    has_anthropic: bool,
    has_openai: bool,
) -> bool {
    match agent {
        AgentId::Claude | AgentId::Amp => has_anthropic,
        AgentId::Codex => has_openai,
        AgentId::Opencode => has_anthropic || has_openai,
        AgentId::Pi | AgentId::Cursor => true,
        AgentId::Mock => true,
    }
}

/// Fallback config options for agents whose ACP adapters don't return
/// `configOptions` in `session/new`. Loaded from committed JSON resource files
/// in `scripts/agent-configs/resources/` (generated by `scripts/agent-configs/dump.ts`).
///
/// To refresh: `cd scripts/agent-configs && npx tsx dump.ts`
pub(super) fn fallback_config_options(agent: AgentId) -> Vec<Value> {
    static CLAUDE: std::sync::LazyLock<Vec<Value>> = std::sync::LazyLock::new(|| {
        parse_agent_config(include_str!(
            "../../../../../scripts/agent-configs/resources/claude.json"
        ))
    });
    static CODEX: std::sync::LazyLock<Vec<Value>> = std::sync::LazyLock::new(|| {
        parse_agent_config(include_str!(
            "../../../../../scripts/agent-configs/resources/codex.json"
        ))
    });
    static OPENCODE: std::sync::LazyLock<Vec<Value>> = std::sync::LazyLock::new(|| {
        parse_agent_config(include_str!(
            "../../../../../scripts/agent-configs/resources/opencode.json"
        ))
    });
    static CURSOR: std::sync::LazyLock<Vec<Value>> = std::sync::LazyLock::new(|| {
        parse_agent_config(include_str!(
            "../../../../../scripts/agent-configs/resources/cursor.json"
        ))
    });

    match agent {
        AgentId::Claude => CLAUDE.clone(),
        AgentId::Codex => CODEX.clone(),
        AgentId::Opencode => OPENCODE.clone(),
        AgentId::Cursor => CURSOR.clone(),
        // Amp returns empty configOptions from session/new but exposes modes via
        // the `modes` field. The model is hardcoded. Modes discovered from ACP
        // session/new response (amp-acp v0.7.0).
        AgentId::Amp => vec![
            json!({
                "id": "model",
                "name": "Model",
                "category": "model",
                "type": "select",
                "currentValue": "amp-default",
                "options": [
                    { "value": "amp-default", "name": "Amp Default" }
                ]
            }),
            json!({
                "id": "mode",
                "name": "Mode",
                "category": "mode",
                "type": "select",
                "currentValue": "default",
                "options": [
                    { "value": "default", "name": "Default" },
                    { "value": "bypass", "name": "Bypass" }
                ]
            }),
        ],
        AgentId::Pi => vec![json!({
            "id": "model",
            "name": "Model",
            "category": "model",
            "type": "select",
            "currentValue": "default",
            "options": [
                { "value": "default", "name": "Default" }
            ]
        })],
        AgentId::Mock => vec![
            json!({
                "id": "model",
                "name": "Model",
                "category": "model",
                "type": "select",
                "currentValue": "mock",
                "options": [
                    { "value": "mock", "name": "Mock" },
                    { "value": "mock-fast", "name": "Mock Fast" }
                ]
            }),
            json!({
                "id": "mode",
                "name": "Mode",
                "category": "mode",
                "type": "select",
                "currentValue": "normal",
                "options": [
                    { "value": "normal", "name": "Normal" },
                    { "value": "plan", "name": "Plan" }
                ]
            }),
            json!({
                "id": "thought_level",
                "name": "Thought Level",
                "category": "thought_level",
                "type": "select",
                "currentValue": "low",
                "options": [
                    { "value": "low", "name": "Low" },
                    { "value": "medium", "name": "Medium" },
                    { "value": "high", "name": "High" }
                ]
            }),
        ],
    }
}

/// Parse an agent config JSON file (from `scripts/agent-configs/resources/`) into
/// ACP `SessionConfigOption` values. The JSON format is:
/// ```json
/// {
///   "defaultModel": "...", "models": [{id, name}],
///   "defaultMode?": "...", "modes?": [{id, name}],
///   "defaultThoughtLevel?": "...", "thoughtLevels?": [{id, name}]
/// }
/// ```
///
/// Note: Claude and Codex don't report configOptions from `session/new`, so these
/// JSON resource files are the source of truth for the capabilities report.
/// Claude modes (plan, default) were discovered via manual ACP probing —
/// `session/set_mode` works but `session/set_config_option` is not implemented.
/// Codex modes/thought levels were discovered from its `session/new` response.
fn parse_agent_config(json_str: &str) -> Vec<Value> {
    #[derive(serde::Deserialize)]
    struct AgentConfig {
        #[serde(rename = "defaultModel")]
        default_model: String,
        models: Vec<ConfigEntry>,
        #[serde(rename = "defaultMode")]
        default_mode: Option<String>,
        modes: Option<Vec<ConfigEntry>>,
        #[serde(rename = "defaultThoughtLevel")]
        default_thought_level: Option<String>,
        #[serde(rename = "thoughtLevels")]
        thought_levels: Option<Vec<ConfigEntry>>,
    }
    #[derive(serde::Deserialize)]
    struct ConfigEntry {
        id: String,
        name: String,
    }

    let config: AgentConfig =
        serde_json::from_str(json_str).expect("invalid agent config JSON (compile-time resource)");

    let mut options = vec![json!({
        "id": "model",
        "name": "Model",
        "category": "model",
        "type": "select",
        "currentValue": config.default_model,
        "options": config.models.iter().map(|m| json!({
            "value": m.id,
            "name": m.name,
        })).collect::<Vec<_>>(),
    })];

    if let Some(modes) = config.modes {
        options.push(json!({
            "id": "mode",
            "name": "Mode",
            "category": "mode",
            "type": "select",
            "currentValue": config.default_mode.or_else(|| modes.first().map(|m| m.id.clone())).unwrap_or_default(),
            "options": modes.iter().map(|m| json!({
                "value": m.id,
                "name": m.name,
            })).collect::<Vec<_>>(),
        }));
    }

    if let Some(thought_levels) = config.thought_levels {
        options.push(json!({
            "id": "thought_level",
            "name": "Thought Level",
            "category": "thought_level",
            "type": "select",
            "currentValue": config.default_thought_level.or_else(|| thought_levels.first().map(|t| t.id.clone())).unwrap_or_default(),
            "options": thought_levels.iter().map(|t| json!({
                "value": t.id,
                "name": t.name,
            })).collect::<Vec<_>>(),
        }));
    }

    options
}

pub(super) fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
    match agent {
        AgentId::Claude => AgentCapabilities {
            plan_mode: false,
            permissions: true,
            questions: true,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: false,
            file_attachments: false,
            session_lifecycle: false,
            error_events: false,
            reasoning: false,
            status: false,
            command_execution: false,
            file_changes: false,
            mcp_tools: true,
            streaming_deltas: true,
            item_started: false,
            shared_process: false,
        },
        AgentId::Codex => AgentCapabilities {
            plan_mode: true,
            permissions: true,
            questions: false,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: true,
            file_attachments: true,
            session_lifecycle: true,
            error_events: true,
            reasoning: true,
            status: true,
            command_execution: true,
            file_changes: true,
            mcp_tools: true,
            streaming_deltas: true,
            item_started: true,
            shared_process: false,
        },
        AgentId::Opencode => AgentCapabilities {
            plan_mode: false,
            permissions: false,
            questions: false,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: true,
            file_attachments: true,
            session_lifecycle: true,
            error_events: true,
            reasoning: false,
            status: false,
            command_execution: false,
            file_changes: false,
            mcp_tools: true,
            streaming_deltas: true,
            item_started: true,
            shared_process: false,
        },
        AgentId::Amp => AgentCapabilities {
            plan_mode: false,
            permissions: false,
            questions: false,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: false,
            file_attachments: false,
            session_lifecycle: false,
            error_events: true,
            reasoning: false,
            status: false,
            command_execution: false,
            file_changes: false,
            mcp_tools: true,
            streaming_deltas: false,
            item_started: false,
            shared_process: false,
        },
        AgentId::Pi => AgentCapabilities {
            plan_mode: false,
            permissions: false,
            questions: false,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: true,
            file_attachments: false,
            session_lifecycle: true,
            error_events: true,
            reasoning: false,
            status: false,
            command_execution: false,
            file_changes: false,
            mcp_tools: false,
            streaming_deltas: true,
            item_started: true,
            shared_process: false,
        },
        AgentId::Cursor => AgentCapabilities {
            plan_mode: true,
            permissions: true,
            questions: false,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: true,
            file_attachments: false,
            session_lifecycle: true,
            error_events: true,
            reasoning: false,
            status: false,
            command_execution: false,
            file_changes: false,
            mcp_tools: false,
            streaming_deltas: true,
            item_started: true,
            shared_process: false,
        },
        AgentId::Mock => AgentCapabilities {
            plan_mode: true,
            permissions: true,
            questions: true,
            tool_calls: true,
            tool_results: true,
            text_messages: true,
            images: true,
            file_attachments: true,
            session_lifecycle: true,
            error_events: true,
            reasoning: true,
            status: true,
            command_execution: true,
            file_changes: true,
            mcp_tools: true,
            streaming_deltas: true,
            item_started: true,
            shared_process: false,
        },
    }
}

pub(super) fn map_install_result(result: InstallResult) -> AgentInstallResponse {
    AgentInstallResponse {
        already_installed: result.already_installed,
        artifacts: result
            .artifacts
            .into_iter()
            .map(|artifact| AgentInstallArtifact {
                kind: map_artifact_kind(artifact.kind),
                path: artifact.path.to_string_lossy().to_string(),
                source: map_install_source(artifact.source),
                version: artifact.version,
            })
            .collect(),
    }
}

pub(super) fn map_install_source(source: InstallSource) -> String {
    match source {
        InstallSource::Registry => "registry",
        InstallSource::Fallback => "fallback",
        InstallSource::LocalPath => "local_path",
        InstallSource::Builtin => "builtin",
    }
    .to_string()
}

pub(super) fn map_artifact_kind(kind: InstalledArtifactKind) -> String {
    match kind {
        InstalledArtifactKind::NativeAgent => "native_agent",
        InstalledArtifactKind::AgentProcess => "agent_process",
    }
    .to_string()
}

pub(super) fn resolve_fs_path(raw_path: &str) -> Result<PathBuf, SandboxError> {
    let path = PathBuf::from(raw_path);
    if path.is_absolute() {
        return Ok(path);
    }

    let home = std::env::var_os("HOME")
        .or_else(|| std::env::var_os("USERPROFILE"))
        .map(PathBuf::from)
        .ok_or_else(|| SandboxError::InvalidRequest {
            message: "home directory unavailable".to_string(),
        })?;

    let relative = sanitize_relative_path(&path)?;
    Ok(home.join(relative))
}

pub(super) fn sanitize_relative_path(path: &StdPath) -> Result<PathBuf, SandboxError> {
    use std::path::Component;
    let mut sanitized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::Normal(value) => sanitized.push(value),
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(SandboxError::InvalidRequest {
                    message: format!("invalid relative path: {}", path.display()),
                });
            }
        }
    }
    Ok(sanitized)
}

pub(super) fn map_fs_error(path: &StdPath, err: std::io::Error) -> SandboxError {
    if err.kind() == std::io::ErrorKind::NotFound {
        SandboxError::InvalidRequest {
            message: format!("path not found: {}", path.display()),
        }
    } else {
        SandboxError::StreamError {
            message: err.to_string(),
        }
    }
}

pub(super) fn content_type_is(headers: &HeaderMap, expected: &str) -> bool {
    let Some(value) = headers
        .get(header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
    else {
        return false;
    };
    media_type_eq(value, expected)
}

pub(super) fn accept_allows(headers: &HeaderMap, expected: &str) -> bool {
    let values = headers.get_all(header::ACCEPT);
    if values.iter().next().is_none() {
        return true;
    }

    values
        .iter()
        .filter_map(|value| value.to_str().ok())
        .flat_map(|value| value.split(','))
        .any(|value| media_type_matches(value, expected))
}

fn media_type_eq(raw: &str, expected: &str) -> bool {
    normalize_media_type(raw).as_deref() == Some(expected)
}

fn media_type_matches(raw: &str, expected: &str) -> bool {
    let Some(media) = normalize_media_type(raw) else {
        return false;
    };
    if media == expected || media == "*/*" {
        return true;
    }

    let Some((media_type, media_subtype)) = media.split_once('/') else {
        return false;
    };
    let Some((expected_type, _expected_subtype)) = expected.split_once('/') else {
        return false;
    };

    media_subtype == "*" && media_type == expected_type
}

fn normalize_media_type(raw: &str) -> Option<String> {
    let media = raw.split(';').next().unwrap_or_default().trim();
    if media.is_empty() {
        return None;
    }
    Some(media.to_ascii_lowercase())
}

pub(super) fn parse_last_event_id(headers: &HeaderMap) -> Result<Option<u64>, SandboxError> {
    let value = headers
        .get("last-event-id")
        .and_then(|value| value.to_str().ok());

    match value {
        Some(value) if !value.trim().is_empty() => {
            value
                .trim()
                .parse::<u64>()
                .map(Some)
                .map_err(|_| SandboxError::InvalidRequest {
                    message: "Last-Event-ID must be a positive integer".to_string(),
                })
        }
        _ => Ok(None),
    }
}

pub(super) fn problem_from_sandbox_error(error: &SandboxError) -> ProblemDetails {
    let mut problem = error.to_problem_details();

    match error {
        SandboxError::InvalidRequest { message } => {
            if message.starts_with("input payload exceeds maxInputBytesPerRequest") {
                problem.status = 413;
                problem.title = "Payload Too Large".to_string();
            } else {
                problem.status = 400;
            }
        }
        SandboxError::NotFound { .. } => {
            problem.status = 404;
            problem.title = "Not Found".to_string();
        }
        SandboxError::Timeout { .. } => {
            problem.status = 504;
        }
        _ => {}
    }

    problem
}

/// Build the OpenCode-compatible provider payload from installed agent config
/// options. This replaces the hardcoded mock/amp/claude/codex list in the
/// opencode-adapter with real model information derived from
/// `fallback_config_options()`.
pub(super) fn build_provider_payload_for_opencode(_state: &Arc<AppState>) -> Value {
    let agents: &[AgentId] = &[
        AgentId::Mock,
        AgentId::Claude,
        AgentId::Codex,
        AgentId::Amp,
        AgentId::Opencode,
        AgentId::Pi,
        AgentId::Cursor,
    ];

    let has_anthropic = std::env::var("ANTHROPIC_API_KEY").is_ok();
    let has_openai = std::env::var("OPENAI_API_KEY").is_ok();

    let mut all_providers = Vec::new();
    let mut defaults = serde_json::Map::new();
    let mut connected = Vec::new();

    for &agent in agents {
        let agent_str = agent.as_str();

        let options = fallback_config_options(agent);
        let model_option = options
            .iter()
            .find(|opt| opt.get("category").and_then(Value::as_str) == Some("model"));

        let Some(model_option) = model_option else {
            continue;
        };

        let current_value = model_option
            .get("currentValue")
            .and_then(Value::as_str)
            .unwrap_or("default");
        let option_list = model_option
            .get("options")
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default();

        let mut models = serde_json::Map::new();
        for opt in &option_list {
            let id = opt
                .get("value")
                .and_then(Value::as_str)
                .unwrap_or("default");
            let name = opt.get("name").and_then(Value::as_str).unwrap_or(id);
            models.insert(
                id.to_string(),
                json!({
                    "id": id,
                    "name": name,
                    "family": capitalize_first(agent_str),
                    "release_date": "1970-01-01",
                    "attachment": false,
                    "reasoning": false,
                    "temperature": true,
                    "tool_call": true,
                    "limit": { "context": 200_000, "output": 8_192 },
                    "options": {},
                }),
            );
        }

        defaults.insert(agent_str.to_string(), json!(current_value));

        if agent == AgentId::Mock || credentials_available_for(agent, has_anthropic, has_openai) {
            connected.push(json!(agent_str));
        }

        all_providers.push(json!({
            "id": agent_str,
            "name": agent_display_name(agent),
            "env": [],
            "models": Value::Object(models),
        }));
    }

    json!({
        "all": all_providers,
        "default": Value::Object(defaults),
        "connected": connected,
    })
}

fn agent_display_name(agent: AgentId) -> &'static str {
    match agent {
        AgentId::Mock => "Mock",
        AgentId::Claude => "Claude Code",
        AgentId::Codex => "Codex CLI",
        AgentId::Amp => "Amp",
        AgentId::Opencode => "OpenCode",
        AgentId::Pi => "Pi",
        AgentId::Cursor => "Cursor Agent",
    }
}

fn capitalize_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
    }
}
