# Otty fish integration — payload (sourced from config.fish managed block)
#
# Idempotent: re-sourcing in the same shell is a no-op. `return` (not `exit`)
# so non-interactive fish that sources config.fish isn't killed.
set -q __otty_loaded; and return
set -g __otty_loaded 1

# Guard: only for interactive shells.
status is-interactive; or return

# ── Bundled `otty` on PATH ──
# Mirror of the bash/zsh logic: if "Install CLI" hasn't been run, the host
# passes the bundled binary's bin dir via $OTTY_BIN_DIR. fish_user_paths is
# the right level to prepend (won't fight the user's own PATH setup).
if test -n "$OTTY_BIN_DIR" -a -d "$OTTY_BIN_DIR"
    if not contains -- "$OTTY_BIN_DIR" $PATH
        set -gx PATH "$OTTY_BIN_DIR" $PATH
    end
end

set -g __otty_cmd_executed ""
set -g __otty_progress_active ""

# Match command line against $OTTY_PROGRESS_COMMANDS (pipe-separated list of
# whitespace-delimited prefixes, e.g. "git push").
function __otty_progress_match
    set -l cmd $argv[1]
    test -z "$OTTY_PROGRESS_COMMANDS"; and return 1
    # Strip leading `NAME=value` env assignments.
    while string match -rq '^[A-Za-z_][A-Za-z0-9_]*=.* .*$' -- $cmd
        set cmd (string replace -r '^[^ ]+ +' '' -- $cmd)
    end
    for p in (string split '|' -- $OTTY_PROGRESS_COMMANDS)
        test -z "$p"; and continue
        if test "$cmd" = "$p"
            return 0
        end
        if string match -q "$p *" -- $cmd
            return 0
        end
    end
    return 1
end

function __otty_prompt_start --on-event fish_prompt
    # Capture $status immediately — it holds the previous command's exit code.
    set -l last $status
    if test -n "$__otty_cmd_executed"
        printf '\e]133;D;%d\a' $last
    end
    if test -n "$__otty_progress_active"
        if test $last -eq 0
            printf '\e]9;4;5;0\a'
        else
            printf '\e]9;4;2\a'
        end
        set -g __otty_progress_active ""
    end
    # See the zsh payload: the token is what distinguishes our prompt mark
    # from one emitted by a program running under us.
    # Not inside a multiplexer only. A nested shell inherits the very same
    # token, and a multiplexer can forward marks to the outer terminal — today
    # by a script wrapping them in tmux's DCS passthrough, and possibly by
    # default in future (tmux/tmux#5237). An inner prompt would then
    # authenticate against the OUTER terminal and reclaim state belonging to
    # the multiplexer itself, yanking it off the alternate screen mid-session.
    # Dropping the token degrades to a bare mark: nothing is reclaimed, which
    # is what terminals without this feature do anyway.
    if test -n "$OTTY_PROMPT_TOKEN" -a -z "$TMUX" -a -z "$STY" -a -z "$ZELLIJ"
        printf '\e]133;A;otty=%s\a' "$OTTY_PROMPT_TOKEN"
    else
        printf '\e]133;A\a'
    end
    set -g __otty_cmd_executed ""
end

function __otty_prompt_end --on-event fish_prompt
    printf '\e]133;B\a'
end

function __otty_preexec --on-event fish_preexec
    printf '\e]133;C\a'
    set -g __otty_cmd_executed 1
    if __otty_progress_match "$argv[1]"
        printf '\e]9;4;3\a'
        set -g __otty_progress_active 1
    end
end

# ── OSC 7 reporting ──
# Every OSC 7 goes through here. fish has no forked subshell and its
# `--on-variable PWD` handler writes to the real stdout rather than into a
# command substitution's capture buffer, so the cwd path was never exposed the
# way zsh's chpwd hook was — but the `ssh` wrapper below is an ordinary
# function, and `set logs (ssh host cat …)` runs it with stdout redirected into
# the capture pipe, wrapping the captured output in OSC 7.
#
# `status is-command-substitution` reports the dynamic context, so it is true
# inside a function called from a substitution — verified, not assumed.
# `isatty stdout` additionally covers a plain redirect to a file.
function __otty_osc7 --argument-names _otty_payload
    status is-command-substitution; and return 1
    isatty stdout; or return 1
    printf '\e]7;%s\a' $_otty_payload
