use crate::{AgentTool, ToolCallEventStream, ToolInput};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use dap::SteppingGranularity;
use dap::client::SessionId;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use project::Project;
use project::debugger::breakpoint_store::{
    Breakpoint, BreakpointEditAction, BreakpointState, BreakpointWithPosition,
};
use project::debugger::session::{Session, ThreadId, ThreadStatus};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use text::Point;
use util::markdown::MarkdownInlineCode;

/// Interact with the debugger to control debug sessions, set breakpoints, and inspect program state.
///
/// This tool allows you to:
/// - Set and remove breakpoints at specific file locations
/// - List all breakpoints in the project
/// - List active debug sessions
/// - Control execution (continue, pause, step over, step in, step out)
/// - Inspect stack traces and variables when stopped at a breakpoint
///
/// <guidelines>
/// - Before using debugger controls (continue, pause, step), ensure there is an active debug session
/// - When setting breakpoints, use the exact file path as it appears in the project
/// - Stack traces and variables are only available when the debugger is stopped at a breakpoint
/// - Use `list_sessions` to see available debug sessions before trying to control them
/// </guidelines>
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DebuggerToolInput {
    /// The debugger operation to perform
    pub operation: DebuggerOperation,
    /// The path to the file (required for set_breakpoint and remove_breakpoint operations)
    #[serde(default)]
    pub path: Option<String>,
    /// The 1-based line number (required for set_breakpoint and remove_breakpoint operations)
    #[serde(default)]
    pub line: Option<u32>,
    /// Whether to enable or disable the breakpoint (for set_breakpoint only)
    #[serde(default)]
    pub enabled: Option<bool>,
    /// Optional condition expression that must evaluate to true for the breakpoint to trigger (for set_breakpoint only)
    #[serde(default)]
    pub condition: Option<String>,
    /// Optional log message to output when the breakpoint is hit (for set_breakpoint only)
    #[serde(default)]
    pub log_message: Option<String>,
    /// Optional hit count condition (for set_breakpoint only)
    #[serde(default)]
    pub hit_condition: Option<String>,
    /// Optional session ID. If not provided, uses the active session.
    #[serde(default)]
    pub session_id: Option<u32>,
    /// Optional thread ID. If not provided, uses an appropriate thread based on the operation.
    #[serde(default)]
    pub thread_id: Option<i64>,
    /// Optional stack frame index (0 = top of stack). Used for get_variables operation.
    #[serde(default)]
    pub frame_index: Option<usize>,
}

/// The debugger operation to perform
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DebuggerOperation {
    /// Set or update a breakpoint at a specific file and line
    SetBreakpoint,
    /// Remove a breakpoint at a specific file and line
    RemoveBreakpoint,
    /// List all breakpoints in the project
    ListBreakpoints,
    /// List all active debug sessions
    ListSessions,
    /// Continue execution of a paused thread
    Continue,
    /// Pause execution of a running thread
    Pause,
    /// Step over the current line (execute without entering functions)
    StepOver,
    /// Step into the current line (enter function calls)
    StepIn,
    /// Step out of the current function
    StepOut,
    /// Get the stack trace for a stopped thread
    GetStackTrace,
    /// Get variables in the current scope
    GetVariables,
}

pub struct DebuggerTool {
    project: Entity<Project>,
}

impl DebuggerTool {
    pub fn new(project: Entity<Project>) -> Self {
        Self { project }
    }

    fn find_session(
        project: &Entity<Project>,
        session_id: Option<u32>,
        cx: &App,
    ) -> Result<Entity<Session>> {
        let dap_store = project.read(cx).dap_store();
        let dap_store = dap_store.read(cx);

        if let Some(id) = session_id {
            dap_store
                .session_by_id(SessionId(id))
                .ok_or_else(|| anyhow!("No debug session found with ID {}", id))
        } else {
            if let Some((session, _)) = project.read(cx).active_debug_session(cx) {
                return Ok(session);
            }
            dap_store
                .sessions()
                .next()
                .cloned()
                .ok_or_else(|| anyhow!("No active debug session. Start a debug session first."))
        }
    }

