use super::*;

// ---------------------------------------------------------------------------
// Permission request handling
// ---------------------------------------------------------------------------

/// Route a permission request to the agent that owns its `session_id`, queue
/// it on that agent's view, and return whether the active view needs a redraw.
///
/// Permissions are routed by `session_id` so that requests for an inactive
/// agent still queue on the owning agent's view. When the user switches back
/// to that agent, the queued permission is visible and can be answered.
///
/// If no agent owns the `session_id` (e.g. session was just cleaned up), the
/// request is cancelled rather than left dangling.
///
/// YOLO mode is honored on the owning agent regardless of which agent is
/// currently active, so background turns aren't blocked waiting for an
/// always-yes answer the user has already given.
pub(super) fn handle_permission_request(
    perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>,
    app: &mut AppView,
) -> bool {
    // 1. Look up the owning agent by session_id (root or subagent view).
    let matched = match find_session_match(app, &perm.request.session_id) {
        Some(m) => m,
        None => {
            tracing::warn!(
                session_id = %perm.request.session_id.0,
                "Permission request for unknown session_id; cancelling"
            );
            cancel_permission(perm);
            return false;
        }
    };
    let owning_agent_id = matched.agent_id();
    let is_active = is_matched_agent_active(app, owning_agent_id);
    let Some(agent) = app.agents.get_mut(&owning_agent_id) else {
        cancel_permission(perm);
        return false;
    };

    // 2. YOLO mode: auto-approve immediately on the owning agent so background
    //    turns aren't blocked waiting for the user to switch back.
    //
    //    If no `AllowOnce` option exists, falls through to
    //    `enqueue_permission` even in YOLO mode (won't pick
    //    `AllowAlways` by default).
    if agent.session.is_yolo()
        && let Some(allow) = perm
            .request
            .options
            .iter()
            .find(|o| o.kind == acp::PermissionOptionKind::AllowOnce)
    {
        let option_id = allow.option_id.clone();
        perm.response_tx
            .send(Ok(acp::RequestPermissionResponse::new(
                acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
                    option_id,
                )),
            )))
            .ok();
        return false; // no redraw needed
    }

    // 3. Fire notification so the user notices the pending approval.
    //    Rate-limit: only fire the bell/popup on the empty→non-empty
    //    transition to avoid stacking notifications during concurrent
    //    permission requests.
    if !app
        .notification_service
        .should_suppress_permission_notification()
    {
        app.notification_service.notify(NotificationEvent {
            kind: NotificationEventKind::ApprovalRequired,
            title: "Kigi".into(),
            body: NotificationEventKind::ApprovalRequired.as_str().into(),
            session_id: Some(perm.request.session_id.0.to_string()),
        });
        app.notification_service.mark_permission_notified();
    }

    // 4. Queue on the owning agent's view. Subagent provenance for display
    //    is still resolved via subagent_sessions in enqueue_permission().
    //    Redraw is only needed when the owning agent is currently visible.
    let needs_redraw = enqueue_permission(perm, agent);
    needs_redraw && is_active
}

