//! Diff mode (#208) — Phase 1: `:DiffOrig`.
//!
//! Shows a unified diff of the current buffer (in-memory, possibly unsaved)
//! against its on-disk version, in a read-only `filetype=diff` scratch buffer
//! opened in a vertical split. Closes with `:q` like any split.
//!
//! The diff itself is computed by [`hjkl_app::git::unified_diff`] (git2
//! `Patch::from_buffers`, no repository required), so this is reusable for the
//! dirty-buffer reload-diff prompt (#241).

use super::App;

impl App {
    /// `:DiffOrig` — open a read-only split with the unified diff of the active
    /// buffer vs its on-disk file.
    pub(crate) fn diff_orig(&mut self) {
        use crate::app::window::{LayoutTree, SplitDir, Window};
        use hjkl_buffer::View;
        use hjkl_engine::{BufferEdit, Settings};

        // Must be a real on-disk file (not a scratch / explorer buffer).
        let Some(path) = self.active().filename.clone() else {
            self.bus.error("E32: No file name");
            return;
        };

        // RPC FS-policy gate: `:DiffOrig` must not read a slot whose filename
        // escapes the working directory (renamed via the RPC API) — the outside
        // file's bytes would land in the diff split. No-op in the TUI (policy
        // off).
        let path = match self.resolve_read_path(&path) {
            Ok(p) => p,
            Err(msg) => {
                self.bus.error(msg);
                return;
            }
        };

        // On-disk bytes (the "old" side). A missing file diffs against empty.
        // In RPC mode the read is capped: the slot's path can point at a file
        // whose size an untrusted client controls, so an over-cap file refuses
        // the diff (no split) instead of allocating its size.
        let disk = if hjkl_engine::policy::fs_restricted() {
            match hjkl_fs::read_capped(&path, crate::embed::rpc_read_cap()) {
                Ok(bytes) => bytes,
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
                Err(e) => {
                    self.bus
                        .error(format!("E484: Can't open file {}: {e}", path.display()));
                    return;
                }
            }
        } else {
            std::fs::read(&path).unwrap_or_default()
        };

        // View bytes (the "new" side), produced exactly as `:w` would write
        // them (trailing newline) so an unmodified buffer yields an empty diff.
        let buf_bytes: Vec<u8> = {
            let rope = self.active_editor().buffer().rope();
            let mut b = Vec::with_capacity(rope.len_bytes() + 1);
            for chunk in rope.chunks() {
                b.extend_from_slice(chunk.as_bytes());
            }
            if !b.is_empty() {
                b.push(b'\n');
            }
            b
        };

        let label = path
            .file_name()
            .map_or_else(|| "file".to_string(), |n| n.to_string_lossy().into_owned());
        let diff = hjkl_app::git::unified_diff(
            &disk,
            &buf_bytes,
            &format!("a/{label}"),
            &format!("b/{label}"),
        )
        .unwrap_or_default();

        let body = if diff.trim().is_empty() {
            format!("# :DiffOrig — no differences between buffer and {label}\n")
        } else {
            diff
        };

        // Build a read-only scratch slot holding the diff (mirrors `do_vnew`).
        // Split off a REGULAR window, never a dock — see `do_split`.
        self.focus_editor_window_for_open();
        let focused = self.focused_window();
        // Inherit the focused window's scroll from its own editor (#151 Phase D).
        let (top_row, top_col) = self.window_scroll(focused);

        let new_slot_idx = {
            let buffer_id = self.next_buffer_id;
            self.next_buffer_id += 1;
            let mut view = View::new();
            BufferEdit::replace_all(&mut view, &body);
            // Read-only view: block edits/insert-mode entry.
            let settings = Settings {
                filetype: "diff".to_string(),
                modifiable: false,
                ..Settings::default()
            };
            let mut slot = super::BufferSlot::new(buffer_id, view, settings);
            // Mark clean so `:q` doesn't trip the unsaved-changes guard and no
            // swap is written for this transient view.
            slot.snapshot_saved();
            self.slots.push(slot);
            self.slots.len() - 1
        };

        let new_win_id = self.next_window_id;
        self.next_window_id += 1;
        self.windows.push(Some(Window::new(new_slot_idx)));
        self.reconcile_window_editors();
        self.seed_window_editor(new_win_id, 0, 0, top_row, top_col);
        // New diff window on the left (a), original on the right (b).
        self.layout_mut()
            .replace_leaf(focused, move |id| LayoutTree::Split {
                dir: SplitDir::Vertical,
                ratio: 0.5,
                fixed: None,
                a: Box::new(LayoutTree::Leaf(new_win_id)),
                b: Box::new(LayoutTree::Leaf(id)),
                last_rect: None,
            });
        self.set_focused_window(new_win_id);
    }
}
