use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;

use crate::prelude::*;

use app_state::ScreenKind;
use commands::ArgValue;
use editor::position::Position;
use crate::issue_registry::{IssueId, NewIssue, Severity};
use crate::persistent_issues::PersistentNewIssue;
use crate::task_registry::{TaskId, TaskKey, TaskStatus, TaskTrigger};

#[derive(Debug, Clone)]
pub enum ExtensionConfigOp {
    MoveUp,
    MoveDown,
    Select(usize),
    SetStatus(usize),
}
// View-local operations for FileSelector — defined here to avoid a circular
// dependency between operation.rs and views/file_selector.rs.

#[derive(Debug, Clone)]
pub enum FileSelectorOp {
    FilterInput(crate::widgets::input_field::InputFieldOp),
    SwitchPane,
    CollapseOrLeft,
    ExpandOrRight,
    ToggleDir,
    /// Delivered by the async search task with the scored+sorted results.
    /// The `generation` field is used to discard results from superseded
    /// queries (user typed again before this result arrived).
    SetResults {
        generation: u64,
        paths: Vec<PathBuf>,
    },
    /// Delivered by the async directory-read task when a lazy directory
    /// expansion completes.  Each entry is `(absolute_path, is_dir)`.
    DirLoaded {
        path: PathBuf,
        entries: Vec<(PathBuf, bool)>,
    },
}

// View-local operations for LogView.

#[derive(Debug, Clone)]
pub enum LogViewOp {
    FilterInput(crate::widgets::input_field::InputFieldOp),
}

// View-local operations for TaskArchiveView.

#[derive(Debug, Clone)]
pub enum TaskArchiveOp {
    FilterInput(crate::widgets::input_field::InputFieldOp),
    /// Open the log for the highlighted task.
    OpenSelected,
    /// Cancel the highlighted running task.
    CancelSelected,
    /// Sent by app.rs when the task list changes so the view can clamp its cursor.
    TaskUpdated { new_len: usize },
    /// Close the inline detail pane and return to the list.
    CloseDetail,
    /// Populate the inline detail pane with loaded log lines.
    ShowDetail {
        task_id: TaskId,
        title:   String,
        status:  TaskStatus,
        lines:   Vec<String>,
    },
}

// ---------------------------------------------------------------------------
// View-local operations for IssueView.
// ---------------------------------------------------------------------------

/// View-local operations for crate::views::issue_view::IssueView.
#[derive(Debug, Clone)]
pub enum IssueViewOp {
    FilterInput(crate::widgets::input_field::InputFieldOp),
    /// Enter — open detail for the selected issue.
    OpenSelected,
    /// `r` — resolve the currently selected issue.
    ResolveSelected,
    /// `d` — dismiss the currently selected issue.
    DismissSelected,
    /// `h` — toggle visibility of dismissed/resolved issues.
    ToggleShowDismissed,
    /// `0`–`4` — filter by severity; `None` = show all.
    SetSeverityFilter(Option<Severity>),
    /// Esc in Detail mode — back to List.
    CloseDetail,
    /// Sent when IssueAdded / IssueRemoved / IssueUpdated arrive, so the view
    /// can clamp its cursor.
    RegistryChanged,
}

// View-local operations for CommandRunner — same pattern as FileSelectorOp.

#[derive(Debug, Clone)]
pub enum CommandSelectorOp {
    FilterInput(crate::widgets::input_field::InputFieldOp),
    SwitchPane,
    CollapseOrLeft,
    ExpandOrRight,
    /// Confirm selection (Enter) from the selector pane.
    Confirm,
    /// In the arg-form view: move focus to next field.
    ArgNext,
    /// In the arg-form view: move focus to previous field.
    ArgPrev,
    /// In the arg-form view: set the focused field's value.
    ArgChanged(String),
    /// Cancel the arg form and go back to selector.
    ArgCancel,
}