end

function __otty_cwd_report --on-variable PWD
    __otty_osc7 "file://"(hostname)"$PWD"
end
# Emit initial CWD.
__otty_osc7 "file://"(hostname)"$PWD"

# ── Dump resolved $PATH for the autocomplete engine ──
# The app process inherits only the bare launchd PATH (/usr/bin:/bin:…);
# the completion engine reads this dump to scan the user's real PATH
# (Homebrew/cargo/mise/…) for command-name completion. Once-only — PATH is
# captured after config.fish has finished setting it up.
function __otty_dump_path --on-event fish_prompt
    set -q __otty_path_dumped; and return
    set -g __otty_path_dumped 1
    set -l tmpdir $TMPDIR
    test -n "$tmpdir"; or set tmpdir /tmp
    set -l dump "$tmpdir/otty-path-"(id -u)
    string join ':' $PATH >$dump 2>/dev/null
end

# ── Omit `otty` prefix + custom aliases (deferred to first prompt) ──
# These wrapper functions must be installed AFTER the user's config.fish so
# otty's wrappers win over same-named functions defined there, and so the
# allow_overwrite sentinel can actually replace a user's existing definition.
# But when auto-loaded from vendor_conf.d we run BEFORE config.fish — so defer
# the whole block to the first fish_prompt (mirrors the zsh deferred-precmd
# load). On the managed-block path (tmux) we're already sourced at the end of
# config.fish, where ordering is fine; deferring there is harmless.
function __otty_install_wrappers --on-event fish_prompt
    # One-shot: only the first prompt installs the wrappers.
    set -q __otty_wrappers_done; and return
    set -g __otty_wrappers_done 1

# Install `edit`/`view`/`watch`/`jump`/`learn` wrappers ONCE; each one stat()s the
# omit_prefix sentinel under $OTTY_FLAGS_DIR at call time so the Settings
# toggle takes effect immediately, without waiting for the next prompt
# event. Sentinel absent → fall through to `command <name>` so PATH lookup
# behaves exactly as if the wrapper weren't there (system `watch` on Linux
# still works). By default, names already defined as user functions are
# left untouched; the `allow_overwrite` sentinel (evaluated at shell init
# — restart the shell for the toggle to take effect) opts into replacing
# them.
function __otty_should_wrap
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/allow_overwrite"
        # `alias` in fish is implemented as a function of the same name, so
        # removing the function covers both. Erase first so our wrapper wins.
        functions -q $argv[1]; and functions -e $argv[1]
        return 0
    end
    functions -q $argv[1]; and return 1
    return 0
end
__otty_should_wrap edit; and function edit
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/omit_prefix"
        command otty edit $argv
    else
        command edit $argv
    end
end
__otty_should_wrap view; and function view
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/omit_prefix"
        command otty view $argv
    else
        command view $argv
    end
end
__otty_should_wrap watch; and function watch
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/omit_prefix"
        command otty watch $argv
    else
        command watch $argv
    end
end
__otty_should_wrap jump; and function jump
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/omit_prefix"
        # Zoxide-style: resolve only, then `builtin cd` locally — keeps the
        # transcript silent (no echoed `cd '<path>'` from line-discipline,
        # no duplicate prompt after CLI exits).
        if test (count $argv) -eq 0
            # No-args: toggle between $HOME and the last jump-source dir.
            # Local-only state ($OTTY_JUMP_PREV) so up-arrow still recalls
            # the user's literal `jump` invocation, not an injected `cd`.
            if test "$PWD" = "$HOME" -a -n "$OTTY_JUMP_PREV"
                set -l _otty_dest "$OTTY_JUMP_PREV"
                set -g OTTY_JUMP_PREV "$PWD"
                builtin cd -- "$_otty_dest"
            else
                set -g OTTY_JUMP_PREV "$PWD"
                builtin cd -- "$HOME"
            end
            return
        end
        set -l _otty_p (command otty jump --no-cd $argv); or return
        if test -n "$_otty_p"
            set -g OTTY_JUMP_PREV "$PWD"
            builtin cd -- $_otty_p
        end
    else
        command jump $argv
    end