    fn find_stopped_thread(
        session: &Entity<Session>,
        thread_id: Option<i64>,
        cx: &mut App,
    ) -> Result<ThreadId> {
        session.update(cx, |session, cx| {
            let threads = session.threads(cx);

            if let Some(tid) = thread_id {
                let thread_id = ThreadId::from(tid);
                if threads
                    .iter()
                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)
                {
                    return Ok(thread_id);
                }
                return Err(anyhow!("Thread {} not found", tid));
            }

            threads
                .iter()
                .find(|(_, status)| *status == ThreadStatus::Stopped)
                .map(|(t, _)| ThreadId::from(t.id))
                .ok_or_else(|| {
                    anyhow!("No stopped thread found. The debugger must be paused at a breakpoint.")
                })
        })
    }

    fn find_running_thread(
        session: &Entity<Session>,
        thread_id: Option<i64>,
        cx: &mut App,
    ) -> Result<ThreadId> {
        session.update(cx, |session, cx| {
            let threads = session.threads(cx);

            if let Some(tid) = thread_id {
                let thread_id = ThreadId::from(tid);
                if threads
                    .iter()
                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)
                {
                    return Ok(thread_id);
                }
                return Err(anyhow!("Thread {} not found", tid));
            }

            threads
                .iter()
                .find(|(_, status)| *status == ThreadStatus::Running)
                .map(|(t, _)| ThreadId::from(t.id))
                .ok_or_else(|| anyhow!("No running thread found."))
        })
    }

    fn find_any_thread(
        session: &Entity<Session>,
        thread_id: Option<i64>,
        cx: &mut App,
    ) -> Result<ThreadId> {
        session.update(cx, |session, cx| {
            let threads = session.threads(cx);

            if let Some(tid) = thread_id {
                let thread_id = ThreadId::from(tid);
                if threads
                    .iter()
                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)
                {
                    return Ok(thread_id);
                }
                return Err(anyhow!("Thread {} not found", tid));
            }

            threads
                .iter()
                .find(|(_, status)| *status == ThreadStatus::Stopped)
                .or_else(|| threads.first())
                .map(|(t, _)| ThreadId::from(t.id))
                .ok_or_else(|| anyhow!("No threads found in the debug session."))
        })
    }

    async fn run_operation(
        project: Entity<Project>,
        input: DebuggerToolInput,
        cx: &mut AsyncApp,
    ) -> Result<String> {
        match input.operation {
            DebuggerOperation::SetBreakpoint => {
                let path = input
                    .path
                    .ok_or_else(|| anyhow!("path is required for set_breakpoint operation"))?;
                let line = input
                    .line
                    .ok_or_else(|| anyhow!("line is required for set_breakpoint operation"))?;
                let enabled = input.enabled;
                let condition = input.condition;
                let log_message = input.log_message;
                let hit_condition = input.hit_condition;

                let (buffer_task, breakpoint_store, abs_path): (_, _, _) = cx.update(|cx| {
                    let project_path = project.read(cx).find_project_path(&path, cx);
                    let Some(project_path) = project_path else {
                        return Err(anyhow!("Could not find path {} in project", path));
                    };

                    let breakpoint_store = project.read(cx).breakpoint_store();
                    let buffer_task = project.update(cx, |project, cx| {
                        project.open_buffer(project_path.clone(), cx)
                    });

                    let worktree = project
                        .read(cx)
                        .worktree_for_id(project_path.worktree_id, cx);
                    let abs_path = worktree.map(|wt| wt.read(cx).absolutize(&project_path.path));

                    Ok((buffer_task, breakpoint_store, abs_path))
                })?;

                let buffer = buffer_task.await?;
                let abs_path =
                    abs_path.ok_or_else(|| anyhow!("Could not determine absolute path"))?;

                Ok(cx.update(|cx| {
                    let snapshot = buffer.read(cx).snapshot();
                    let row = line.saturating_sub(1);
                    let point = Point::new(row, 0);
                    let position = snapshot.anchor_before(point);

                    let state = match enabled {
                        Some(true) | None => BreakpointState::Enabled,
                        Some(false) => BreakpointState::Disabled,
                    };

                    let breakpoint = Breakpoint {
                        message: log_message.map(|s| s.into()),
                        hit_condition: hit_condition.map(|s| s.into()),
                        condition: condition.map(|s| s.into()),
                        state,
                    };

                    let breakpoint_with_position = BreakpointWithPosition {
                        position,
                        bp: breakpoint,
                    };

                    breakpoint_store.update(cx, |store, cx| {
                        store.toggle_breakpoint(
                            buffer,
                            breakpoint_with_position,
                            BreakpointEditAction::Toggle,
                            cx,
                        );
                    });

                    format!("Breakpoint set at {}:{}", abs_path.display(), line)
                }))
            }

            DebuggerOperation::RemoveBreakpoint => {
                let path = input
                    .path
                    .ok_or_else(|| anyhow!("path is required for remove_breakpoint operation"))?;
                let line = input
                    .line
                    .ok_or_else(|| anyhow!("line is required for remove_breakpoint operation"))?;

                cx.update(|cx| {
                    let project = project.read(cx);
                    let Some(project_path) = project.find_project_path(&path, cx) else {
                        return Err(anyhow!("Could not find path {} in project", path));
                    };

                    let worktree = project
                        .worktree_for_id(project_path.worktree_id, cx)
                        .ok_or_else(|| anyhow!("Worktree not found"))?;
                    let abs_path = worktree.read(cx).absolutize(&project_path.path);

                    let breakpoint_store = project.breakpoint_store();
                    let row = line.saturating_sub(1);

                    let result = breakpoint_store
                        .read(cx)
                        .breakpoint_at_row(&abs_path, row, cx);

                    if let Some((buffer, breakpoint)) = result {
                        breakpoint_store.update(cx, |store, cx| {
                            store.toggle_breakpoint(
                                buffer,
                                breakpoint,
                                BreakpointEditAction::Toggle,
                                cx,
                            );
                        });
                        Ok(format!("Breakpoint removed at {}:{}", path, line))
                    } else {
                        Ok(format!("No breakpoint found at {}:{}", path, line))
                    }
                })
            }
            DebuggerOperation::ListBreakpoints => Ok(cx.update(|cx| {
                let breakpoint_store = project.read(cx).breakpoint_store();
                let breakpoints = breakpoint_store.read(cx).all_source_breakpoints(cx);

                let mut output = String::new();
                if breakpoints.is_empty() {
                    output.push_str("No breakpoints set.");
                } else {
                    writeln!(output, "Breakpoints:").ok();
                    for (path, bps) in &breakpoints {
                        for bp in bps {
                            let state = if bp.state.is_enabled() {
                                "enabled"
                            } else {
                                "disabled"
                            };
                            let mut details = vec![state.to_string()];

                            if let Some(ref cond) = bp.condition {
                                details.push(format!("condition: {}", cond));
                            }
                            if let Some(ref msg) = bp.message {
                                details.push(format!("log: {}", msg));
                            }
                            if let Some(ref hit) = bp.hit_condition {
                                details.push(format!("hit: {}", hit));
                            }

                            writeln!(
                                output,
                                "  - {}:{} [{}]",
                                path.display(),
                                bp.row + 1,
                                details.join(", ")
                            )
                            .ok();
                        }
                    }
                }
                output
            })),

            DebuggerOperation::ListSessions => Ok(cx.update(|cx| {
                let dap_store = project.read(cx).dap_store();
                let sessions: Vec<_> = dap_store.read(cx).sessions().cloned().collect();

                let mut output = String::new();
                if sessions.is_empty() {
                    output.push_str("No active debug sessions.");
                } else {
                    writeln!(output, "Debug sessions:").ok();
                    for session in sessions {
                        let session_ref = session.read(cx);
                        let session_id = session_ref.session_id();
                        let adapter = session_ref.adapter();
                        let label = session_ref.label();
                        let is_terminated = session_ref.is_terminated();

                        let status = if is_terminated {
                            "terminated"
                        } else if session_ref.is_building() {
                            "building"
                        } else {
                            "running"
                        };

                        let label_str = label.as_ref().map(|l| l.as_ref()).unwrap_or("unnamed");
                        writeln!(
                            output,
                            "  - Session {} ({}): {} [{}]",
                            session_id.0, adapter, label_str, status
                        )
                        .ok();
                    }
                }
                output
            })),

            DebuggerOperation::Continue => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;

                session.update(cx, |session, cx| {
                    session.continue_thread(tid, cx);
                });

                Ok(format!("Continued execution of thread {}", tid.0))
            }),

            DebuggerOperation::Pause => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_running_thread(&session, input.thread_id, cx)?;

                session.update(cx, |session, cx| {
                    session.pause_thread(tid, cx);
                });

                Ok(format!("Paused thread {}", tid.0))
            }),

            DebuggerOperation::StepOver => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;

                session.update(cx, |session, cx| {
                    session.step_over(tid, SteppingGranularity::Line, cx);
                });

                Ok(format!("Stepped over on thread {}", tid.0))
            }),

            DebuggerOperation::StepIn => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;

                session.update(cx, |session, cx| {
                    session.step_in(tid, SteppingGranularity::Line, cx);
                });

                Ok(format!("Stepped into on thread {}", tid.0))
            }),

            DebuggerOperation::StepOut => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;

                session.update(cx, |session, cx| {
                    session.step_out(tid, SteppingGranularity::Line, cx);
                });

                Ok(format!("Stepped out on thread {}", tid.0))
            }),

            DebuggerOperation::GetStackTrace => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_any_thread(&session, input.thread_id, cx)?;

                let frames = session.update(cx, |session, cx| session.stack_frames(tid, cx))?;

                let mut output = String::new();
                if frames.is_empty() {
                    output.push_str("No stack frames available. The thread may be running.");
                } else {
                    writeln!(output, "Stack trace for thread {}:", tid.0).ok();
                    for (i, frame) in frames.iter().enumerate() {
                        let location = frame
                            .dap
                            .source
                            .as_ref()
                            .and_then(|s| s.path.as_ref())
                            .map(|p| format!("{}:{}", p, frame.dap.line))
                            .unwrap_or_else(|| "unknown".to_string());

                        writeln!(output, "  #{} {} at {}", i, frame.dap.name, location).ok();
                    }
                }
                Ok(output)
            }),

            DebuggerOperation::GetVariables => cx.update(|cx| {
                let session = Self::find_session(&project, input.session_id, cx)?;
                let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
                let frame_idx = input.frame_index.unwrap_or(0);

                session.update(cx, |session, cx| {
                    let frames = session.stack_frames(tid, cx)?;

                    let frame = frames.get(frame_idx).ok_or_else(|| {
                        anyhow!(
                            "Stack frame index {} out of range (0-{})",
                            frame_idx,
                            frames.len().saturating_sub(1)
                        )
                    })?;

                    let frame_id = frame.dap.id;
                    let frame_name = frame.dap.name.clone();

                    let scopes: Vec<_> = session.scopes(frame_id, cx).to_vec();

                    let mut output = String::new();
                    if scopes.is_empty() {
                        output.push_str("No variables available in the current scope.");
                    } else {
                        writeln!(
                            output,
                            "Variables in frame #{} ({}):",
                            frame_idx, frame_name
                        )
                        .ok();

                        for scope in &scopes {
                            writeln!(output, "\n  {}:", scope.name).ok();

                            let variables = session.variables(scope.variables_reference.into(), cx);
                            for var in variables {
                                let type_info = var
                                    .type_
                                    .as_ref()
                                    .map(|t| format!(" ({})", t))
                                    .unwrap_or_default();

                                writeln!(output, "    {} = {}{}", var.name, var.value, type_info)
                                    .ok();
                            }
                        }
                    }

                    Ok(output)
                })
            }),
        }
    }
}