// ---------------------------------------------------------------------------
// View-local operations for CommitWindow.
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub enum CommitOp {
    // ── UI trigger ops (view → app → spawn_blocking) ─────────────────────
    /// Switch between Commit and Diff tabs.
    SwitchTab,
    // Commit tab
    /// Cycle focus: Staged → Unstaged → Message.
    SwitchPane,
    /// Stage or unstage the selected file.
    ToggleFileStage,
    /// Set the commit message text programmatically (e.g., clear or load a draft).
    MessageChanged(String),
    /// Keystroke editing ops for the message field.
    MessageInsertChar(char),
    MessageInsertNewline,
    MessageBackspace,
    MessageCursorLeft,
    MessageCursorRight,
    MessageCursorUp,
    MessageCursorDown,
    /// Open the new-branch input (clears any previous value).
    BranchInputOpen,
    /// Cancel the branch-name input without creating a branch.
    BranchInputCancel,
    /// Set the branch-name input text.
    BranchChanged(String),
    /// Confirm new branch from branch_input.
    BranchCreate,
    /// Open the branch selector popup (shows existing branches).
    BranchSelectorOpen,
    /// Close the branch selector popup without action.
    BranchSelectorCancel,
    /// Checkout the given branch (handled by app.rs with git ops).
    BranchCheckout(String),
    /// Execute commit (guarded: message must be non-empty).
    Confirm,
    /// Enter amend mode by loading the most recent commit message.
    EnterAmendMode,
    /// Confirm an amend (git commit --amend equivalent).
    ConfirmAmend,
    /// Cancel an in-progress amend — exit amend mode without committing.
    CancelAmend,
    // Diff tab
    /// Toggle focus between file list and diff lines panels in the diff tab.
    DiffFocusToggle,
    /// Toggle staged_diff_lines for the selected diff line (local, no git call).
    /// On a hunk header row this toggles fold instead.
    ToggleLineStage,
    /// Revert the selected diff line.
    RevertLine,
    /// Request diff for a file.
    LoadDiff {
        path: PathBuf,
        staged: bool,
    },
    /// Async result ops (spawn_blocking → op_tx → handle_operation)
    StatusLoaded {
        staged: Vec<crate::vcs::StatusEntry>,
        unstaged: Vec<crate::vcs::StatusEntry>,
        branches: Vec<String>,
        current_branch: String,
    },
    DiffLoaded {
        path: PathBuf,
        /// Whether this diff is staged (HEAD↔index) or unstaged (index↔workdir).
        staged: bool,
        lines: Vec<crate::vcs::DiffLine>,
    },
    /// Toggle or set amend mode in the view (delivered after async load).
    AmendModeSet(bool, Option<String>),
    GitOpSuccess(String),
    GitOpError(String),
    /// Async result: detected Git operation mode (rebase / merge / normal).
    /// Delivered by the detection task spawned on CommitWindow open and refresh.
    GitModeDetected {
        mode: crate::vcs::GitMode,
        has_conflicts: bool,
        /// Pre-filled commit message for the current rebase step or merge.
        message: String,
    },
    /// Abort an in-progress rebase (`git rebase --abort`).
    AbortRebase,
    /// Abort an in-progress merge (`git merge --abort`).
    AbortMerge,
}

// LSP-related types and view-local operations for the editor.

#[derive(Debug, Clone)]
pub struct LspCompletionItem {
    pub label: String,
    pub kind: Option<u8>,
    pub detail: Option<String>,
    pub insert_text: Option<String>,
}

#[derive(Debug, Clone)]
pub struct LspLocation {
    pub path: PathBuf,
    pub row: usize,
    pub col: usize,
}

#[derive(Debug, Clone)]
pub enum GoToTarget {
    /// Request LSP definition at the current cursor (editor context).
    LspRequest,
    /// Concrete LSP location returned by server.
    LspLocation(LspLocation),
    /// Open file at line (0-based) and optional column.
    FileLine {
        path: std::path::PathBuf,
        line: usize,
        col: Option<usize>,
    },
    /// Placeholder for future diff/commit targets.
    DiffLine {
        repo: Option<std::path::PathBuf>,
        commit: Option<String>,
        path: std::path::PathBuf,
        line: usize,
    },
    /// Symbol name with optional scope.
    Symbol {
        name: String,
        scope: Option<String>,
    },
    /// Raw URI & range.
    UriRange {
        uri: String,
        start_line: usize,
        start_char: usize,
    },
}

#[derive(Debug, Clone)]
pub enum ClipOp {
    /// Copy active selection (or current line if empty) to clipboard + history.
    Copy,
    /// Cut active selection (or current line if empty) to clipboard + history.
    Cut,
    /// Paste from system clipboard at cursor.
    Paste,
    /// Open the clipboard history picker dialog.
    OpenHistory,
    /// Navigate history picker up.
    HistoryUp,
    /// Navigate history picker down.
    HistoryDown,
    /// Paste the selected history item and close picker.
    HistoryConfirm,
    /// Close the history picker without pasting.
    HistoryClose,
}

#[derive(Debug, Clone)]
pub enum SelectionOp {
    /// Extend active selection head (Shift+arrow).
    Extend { head: Position },
    /// Select the entire buffer.
    SelectAll,
    /// Clear selection (Esc).
    Clear,
}

#[derive(Debug, Clone)]
pub enum GoToLineOp {
    /// Open the go-to-line bar (or focus it if already open).
    Open,
    /// Close the bar without committing.
    Close,
    /// Commit the current line number and close.
    Confirm,
    /// Feed a key to the line-number input field.
    Input(crate::widgets::input_field::InputFieldOp),
    /// Jump directly to a specific line (programmatic, e.g., from terminal link).
    JumpTo { line: usize, column: usize },
}