/// Enqueue a permission request on an agent view.
///
/// Parses bash highlights, builds display content, stashes the prompt on
/// queue transition, and pushes the request onto the FIFO queue.
fn enqueue_permission(
    perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>,
    agent: &mut AgentView,
) -> bool {
    // 1. Parse bash highlights from request meta (imported from kigi-shell).
    let bash_highlights: Option<BashCommandHighlights> = perm
        .request
        .meta
        .as_ref()
        .and_then(|meta| serde_json::from_value(serde_json::Value::Object(meta.clone())).ok());
    let bash_selection_count = bash_highlights
        .as_ref()
        .map(|h| kigi_workspace::permission::default_always_allow_scope(&h.highlighted_words))
        .unwrap_or(0);

    // 1b. Parse MCP scope state from the `allow-always-mcp` option's meta.
    //     Mutually exclusive with the bash flow at the per-request level —
    //     the same prompt cannot carry both.
    let mcp_scope = perm
        .request
        .options
        .iter()
        .find(|o| o.option_id.0.as_ref() == "allow-always-mcp")
        .and_then(|opt| opt.meta.as_ref())
        .and_then(|m| {
            serde_json::from_value::<kigi_workspace::permission::McpToolPermission>(
                serde_json::Value::Object(m.clone()),
            )
            .ok()
        })
        .map(|perm| McpScopeState {
            tool_name: perm.tool_name,
            server_prefix: perm.server_prefix,
            selected: McpScope::Tool,
        });

    // 2. Build subagent provenance label.
    //    If session_id differs from the root session, look up subagent info.
    let subagent_label = resolve_subagent_label(agent, &perm.request.session_id);

    // 3. Build title and description from the tool call.
    let (title, description, bash_command_raw) =
        build_permission_display(&perm.request, bash_highlights.as_ref());

    // 4. Assign a monotonic ID.
    let perm_id = agent.next_perm_req_id;
    agent.next_perm_req_id += 1;

    // 5. Stash prompt on queue transition: empty -> non-empty.
    //    Do NOT stash again if the queue is already non-empty (that would
    //    capture followup text from the current permission as the "original"
    //    prompt, losing the user's real input).
    if agent.permission_queue.is_empty() && agent.permission_stashed_prompt.is_none() {
        agent.permission_stashed_prompt = Some(agent.prompt.stash());
        agent.prompt.set_text("");
    }

    // 6. Clone options before moving perm into the struct.
    let options = perm.request.options.clone();

    // 7. Cursor preselection (sticky last-used → configured default → the
    //    enable-always-approve row → index 0). See `permission_cursor`.
    let active_idx = crate::appearance::permission_cursor::resolve_initial_cursor(&options);

    // 8. Queue the request FIFO (do NOT replace/cancel existing requests).
    agent.permission_queue.push_back(PermissionViewState {
        request: perm,
        id: perm_id,
        focus: PermissionFocus::Options,
        options,
        active_idx,
        bash_highlights,
        bash_selection_count,
        bash_command_raw,
        mcp_scope,
        title,
        description,
        args_expanded: false,
        desc_scroll: 0,
        subagent_label,
        options_area_height: 0,
        options_scroll_offset: 0,
    });

    // Stamp the agent's "last activity" anchor so
    // the dashboard's age column for `NeedsInput` rows reflects
    // "time since permission arrived" rather than "time since last
    // turn ended". The same field powers the dashboard relative-time label.
    agent.last_active_at = Some(std::time::Instant::now());

    true // needs redraw
}

/// Build a subagent provenance label for display.
///
/// Two tiers of provenance quality:
///
/// 1. **Tracked provenance** (`SubagentSpawned` was received): renders as
///    `Subagent "Find endpoints" (explore):` with description and type
///    from the tracked `SubagentInfo`. This is the trusted path.
///
/// 2. **Opaque non-root session**: the session_id does not match root and
///    is not in the tracked subagent map. Renders as
///    `Child session (untracked):` to signal reduced confidence.
///
/// Returns `None` for root session (no provenance needed).
fn resolve_subagent_label(agent: &AgentView, session_id: &acp::SessionId) -> Option<String> {
    let sid = session_id.0.as_ref();
    // Check if this is the root session (no provenance needed).
    if let Some(ref root_sid) = agent.session.session_id
        && root_sid.0.as_ref() == sid
    {
        return None;
    }
    // Tier 1: tracked subagent with full metadata.
    if let Some(info) = agent.subagent_sessions.get(sid) {
        return Some(format!(
            "Subagent \"{}\" ({}):",
            info.description, info.subagent_type
        ));
    }
    // Tier 2: non-root session with no tracked info.
    Some("Child session (untracked):".to_string())
}