impl AgentTool for DebuggerTool {
    type Input = DebuggerToolInput;
    type Output = String;

    const NAME: &'static str = "debugger";

    fn kind() -> acp::ToolKind {
        acp::ToolKind::Execute
    }

    fn initial_title(
        &self,
        input: Result<Self::Input, serde_json::Value>,
        _cx: &mut App,
    ) -> SharedString {
        match input {
            Ok(input) => match input.operation {
                DebuggerOperation::SetBreakpoint => {
                    if let (Some(path), Some(line)) = (&input.path, input.line) {
                        format!("Set breakpoint at {}:{}", MarkdownInlineCode(path), line).into()
                    } else {
                        "Set breakpoint".into()
                    }
                }
                DebuggerOperation::RemoveBreakpoint => {
                    if let (Some(path), Some(line)) = (&input.path, input.line) {
                        format!("Remove breakpoint at {}:{}", MarkdownInlineCode(path), line).into()
                    } else {
                        "Remove breakpoint".into()
                    }
                }
                DebuggerOperation::ListBreakpoints => "List breakpoints".into(),
                DebuggerOperation::ListSessions => "List debug sessions".into(),
                DebuggerOperation::Continue => "Continue execution".into(),
                DebuggerOperation::Pause => "Pause execution".into(),
                DebuggerOperation::StepOver => "Step over".into(),
                DebuggerOperation::StepIn => "Step into".into(),
                DebuggerOperation::StepOut => "Step out".into(),
                DebuggerOperation::GetStackTrace => "Get stack trace".into(),
                DebuggerOperation::GetVariables => "Get variables".into(),
            },
            Err(_) => "Debugger operation".into(),
        }
    }

    fn run(
        self: Arc<Self>,
        input: ToolInput<Self::Input>,
        _event_stream: ToolCallEventStream,
        cx: &mut App,
    ) -> Task<Result<Self::Output, Self::Output>> {
        let project = self.project.clone();
        cx.spawn(async move |cx| {
            let input = input
                .recv()
                .await
                .map_err(|e| format!("Failed to receive tool input: {e}"))?;
            Self::run_operation(project, input, cx)
                .await
                .map_err(|e| e.to_string())
        })
    }
}