#[derive(Debug, Clone)]
pub enum SearchOp {
    Open {
        replace: bool,
    },
    Close,
    QueryInput(crate::widgets::input_field::InputFieldOp),
    ReplacementInput(crate::widgets::input_field::InputFieldOp),
    ToggleIgnoreCase,
    ToggleRegex,
    ToggleSmartCase,
    /// Toggle query ↔ replacement input focus.
    FocusSwitch,
    NextMatch,
    PrevMatch,
    ReplaceOne,
    ReplaceAll,
    /// Stream one project-wide file result into the editor search state.
    /// Sent by the async project-search task for every file that contains matches.
    /// `generation` is used to discard results from a superseded search.
    AddProjectResult { file: PathBuf, result: MatchSpan, generation: u64 },
    /// Clear all project-wide file results — sent before a new search starts.
    /// `generation` identifies the search about to begin so the editor can
    /// ignore any in-flight `AddProjectResult` ops from the previous search.
    ClearProjectResults { generation: u64 },
    /// Select a file in the project-search file list (0-based index into `files`).
    SelectFile(usize),
    /// Scroll the match-results panel by N rows (positive = down, negative = up).
    ScrollMatchPanel(i8),
    /// Keystroke routed to the include-glob filter field (Expanded mode only).
    IncludeGlobInput(crate::widgets::input_field::InputFieldOp),
    /// Keystroke routed to the exclude-glob filter field (Expanded mode only).
    ExcludeGlobInput(crate::widgets::input_field::InputFieldOp),
    /// Move cursor up in the search results tree.
    TreeMoveUp,
    /// Move cursor down in the search results tree.
    TreeMoveDown,
    /// Toggle expand/collapse on a file node, or activate a match entry.
    TreeToggle,
    /// Collapse current file node (or move to parent file when on a match row).
    TreeCollapse,
}

#[derive(Debug, Clone)]
pub enum LspOp {
    CompletionResponse {
        items: Vec<LspCompletionItem>,
        trigger: Option<Position>,
        version: Option<i32>,
    },
    /// Sent immediately when completion is triggered to show loading state
    CompletionLoading {
        trigger: Position,
    },
    HoverResponse(Option<String>),
    CompletionMoveUp,
    CompletionMoveDown,
    CompletionConfirm,
    CompletionDismiss,
    HoverDismiss,
}

// ---------------------------------------------------------------------------
// LSP rename
// ---------------------------------------------------------------------------

/// A single text replacement within one file, delivered as part of a
/// workspace edit.  Positions are 0-based (line, UTF-16 character offset).
#[derive(Debug, Clone)]
pub struct LspTextEdit {
    pub start_line: u32,
    pub start_char: u32,
    pub end_line: u32,
    pub end_char: u32,
    pub new_text: String,
}

/// All text edits for a single file, as part of a `WorkspaceEdit` result.
#[derive(Debug, Clone)]
pub struct LspFileEdit {
    pub path: std::path::PathBuf,
    pub edits: Vec<LspTextEdit>,
}

/// View-local operations for the editor rename prompt.
#[derive(Debug, Clone)]
pub enum LspRenameOp {
    /// Open the rename prompt pre-filled with the current symbol name.
    OpenPrompt { current_name: String },
    /// Update the text input field.
    Input(crate::widgets::input_field::InputFieldOp),
    /// Close the prompt without renaming.
    Dismiss,
}
// Project-wide search & replace
// ---------------------------------------------------------------------------

/// A single match span within a file line.
#[derive(Debug, Clone)]
pub struct MatchSpan {
    /// 0-based line index.
    pub line: usize,
    /// Byte offset of match start within the line.
    pub byte_start: usize,
    /// Byte offset of match end within the line.
    pub byte_end: usize,
    /// Full text of the line (no newline).
    pub line_text: String,
}

/// All matches found in one file.
#[derive(Debug, Clone)]
pub struct FileMatchResult {
    pub path: PathBuf,
    pub matches: Vec<MatchSpan>,
}

// ---------------------------------------------------------------------------
// Git History Viewer view-local operations
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub enum GitHistoryOp {
    /// Keystroke routed to the filter input field.
    FilterInput(crate::widgets::input_field::InputFieldOp),
    /// Async result: commits loaded for the current filter + generation.
    CommitsLoaded {
        generation: u64,
        commits: Vec<crate::vcs::CommitInfo>,
    },
    /// User confirmed a revision selection (Enter on revision list).
    /// Intercepted by app.rs which spawns file-list + diff loading.
    SelectRevision,
    /// Async result: file list for the selected revision.
    FilesLoaded {
        oid: String,
        files: Vec<crate::vcs::FileChange>,
    },
    /// User navigated to or confirmed a file selection.
    /// Intercepted by app.rs which spawns diff loading.
    SelectFile,
    /// Async result: diff for the selected revision + file.
    DiffLoaded {
        lines: Vec<crate::vcs::DiffLine>,
    },
    /// An async git task failed.
    LoadError(String),
}