/// Build title, description lines, and optional raw command for a permission request.
///
/// Deserializes `raw_input` into the shared [`BashToolInput`] from
/// `kigi-tools` for typed access to `command` and `description`.
/// Falls back to ACP-level `title`/`kind` fields when deserialization fails.
///
/// Returns `(title, description, bash_command_raw)`.
fn build_permission_display(
    req: &acp::RequestPermissionRequest,
    bash_highlights: Option<&BashCommandHighlights>,
) -> (String, Vec<String>, Option<String>) {
    let is_bash = bash_highlights.is_some();

    let bash_input = req.tool_call.fields.raw_input.as_ref().and_then(|v| {
        serde_json::from_value::<kigi_tools::implementations::BashToolInput>(v.clone()).ok()
    });

    let raw_command = bash_input.as_ref().map(|b| b.command.clone()).or_else(|| {
        req.tool_call
            .fields
            .title
            .as_deref()
            .and_then(|t| t.strip_prefix("Execute `"))
            .and_then(|t| t.strip_suffix('`'))
            .map(|s| s.to_string())
    });

    let bash_description = bash_input.map(|b| b.description);

    let is_execute = is_bash
        || req.tool_call.fields.kind == Some(acp::ToolKind::Execute)
        || raw_command.is_some();

    let title = if is_execute {
        bash_description
            .as_deref()
            .map(str::trim)
            .filter(|t| !t.is_empty())
            .map(|t| t.to_string())
            .unwrap_or_else(
                || match bash_highlights.and_then(|h| h.highlighted_words.first()) {
                    Some(bin) => format!("Allow `{bin}`?"),
                    None => "Allow Execute?".to_string(),
                },
            )
    } else if is_edit_permission(req) {
        let file_path = req
            .tool_call
            .fields
            .raw_input
            .as_ref()
            .and_then(|v| v.get("file_path"))
            .and_then(|v| v.as_str());
        if let Some(path) = file_path {
            format!("Allow Edit to {}?", path)
        } else if let Some(ref t) = req.tool_call.fields.title {
            format!(
                "Allow {}?",
                kigi_workspace::permission::mcp_pretty_name_if_qualified(t)
            )
        } else {
            "Allow Edit?".to_string()
        }
    } else if let Some(ref t) = req.tool_call.fields.title {
        format!(
            "Allow {}?",
            kigi_workspace::permission::mcp_pretty_name_if_qualified(t)
        )
    } else {
        match req.tool_call.fields.kind {
            Some(acp::ToolKind::Edit) => "Allow Edit?".to_string(),
            Some(acp::ToolKind::Execute) => "Allow Execute?".to_string(),
            Some(acp::ToolKind::Delete) => "Allow Delete?".to_string(),
            _ => "Allow?".to_string(),
        }
    };

    let description = mcp_args_lines(req);
    let bash_cmd = if is_execute { raw_command } else { None };
    (title, description, bash_cmd)
}

/// Maximum stored lines for the MCP planned-arguments display. The overlay
/// clips further (options always stay visible); this only bounds memory for
/// pathologically large inputs.
pub(super) const MCP_ARGS_MAX_LINES: usize = 200;

/// Maximum stored characters per argument line. Bounds the per-frame
/// wrap/render cost for pathological single-line values (e.g. an embedded
/// base64 blob); anything longer is elided with a marker.
pub(super) const MCP_ARGS_MAX_LINE_CHARS: usize = 2000;