end
__otty_should_wrap learn; and function learn
    if test -n "$OTTY_FLAGS_DIR" -a -f "$OTTY_FLAGS_DIR/omit_prefix"
        command otty learn $argv
    else
        command learn $argv
    end
end
# `watch:<agent>` — wait for a Claude/Codex/OpenCode session to reach
# idle. The colon is part of the command name as far as the shell is
# concerned, so the bare `watch` wrapper above never catches it; install
# one wrapper per agent. Keep in sync with WATCH_AGENTS in cli/src/main.rs.
for __otty_a in claude codex opencode
    if __otty_should_wrap "watch:$__otty_a"
        eval "function watch:$__otty_a
            if test -n \"\$OTTY_FLAGS_DIR\" -a -f \"\$OTTY_FLAGS_DIR/omit_prefix\"
                command otty watch:$__otty_a \$argv
            else
                command watch:$__otty_a \$argv
            end
        end"
    end
end
set -e __otty_a

# ── Custom CLI aliases ($OTTY_CLI_ALIASES = "name=cmd|name=cmd|…") ──
if test -n "$OTTY_CLI_ALIASES"
    for __otty_pair in (string split '|' -- $OTTY_CLI_ALIASES)
        test -z "$__otty_pair"; and continue
        set -l __otty_name (string split -m1 '=' -- $__otty_pair)[1]
        set -l __otty_cmd  (string split -m1 '=' -- $__otty_pair)[2]
        test -z "$__otty_name" -o -z "$__otty_cmd"; and continue
        string match -rq '^[A-Za-z_][A-Za-z0-9_]*$' -- $__otty_name; or continue
        string match -rq '^[A-Za-z_][A-Za-z0-9_]*$' -- $__otty_cmd;  or continue
        __otty_should_wrap $__otty_name; or continue
        # Use eval to bake the captured $__otty_cmd into the function body so
        # later changes to the local var can't leak into the wrapper.
        # When the alias maps to a wrapped command (jump/edit/view/watch/
        # learn), delegate to the wrapper function so its `--no-cd` /
        # builtin-cd logic still runs. Calling `command otty $cmd` directly
        # would bypass the wrapper and re-introduce the visible
        # `cd '<path>'` echo plus a spurious shell-history entry.
        switch $__otty_cmd
            case jump edit view watch learn
                eval "function $__otty_name; $__otty_cmd \$argv; end"
            case '*'
                eval "function $__otty_name; command otty $__otty_cmd \$argv; end"
        end
    end
    set -e __otty_pair
    set -e __otty_name
    set -e __otty_cmd
end

    functions -e __otty_should_wrap
end

# ── Command history scope ($OTTY_HISTORY_SCOPE) ──
# fish is already per-pane by design: a session reads the history file at
# startup and then keeps its own commands in memory, so a sibling's commands
# never appear mid-session. Upstream declined to make cross-session syncing a
# config option (fish-shell#825), which is why `history merge` exists as a
# manual verb. `per-pane` therefore needs no action; only `shared` does work.
if test "$OTTY_HISTORY_SCOPE" = shared
    function __otty_history_merge --on-event fish_prompt
        history merge
    end
end