// ---------------------------------------------------------------------------
// Branch Management View view-local operations
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub enum BranchViewOp {
    /// Async result: branch lists loaded on view open.
    BranchesLoaded {
        local: Vec<String>,
        remote: Vec<String>,
        current: String,
    },
    /// Async error while loading branches.
    LoadError(String),
    /// User pressed Enter — intercepted by app.rs to spawn a checkout.
    CheckoutBranch,
    /// Async result: checkout succeeded; carries the new current branch name.
    CheckoutCompleted(String),
    /// Async result: checkout failed.
    CheckoutError(String),
    /// User pressed 'n' — open the new-branch name input.
    NewBranchOpen,
    /// Keystroke routed to the new-branch name input field.
    NewBranchInput(crate::widgets::input_field::InputFieldOp),
    /// User confirmed the new branch name (Enter while input is open).
    /// Intercepted by app.rs to spawn a create+checkout task.
    NewBranchConfirm,
    /// User cancelled the new-branch input (Esc).
    NewBranchCancel,
    /// Async result: branch created and checked out.
    CreateCompleted(String),
    /// Async result: branch creation failed.
    CreateError(String),
    /// User pressed Delete (first press) — app.rs checks merge status and replies with
    /// DeleteConfirmNeeded if required or performs immediate deletion.
    DeleteRequested,
    /// App replies that confirmation is required: (branch, merged)
    DeleteConfirmNeeded { branch: String, merged: bool },
    /// User pressed Delete again to confirm (view converts to this when pending matches).
    DeleteConfirmed,
    /// Async result: deletion succeeded — carries the deleted branch name.
    DeleteCompleted(String),
    /// Async deletion failed.
    DeleteError(String),
}

// ---------------------------------------------------------------------------
// Git Pull view-local operations
// ---------------------------------------------------------------------------

/// View-local operations for the Git Pull workflow view.
///
/// The pull flow is:
/// 1. Fetch remote (`FetchCompleted` / `FetchError`)
///    2a. Fast-forward if possible (`FastForwardCompleted` / `FastForwardError`)
///    2b. Present rebase / merge choice; user navigates + confirms
/// 3. Apply selected strategy (`RebaseCompleted` / `MergeCompleted` / errors)
#[derive(Debug, Clone)]
pub enum GitPullOp {
    // ── Async results ────────────────────────────────────────────────────────

    /// Fetch succeeded.
    ///
    /// `can_fast_forward` is `true` when HEAD is an ancestor of the upstream
    /// ref — i.e., a fast-forward merge is safe and will be applied
    /// automatically.  When `false`, the view shows the Rebase / Merge choice.
    FetchCompleted {
        can_fast_forward: bool,
        /// Short name of the upstream tracking ref (e.g. `"origin/main"`).
        upstream: String,
        /// Name of the local branch being updated.
        current_branch: String,
    },
    /// Fetch failed (network error, no remote configured, etc.).
    FetchError(String),
    /// Fast-forward completed successfully.
    FastForwardCompleted,
    /// Fast-forward failed (should be rare — upstream diverged since fetch).
    FastForwardError(String),
    /// Rebase on the upstream completed successfully.
    RebaseCompleted,
    /// Rebase failed (conflicts or other errors).
    RebaseError(String),
    /// Merge of the upstream completed successfully.
    MergeCompleted,
    /// Merge failed.
    MergeError(String),

    // ── User actions (choice prompt) ─────────────────────────────────────────

    /// Move the highlighted choice up.
    ChoiceUp,
    /// Move the highlighted choice down.
    ChoiceDown,
    /// Confirm the currently highlighted choice (Rebase / Merge / Cancel).
    /// Intercepted by `app.rs`; the view does not need to spawn tasks.
    ChoiceConfirm,
}

// ---------------------------------------------------------------------------
// Git History Editor view-local operations
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub enum HistoryEditorOp {
    // --- Async results -------------------------------------------------------
    /// Initial commit list loaded on view open.
    CommitsLoaded(Vec<crate::vcs::CommitInfo>),
    /// Async operation failed (generic).
    OperationFailed(String),
    /// A git operation (rebase / reset) completed successfully.
    /// Carries the new commit list and optionally the pre-op SHA for undo recording.
    /// `pre_sha` is `None` for undo operations (so they don't themselves get undone again).
    OperationCompleted {
        commits: Vec<crate::vcs::CommitInfo>,
        pre_sha: Option<String>,
        description: String,
    },

    // --- User actions (intercepted by app.rs) --------------------------------
    /// `u` — move selected commit one step toward older commits.
    MoveUp,
    /// `d` — move selected commit one step toward newer commits.
    MoveDown,
    /// `Enter` — confirm pending move (run git rebase).
    ConfirmMove,
    /// `r` — open the commit message editor for rewording.
    StartReword,
    /// `s` — open the combined message editor for squashing with the commit at
    /// the given index (the older commit; one index higher than cursor).
    StartSquash { target_idx: usize },
    /// `ctrl+s` in editor mode — apply the edited message (triggers git).
    ConfirmEdit,
    /// `Del` — drop the selected commit (may require a second press).
    DeleteCommit,
    /// `ctrl+z` — undo the last operation (runs `git reset --hard <sha>`).
    UndoLast,

    // --- Editor mode text input (emitted in EditorMode) ----------------------
    /// Insert a single character into the message buffer.
    EditorInsertChar(char),
    /// Insert a newline into the message buffer.
    EditorInsertNewline,
    /// Backspace in the message buffer.
    EditorBackspace,
    /// Move cursor left in the message buffer.
    EditorCursorLeft,
    /// Move cursor right in the message buffer.
    EditorCursorRight,
}