/// Pretty-printed JSON lines of the arguments an MCP tool call plans to
/// send, taken from the serialized `ToolInput` the shell puts in
/// `tool_call.fields.raw_input` (`{"variant": "UseTool"|"MCPTool",
/// "tool_name": …, "tool_input": …}`). The prompt otherwise names the tool
/// without what it would do — the payload is what an approval (especially
/// an always-approve) is judged on.
///
/// Empty for non-MCP requests (bash/edit have dedicated displays) and for
/// MCP requests without a JSON `tool_input`.
pub(super) fn mcp_args_lines(req: &acp::RequestPermissionRequest) -> Vec<String> {
    let Some(raw) = req.tool_call.fields.raw_input.as_ref() else {
        return Vec::new();
    };
    // Match by serde tag rather than deserializing the whole enum, so args
    // still display when the shell adds variants this build doesn't know.
    let is_mcp = matches!(
        raw.get("variant").and_then(|v| v.as_str()),
        Some("UseTool") | Some("MCPTool")
    );
    if !is_mcp {
        return Vec::new();
    }
    let args = match raw.get("tool_input") {
        Some(serde_json::Value::Null) | None => return Vec::new(),
        Some(args) => args,
    };
    let pretty = match serde_json::to_string_pretty(args) {
        Ok(p) => p,
        Err(_) => return Vec::new(),
    };
    let mut lines: Vec<String> = pretty
        .lines()
        .map(|l| match l.char_indices().nth(MCP_ARGS_MAX_LINE_CHARS) {
            Some((byte_idx, _)) => format!("{}…", &l[..byte_idx]),
            None => l.to_owned(),
        })
        .collect();
    if lines.len() > MCP_ARGS_MAX_LINES {
        let hidden = lines.len() - MCP_ARGS_MAX_LINES;
        lines.truncate(MCP_ARGS_MAX_LINES);
        lines.push(format!("… (+{hidden} more lines)"));
    }
    lines
}

/// Check if this is an edit permission by looking at option names.
///
/// The shell's edit options include "allow all edits" in the AllowAlways
/// option name. This is reliable even when tool_call.fields.kind is None.
fn is_edit_permission(req: &acp::RequestPermissionRequest) -> bool {
    req.options.iter().any(|o| {
        o.kind == acp::PermissionOptionKind::AllowAlways && o.name.to_lowercase().contains("edit")
    })
}

/// Cancel a permission request by sending `Cancelled` on the response channel.
fn cancel_permission(perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>) {
    perm.response_tx
        .send(Ok(acp::RequestPermissionResponse::new(
            acp::RequestPermissionOutcome::Cancelled,
        )))
        .ok();
}

/// Live auto recap arrived while the agent is busy (turn or command in
/// flight) — drop so it cannot land under newer output. Manual `/recap` and
/// history replay always apply.
pub(super) fn should_drop_late_auto_recap(auto: bool, is_replay: bool, agent_idle: bool) -> bool {
    auto && !is_replay && !agent_idle
}

/// Land a `SessionRecap` block: fill a manual `/recap`'s in-flight loading
/// spinner in place (and stop its animation) when one is showing, otherwise
/// append a fresh block. An automatic recap never consumes the manual loading
/// slot (`auto`) — it would orphan the in-flight manual response into a second
/// block — so it always appends.
///
/// Minimal (scrollback-native) mode may have already printed the loading
/// spinner into the terminal's native scrollback (the idle commit pass consumes
/// it print-once). Filling that entry in place would mutate state the terminal
/// never re-reads — the recap text would exist only in `/transcript`, never on
/// screen. Re-print instead: drop the stale committed entry from state (its
/// printed copy can't be un-printed, matching the K10 re-print semantics) and
/// append the real recap as a fresh block so the commit pass emits it.
/// `is_committed` is always false outside minimal, so the fill-in-place path is
/// unchanged for the alt-screen / inline modes.
pub(super) fn apply_recap_block(agent: &mut AgentView, auto: bool, recap_block: RenderBlock) {
    let fill_id = if auto {
        None
    } else {
        agent
            .pending_recap_entry
            .take()
            .filter(|&id| agent.scrollback.get_by_id(id).is_some())
    };
    match fill_id {
        Some(id) if agent.scrollback.is_committed(id) => {
            agent.scrollback.remove_entry(id);
            agent.scrollback.push_block(recap_block);
        }
        Some(id) => {
            // Existence just confirmed; scope the `&mut` borrow so it
            // ends before `finish_running` re-borrows the scrollback.
            if let Some(entry) = agent.scrollback.get_by_id_mut(id) {
                entry.block = recap_block;
            }
            agent.scrollback.finish_running(id);
        }
        None => {
            agent.scrollback.push_block(recap_block);
        }
    }
}