# ── Up/Down arrow behavior ($OTTY_HISTORY_UP_ARROW) ──
# fish's default `up-or-search` already prefix-matches, so `prefix-search` just
# makes that explicit and pins it against a user rebinding.
#
# `sequential` is deliberately NOT implemented here. fish offers prefix search
# and "previous match" (which matches anywhere in the line), but no widget that
# ignores what is typed and walks the history entry by entry. Binding
# `history-search-backward` would silently give substring search instead of
# sequential walking, so fish keeps its own default rather than a wrong one.
if status is-interactive
    switch "$OTTY_HISTORY_UP_ARROW"
        case prefix-search
            bind \e\[A history-prefix-search-backward
            bind \eOA history-prefix-search-backward
            bind \e\[B history-prefix-search-forward
            bind \eOB history-prefix-search-forward
    end
end

# ── SSH Integration ──
if test "$OTTY_SSH_INTEGRATION" = "1"
    function ssh --wraps=ssh
        set -l _otty_term "xterm-256color"
        set -l _otty_opts \
            -o "SetEnv=COLORTERM=truecolor TERM_PROGRAM=otty" \
            -o "SendEnv=COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION"

        # ── Resolve hostname via ssh -G ──
        set -l _otty_user ""
        set -l _otty_hostname ""
        for _line in (command ssh -G $argv 2>/dev/null)
            set -l _parts (string split " " -- $_line)
            switch $_parts[1]
                case "user"
                    set _otty_user $_parts[2]
                case "hostname"
                    set _otty_hostname $_parts[2]
            end
            if test -n "$_otty_user" -a -n "$_otty_hostname"
                break
            end
        end

        set -l _otty_target "$_otty_user@$_otty_hostname"
        set -l _otty_tmpdir (set -q TMPDIR; and echo $TMPDIR; or echo /tmp)
        # $fish_pid, not %self: fish removed `%` process expansion in 3.1, so
        # %self is a literal everywhere now (and even on older fish it only
        # expanded as a standalone token, never concatenated like this). A
        # literal here means the host looks for otty-ssh-shell-<pid>.meta and
        # never finds it — no ControlMaster reuse for remote git/SFTP.
        set -l _otty_meta_file "$_otty_tmpdir/otty-ssh-shell-$fish_pid.meta"
        set -l _otty_ctl_path ""
        # Declared at function scope, not inside the `if` below: fish's `set -l`
        # is block-scoped, so a declaration in that block would be invisible to
        # the cleanup at the end of this function.
        set -l _otty_ctl_dir ""

        # ── Split argv into connection args and the remote command ──
        # `ssh host cmd` folds every trailing argument into ONE remote command
        # string, so anything appended after the user's own arguments lands as
        # arguments OF their command. The terminfo probe below used to pass
        # $argv and then append its script, which made the remote run
        #     <user command> otty_term=…; <probe script>
        # — the user's command executed a second time, in the probe connection,
        # with a stray argument glued on (`ssh host 'echo TOKEN'` printed
        # "TOKEN otty_term=xterm-256color" and then "TOKEN").
        #
        # Where the connection arguments end is a getopt(3) question — which
        # letters take a value, how clusters like `-vp 2222` split, that ssh
        # re-enters getopt after the destination — and it is answered in ONE
        # place, `otty ssh scan-argv` (packages/ssh-argv), which the macOS app
        # also reaches through the FFI. It used to be four transcriptions of
        # the same rules in four languages, and they drifted: OpenSSH 9.4's
        # `-P tag` took a value in none of them.
        #
        # `otty` is on PATH here — this payload prepends $OTTY_BIN_DIR at the
        # top. When it is not (a hand-installed payload with no app behind it),
        # the safe degradation is the one already used for a ControlPath we
        # cannot mint: skip the extras and let plain ssh run.
        set -l _otty_conn
        set -l _otty_has_cmd 0
        set -l _otty_scan_ok 0
        set -l _otty_scan (command otty ssh scan-argv -- $argv 2>/dev/null)
        set -l _otty_fields (string match -r '^conn=(\d+) cmd=([01])$' -- "$_otty_scan")
        if test (count $_otty_fields) -eq 3
            set _otty_has_cmd $_otty_fields[3]
            set _otty_scan_ok 1
            # `$argv[1..0]` is an error in fish, so an empty connection list
            # (no destination) is spelled out rather than sliced.
            if test $_otty_fields[2] -gt 0
                set _otty_conn $argv[1..$_otty_fields[2]]
            end
        end

        # `_otty_scan_ok` too: without a scan we do not know where the
        # user's remote command begins, so neither the probe nor a
        # ControlMaster (whose teardown needs the same split) may run.
        if test -n "$_otty_hostname" -a $_otty_scan_ok -eq 1
            # Set up a ControlMaster socket so the main connection can be reused
            # for SFTP/exec by the file/git panels.
            #
            # If the caller already passed an explicit ControlPath, honor it and
            # don't mint a competing master. Otty's split-pane "inherit SSH"
            # passes `-o ControlPath=<socket>` to ride the existing master — no
            # re-auth, no proxy/alias re-resolution.
            for _otty_arg in $argv
                switch $_otty_arg
                    case 'ControlPath=*'
                        set _otty_ctl_path (string replace 'ControlPath=' '' -- $_otty_arg)
                end
            end
            if test -z "$_otty_ctl_path"
                # ControlPath must stay short: macOS caps Unix-domain socket
                # paths at 104 bytes and $TMPDIR (/var/folders/...) already eats
                # ~49 of them, so the socket goes directly under /tmp instead.
                # mktemp -d yields a fresh 0700 dir, keeping this race-free.
                #
                # Deliberately no fallback when mktemp is unavailable. A
                # predictable name like /tmp/otty-ssh.$fish_pid in a
                # world-writable directory can be pre-created by another local
                # user, and this path becomes the `-o ControlPath` that every
                # panel exec and SFTP transfer rides — the same class of attack
                # the host guards against when it vets the metadata file's
                # ownership. Skipping multiplexing is the safe degradation:
                # socket= is written empty and the host simply leaves
                # controlSocket unset.
                set _otty_ctl_dir (mktemp -d /tmp/otty-ssh.XXXXXX 2>/dev/null)
                if test -n "$_otty_ctl_dir"
                    set _otty_ctl_path "$_otty_ctl_dir/socket"
                    set _otty_opts $_otty_opts \
                        -o "ControlMaster=auto" \
                        -o "ControlPath=$_otty_ctl_path" \
                        -o "ControlPersist=60"
                end
            end

            # Terminfo install (cached per user@host).
            #
            # Skipped entirely when the caller supplied a remote command: a
            # probe there would open a second connection behind the back of a
            # script that asked for exactly one, and terminfo only matters for a
            # session that has a tty. TERM then stays at the xterm-256color
            # default, which every system terminfo database already resolves.
            set -l _otty_cache_dir "$_otty_tmpdir/otty-terminfo-cache"
            command mkdir -p "$_otty_cache_dir" 2>/dev/null
            set -l _otty_cache_file "$_otty_cache_dir/$_otty_target"

            if test -f "$_otty_cache_file"
                set _otty_term (set -q TERM; and echo $TERM; or echo "xterm-256color")
            else if test $_otty_has_cmd -eq 0; and command -v infocmp >/dev/null 2>&1
                set -l _otty_terminfo (infocmp -0 -x 2>/dev/null)

                if test -n "$_otty_terminfo"
                    # Remote command (TERM injected via the leading assignment):
                    #   1. If the remote already resolves this TERM (system DB or
                    #      a user entry), do nothing — never shadow a standard
                    #      name like xterm-256color with our copy.
                    #   2. Otherwise install via `tic`, then verify the fresh
                    #      entry actually has `clear=`; a truncated transfer can
                    #      compile a stub that lacks clear/cup and breaks tput &
                    #      tmux. If incomplete, remove it and fail so we fall back
                    #      to the system database instead of a broken shadow.
                    set -l _otty_remote_term (set -q TERM; and echo $TERM; or echo "xterm-256color")
                    # Deliberately NOT sharing $_otty_ctl_path here. This probe
                    # used to run with ControlMaster=yes and hand its master to
                    # the interactive ssh below, saving one auth round-trip. But
                    # that demoted the interactive connection to a mux *client*,
                    # and a client's interactive session can hang with no output
                    # at all — no banner, no prompt, nothing — which reads as
                    # "ssh froze". It only bit on the first connection to a host
                    # (later ones hit the terminfo cache and skipped this probe
                    # entirely), which is what made it look intermittent.
                    # ControlPath=none keeps the probe off the socket so the
                    # interactive ssh below stays the master.
                    if echo "$_otty_terminfo" | command ssh \
                        -o "ControlMaster=no" \
                        -o "ControlPath=none" \
                        $_otty_conn \
                        "otty_term=$_otty_remote_term; "'command -v infocmp >/dev/null 2>&1 && infocmp "$otty_term" >/dev/null 2>&1 && exit 0; command -v tic >/dev/null 2>&1 || exit 1; mkdir -p ~/.terminfo 2>/dev/null; tic -x - >/dev/null 2>&1 || exit 1; command -v infocmp >/dev/null 2>&1 && { infocmp -1 "$otty_term" 2>/dev/null | grep -q "clear=" || { rm -f ~/.terminfo/?/"$otty_term" ~/.terminfo/??/"$otty_term" 2>/dev/null; exit 1; }; }; exit 0' \
                        2>/dev/null
                        set _otty_term (set -q TERM; and echo $TERM; or echo "xterm-256color")
                        command touch "$_otty_cache_file"
                    else if test -n "$OTTY_DEBUG_SSH"
                        # Nothing is cached on failure — deliberately, so a host
                        # that later grows `tic` is picked up — which means a
                        # permanently failing probe re-runs on every connection.
                        # Opt-in only: an unconditional line here would land in
                        # the middle of `set logs (ssh host …)`.
                        echo "otty: terminfo probe failed for $_otty_target; TERM stays xterm-256color" >&2
                    end
                end
            end

            printf 'user=%s\nhostname=%s\nsocket=%s\n' $_otty_user $_otty_hostname $_otty_ctl_path > "$_otty_meta_file" 2>/dev/null
        end

        # Notify Otty we're entering an SSH session. Guarded like every other
        # OSC 7: `set logs (ssh host cat …)` runs this wrapper with stdout
        # redirected into the substitution's capture pipe.
        __otty_osc7 "ssh://"(test -n "$_otty_hostname"; and echo $_otty_hostname; or echo "unknown")

        TERM=$_otty_term command ssh $_otty_opts $argv
        set -l _ret $status

        command rm -f "$_otty_meta_file" 2>/dev/null

        # Retire the ControlMaster we minted above. Only ours — a ControlPath the
        # caller passed in belongs to whoever created it, hence the _otty_ctl_dir
        # guard. Without this the master lingers for the whole ControlPersist
        # window and its mktemp'd directory was never removed at all, so every
        # ssh invocation leaked one empty /tmp/otty-ssh.XXXXXX forever.
        if test -n "$_otty_ctl_dir"
            command ssh -o "ControlPath=$_otty_ctl_path" -O exit $_otty_conn >/dev/null 2>&1
            command rm -rf "$_otty_ctl_dir" 2>/dev/null
        end
        # Sweep dirs orphaned by earlier crashes. -empty means a live master's
        # dir (it still holds its socket) is never touched. The trailing slash on
        # /tmp/ is load-bearing: on macOS /tmp is a symlink to private/tmp and
        # find won't descend into it without one, silently matching nothing.
        command find /tmp/ -maxdepth 1 -type d -name 'otty-ssh.*' -empty -delete 2>/dev/null

        # Restore local CWD reporting. Guarded in lockstep with the ssh:// mark
        # above — both conditions are unchanged across the call, so the pair is
        # always emitted together or not at all.
        __otty_osc7 "file://"(hostname)"$PWD"
        return $_ret
    end
end