#[derive(Debug, Clone)]
pub enum TerminalOp {
    /// Raw bytes received from the PTY process (sent by the async read task).
    Output { id: u64, data: Vec<u8> },
    /// Completed styled lines produced from PTY output by the async read task.
    ///
    /// Sent alongside `Output` so the main thread can maintain a styled
    /// scrollback buffer per tab (enabling state persistence and reuse of
    /// `DiagnosticsExtractor::extract_from_line`).
    AppendScrollback { id: u64, lines: Vec<crate::vt_parser::StyledLine> },
    /// Open a new tab. `command` defaults to the configured shell if `None`.
    NewTab { command: Option<String> },
    /// Close a tab by ID. If it was the last tab, a new shell is spawned.
    CloseTab { id: u64 },
    /// Switch to the tab at the given index (clamped to valid range).
    SwitchToTab { index: usize },
    /// Switch to the next tab (wraps around).
    NextTab,
    /// Switch to the previous tab (wraps around).
    PrevTab,
    /// Reset scroll to the bottom of the active tab.
    ScrollReset,
    /// Enable/disable link highlight mode (triggered by holding Ctrl while using the mouse).
    SetLinkHighlight { enabled: bool },
    /// Open the context menu for the given tab.
    OpenMenu { id: u64 },
    /// Close the context menu without taking action.
    CloseMenu,
    /// Confirm the highlighted context-menu item.
    MenuConfirm,
    /// Open the inline rename input for the given tab directly.
    OpenRename { id: u64 },
    /// Clear the screen of the given tab (sends VT clear sequence to PTY).
    ClearTab { id: u64 },
    /// Set the rename input text.
    RenameChanged(String),
    /// Commit the rename.
    RenameConfirm,
    /// Cancel the rename.
    RenameCancel,
    /// Open the tab selector popup (keyboard/mouse-navigable list of all tabs).
    OpenTabSelector,
    /// Terminal dimensions changed — resize all PTY tabs.
    Resize { cols: u16, rows: u16 },
    /// The PTY child process has exited.
    ProcessExited { id: u64 },
}

#[derive(Debug, Clone)]
pub enum SearchReplaceOp {
    //  Input field changes
    QueryChanged(String),
    ReplaceChanged(String),
    IncludeGlobChanged(String),
    ExcludeGlobChanged(String),
    //  Toggle options
    ToggleRegex,
    ToggleCase,
    ToggleSmartCase,
    //  Tree navigation
    TreeExpandOrEnter,
    TreeCollapseOrLeave,
    //  Replace actions
    ReplaceCurrentFile,
    ReplaceAll,
    //  Undo / redo
    Undo,
    Redo,
    //  Async results
    SearchResultsReady {
        results: Vec<FileMatchResult>,
        generation: u64,
    },
    ReplaceComplete {
        path: PathBuf,
        replaced_count: usize,
    },
    AsyncError(String),
    ProgressUpdate {
        scanned: usize,
        total: usize,
    },
}

#[derive(Debug, Clone)]
pub enum Operation {
    // Buffer mutations
    InsertText {
        path: Option<PathBuf>,
        #[allow(dead_code)]
        cursor: Position,
        text: String,
    },
    /// Bracketed paste (or fallback-heuristic detected paste) delivered to the
    /// active editor. Unlike `InsertText`, the full multi-line string is inserted
    /// at once via `insert_text_raw` and also pushed to the clipboard register.
    PasteText {
        path: Option<PathBuf>,
        text: String,
    },
    DeleteText {
        path: Option<PathBuf>,
        #[allow(dead_code)]
        cursor: Position,
        #[allow(dead_code)]
        len: usize,
    },
    DeleteForward {
        path: Option<PathBuf>,
        #[allow(dead_code)]
        cursor: Position,
    },
    DeleteWordBackward {
        path: Option<PathBuf>,
    },
    DeleteWordForward {
        path: Option<PathBuf>,
    },
    IndentLines {
        path: Option<PathBuf>,
    },
    UnindentLines {
        path: Option<PathBuf>,
    },
    DeleteLine {
        path: Option<PathBuf>,
    },
    /// Replace the text from `start` to `end` (same line, byte offsets) with `text`.
    /// Used by CompletionConfirm to delete the filter prefix before inserting.
    ReplaceRange {
        path: Option<PathBuf>,
        start: Position,
        end: Position,
        text: String,
    },
    MoveCursor {
        path: Option<PathBuf>,
        cursor: Position,
    },
    Undo {
        path: Option<PathBuf>,
    },
    Redo {
        path: Option<PathBuf>,
    },
    ToggleFold {
        path: Option<PathBuf>,
        line: usize,
    },
    ToggleMarker {
        path: Option<PathBuf>,
        line: usize,
    },
    ToggleWordWrap,

    // File lifecycle
    OpenFile {
        path: PathBuf,
    },
    /// Navigate from the project search tree to a match.
    /// Opens the file (if not already active) while preserving the search
    /// state, then positions the cursor at the given line and column.
    GoToProjectMatch {
        file: PathBuf,
        line: usize,
        col: usize,
    },
    /// Open an external URL in the system default browser.
    OpenUrl {
        url: String,
    },
    SaveFile {
        path: PathBuf,
    },
    ReloadFromDisk {
        path: PathBuf,
        accept_external: bool,
        /// When true, suppress the "Reloaded from disk." status message.
        silent: bool,
    },

    // App / navigation
    SwitchScreen {
        to: ScreenKind,
    },
    /// Close the currently active overlay, sub-view, or modal.
    ///
    /// The handler in `app.rs` walks a priority list to determine what to
    /// close (completion menu, search bar, modal sub-views, etc.).
    Close,
    Quit,

    // Focus navigation — handled by whichever view is active
    Focus(crate::widgets::focusable::FocusOp),

    // Generic list/tree/scroll navigation — dispatched to the active view.
    // Each view interprets these according to its own focus state.
    NavigateUp,
    NavigateDown,
    NavigatePageUp,
    NavigatePageDown,
    NavigateHome,
    NavigateEnd,

    // View-local ops that don't cross view boundaries
    FileSelectorLocal(FileSelectorOp),
    CommandSelectorLocal(CommandSelectorOp),
    LogViewLocal(LogViewOp),
    CommitLocal(CommitOp),
    GitHistoryLocal(GitHistoryOp),
    BranchViewLocal(BranchViewOp),
    HistoryEditorLocal(HistoryEditorOp),
    GitPullLocal(GitPullOp),
    ExtensionConfig(ExtensionConfigOp),

    // LSP trigger ops (handled in app.rs, forwarded to LspClient)
    LspTriggerCompletion {
        #[allow(dead_code)]
        path: Option<PathBuf>,
        /// The character(s) that triggered completion (for LSP context)
        #[allow(dead_code)]
        trigger_char: Option<String>,
    },
    LspTriggerHover {
        #[allow(dead_code)]
        path: Option<PathBuf>,
        /// If `Some`, this trigger was generated by the idle debounce timer.
        /// The value is the generation counter at spawn time; the handler
        /// ignores this operation if the stored generation has advanced.
        idle_generation: Option<u64>,
    },
    LspGoToDefinition {
        #[allow(dead_code)]
        path: Option<PathBuf>,
    },
    /// Result from a go-to-definition request, dispatched back into the queue.
    LspGoToDefinitionResult(Vec<LspLocation>),
    /// Generic go-to operation for cross-context navigation.
    GoTo {
        target: GoToTarget,
    },
    /// LSP server returned trigger characters for a language.
    LspTriggerCharactersReceived {
        lang_id: String,
        trigger_chars: Vec<String>,
    },
    /// LSP editor-local ops (dropdown navigation, response delivery).
    LspLocal(LspOp),
    /// Trigger the LSP rename prompt for the symbol under the cursor.
    LspTriggerRename {
        #[allow(dead_code)]
        path: Option<PathBuf>,
    },
    /// Sent by the rename prompt after the user confirms a new name.
    LspTriggerRenameWith {
        new_name: String,
        /// Cursor position (line, col) at the time the prompt was opened.
        row: u32,
        col: u32,
    },
    /// Workspace edit returned by the server — apply across all affected files.
    LspRenameResult(Vec<LspFileEdit>),
    /// Editor-local rename prompt ops.
    LspRenameLocal(LspRenameOp),
    /// Search/replace bar ops (editor-local).
    SearchLocal(SearchOp),
    /// Go-to-line bar ops (editor-local).
    GoToLineLocal(GoToLineOp),
    /// Selection management ops (editor-local).
    SelectionLocal(SelectionOp),
    /// Clipboard ops (editor-local).
    ClipboardLocal(ClipOp),
    /// Project-wide search & replace ops.
    SearchReplaceLocal(SearchReplaceOp),

    /// Execute a registered command by id (with pre-filled args).
    RunCommand {
        id: crate::commands::CommandId,
        args: HashMap<String, ArgValue>,
    },

    /// Open a generic context menu at `(x, y)` with the given items.
    ///
    /// Items are `(label, command_id)` pairs.  `app.rs` stores the menu on
    /// [`crate::app_state::AppState`], intercepts input, and dispatches the selected command.
    OpenContextMenu {
        // Each item: (label, command_id, enabled_override)
        // - enabled_override: `Some(bool)` to explicitly set enabled state,
        //   or `None` to let the runtime compute a sensible default.
        items: Vec<(String, crate::commands::CommandId, Option<bool>)>,
        x: u16,
        y: u16,
    },

    /// Close the currently open context menu without selecting any item.
    CloseContextMenu,

    // Escape hatch for plugins
    #[allow(dead_code)]
    Generic {
        source: Cow<'static, str>,
        name: Cow<'static, str>,
        payload: HashMap<String, ArgValue>,
    },

    // Extension system
    /// Broadcast a named event to all active extensions.
    ExtensionEvent {
        name: String,
        payload: HashMap<String, String>,
    },
    /// Display a user-visible notification (shown in status bar).
    ShowNotification {
        message: String,
        level: NotificationLevel,
    },
    /// Update the IDE status bar slot with custom text.
    UpdateStatusBar {
        text: String,
        #[allow(unused)]
        slot: StatusSlot,
    },
    /// Request a terminal be created running the given command.
    CreateTerminal {
        command: String,
    },
    /// Send raw bytes to the PTY of the terminal tab with the given ID.
    TerminalInput {
        id: u64,
        data: Vec<u8>,
    },
    /// Terminal view-local operations.
    TerminalLocal(TerminalOp),
    /// Show a picker (like command palette) populated with arbitrary items.
    ShowPicker {
        title: String,
        items: Vec<PickerItem>,
    },

    /// Deliver pre-computed syntax-highlight spans for an editor buffer.
    ///
    /// Sent by the background highlight task; handled in `app.rs`, which stores
    /// the result in `crate::views::editor::EditorView::highlight_cache` when the version still
    /// matches the live buffer.  The next frame then renders without blocking.
    SetEditorHighlights {
        path: Option<std::path::PathBuf>,
        version: editor::buffer::Version,
        start_line: usize,
        generation: u64,
        spans: Vec<Vec<editor::highlight::StyledSpan>>,
    },

    /// Incremental highlight update for multiple lines.
    ///
    /// Unlike SetEditorHighlights, this operation updates individual lines
    /// without clearing existing highlights. This prevents highlights from
    /// disappearing while typing or scrolling.
    SetEditorHighlightsChunk {
        path: Option<std::path::PathBuf>,
        version: editor::buffer::Version,
        generation: u64,
        spans: Vec<(usize, Vec<editor::highlight::StyledSpan>)>,
    },

    /// Deliver git-diff change line numbers for the editor gutter.
    ///
    /// Sent by a background task after a file is opened or saved.
    /// Contains the set of 0-based line indices that are added or modified
    /// in the workdir relative to the index.
    SetEditorGitChanges {
        path: std::path::PathBuf,
        /// 0-based line indices that have been added or modified.
        changed_lines: std::collections::HashSet<usize>,
    },

    // -----------------------------------------------------------------------
    // Issue registry — ephemeral commands + startup loads
    // (no disk I/O; produced by async tasks and the persistent-load path)
    // -----------------------------------------------------------------------

    /// Add an issue to the registry (registry-only, no disk write).
    ///
    /// `issue.marker = Some(m)` → ephemeral (cleared via `ClearIssuesByMarker`).
    /// `issue.marker = None`   → used only for startup loads by the Persistent
    ///                           Component.  User-initiated persistent issues
    ///                           must go through `PersistentIssueLocal(Add)`.
    AddIssue { issue: NewIssue },

    /// Remove an issue by ID (registry-only).  Emits `IssueRemoved` on success.
    RemoveIssue { id: IssueId },

    /// Mark an issue as resolved (registry-only).  Emits `IssueUpdated` on success.
    ResolveIssue { id: IssueId },

    /// Mark an issue as dismissed (registry-only).  Emits `IssueUpdated` on success.
    DismissIssue { id: IssueId },

    /// Remove all ephemeral issues belonging to `marker`.  Emits one
    /// `IssueRemoved` per removed issue.
    ClearIssuesByMarker { marker: String },

    // -----------------------------------------------------------------------
    // Persistent issue operations — update registry AND write to disk
    // -----------------------------------------------------------------------

    /// Persistent-issue commands: each variant updates the in-memory
    /// [`IssueRegistry`] **and** atomically rewrites `.oo/issues.yaml`.
    ///
    /// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
    PersistentIssueLocal(PersistentIssueOp),

    // -----------------------------------------------------------------------
    // Issue registry — events (enqueued by apply_operation after mutations)
    // -----------------------------------------------------------------------

    /// An issue was added.
    IssueAdded { id: IssueId },

    /// An issue was removed.
    IssueRemoved { id: IssueId },

    /// An issue's `resolved` or `dismissed` flag was toggled.
    IssueUpdated { id: IssueId },

    // -----------------------------------------------------------------------
    // Task system lifecycle events
    // -----------------------------------------------------------------------

    /// A task was created and either started immediately or added to its queue.
    TaskScheduled(TaskId),

    /// A task transitioned from `Pending` to `Running`.
    TaskStarted(TaskId),

    /// A task reached a terminal status (`Success`, `Warning`, or `Error`).
    ///
    /// Sent by [`crate::task_executor::TaskExecutor`] when the spawned process
    /// exits.  Handled by `apply_operation` in `app.rs`, which calls
    /// [`crate::task_registry::TaskRegistry::mark_finished`] and starts the
    /// next queued task if one exists.
    TaskFinished {
        id: TaskId,
        status: TaskStatus,
    },

    /// A task was cancelled (running or queued).
    TaskCancelled(TaskId),

    // -----------------------------------------------------------------------
    // Task system commands
    // -----------------------------------------------------------------------

    /// Schedule a new task.  Handled by `apply_operation`, which calls
    /// [`crate::task_registry::TaskRegistry::schedule_task`] and immediately
    /// spawns the executor if the queue was idle.
    ScheduleTask {
        key: TaskKey,
        trigger: TaskTrigger,
        /// The shell command to execute (passed through to the process).
        command: String,
    },

    /// Cancel a running or queued task by its ID.  Handled by `apply_operation`,
    /// which calls [`crate::task_registry::TaskRegistry::cancel`] and starts the
    /// next queued task if one exists.
    CancelTask(TaskId),

    /// Open the log view and pre-populate its filter with the task's label so
    /// the user can quickly inspect task-related entries.
    OpenLogForTask { task_id: TaskId },

    /// Open the Task Archive View.
    OpenTaskArchive,

    /// Delivered by app.rs after it asynchronously loads task output from the
    /// log store.  Switches to `TaskOutputView` and populates it with the lines.
    ShowTaskOutput {
        task_id: TaskId,
        title: String,
        status: TaskStatus,
        lines: Vec<String>,
    },

    /// View-local ops for `crate::views::task_archive::TaskArchiveView`.
    TaskArchiveLocal(TaskArchiveOp),

    /// Open the Issue List View.
    OpenIssueList,

    /// View-local ops for `crate::views::issue_view::IssueView`.
    IssueViewLocal(IssueViewOp),
}

// ---------------------------------------------------------------------------
// Persistent issue sub-operations
// ---------------------------------------------------------------------------

/// Commands that manipulate **persistent** issues.
///
/// Each variant is handled by `apply_operation` in `app.rs`, which:
/// 1. Calls the corresponding [`IssueRegistry`] mutation method.
/// 2. Enqueues the matching `IssueAdded` / `IssueRemoved` / `IssueUpdated`
///    event operation.
/// 3. Spawns a fire-and-forget [`crate::persistent_issues::save_atomic`] task.
///
/// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
#[derive(Debug, Clone)]
pub enum PersistentIssueOp {
    /// Add a new persistent issue and write the updated list to disk.
    Add { issue: PersistentNewIssue },
    /// Remove a persistent issue by ID and write the updated list to disk.
    Remove { id: IssueId },
    /// Mark a persistent issue as resolved and write the updated list to disk.
    Resolve { id: IssueId },
    /// Mark a persistent issue as dismissed and write the updated list to disk.
    Dismiss { id: IssueId },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NotificationLevel {
    Info,
    Warn,
    Error,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StatusSlot {
    Left,
    Center,
    Right,
}

#[derive(Debug, Clone)]
#[allow(unused)]
pub struct PickerItem {
    #[allow(dead_code)]
    pub label: String,
    #[allow(dead_code)]
    pub value: String,
}

#[derive(Debug, Clone)]
pub struct Event {
    /// Which subsystem produced the operation: "editor", "file_selector", "plugin.git"
    #[allow(dead_code)]
    pub source: Cow<'static, str>,
    #[allow(dead_code)]
    pub kind: EventKind,
}

#[derive(Debug, Clone)]
pub enum EventKind {
    #[allow(dead_code)]
    Applied(Operation),
}

impl Event {
    pub fn applied(source: impl Into<Cow<'static, str>>, op: Operation) -> Self {
        Self {
            source: source.into(),
            kind: EventKind::Applied(op),
        }
    }
}