#!/bin/sh
# bin/bcut-cli — the baocut skill's command entry (macOS / Linux).
#
# Every bcut command the skill runs goes through here:
#
#   bin/bcut-cli --json version
#   bin/bcut-cli serve --ensure
#   bin/bcut-cli transcribe <media> --json
#
# It finds the `bcut` CLI that ships inside the BaoCut App (or a workspace
# build / an explicit override), then execs it *through a link named
# `bcut-cli`*, so the process shows up as `bcut-cli` in Activity Monitor / ps
# and is gone the moment the command returns. The long-running runtime is a
# separate process named `bcut-serve`; `serve --ensure` starts or reuses it,
# `serve --stop` ends it.
#
# The skill's version gate rides along: metadata.requireSpec from SKILL.md is
# passed as `--require-spec`, so an incompatible CLI fails before doing work.
#
# Exit codes: the CLI's own. Launcher-only failures: 2 usage; 3 no BaoCut App
# found (stdout carries a structured `app_not_installed` guide; run
# bin/install-app after telling the user).
#
# Launcher-only options (must be the first argument):
#   --print [--json]   print the resolved CLI path and exit; nothing is run
# Environment overrides (rarely needed): BAOCUT_CLI / BCUT_EXECUTABLE /
# BAOCUT_BIN (explicit CLI), BAOCUT_APP (explicit .app bundle),
# BAOCUT_SKILL_NO_DEV=1 (ignore a development checkout around this skill),
# BAOCUT_SKILL_NO_LINK=1 (run the CLI under its own name, no `bcut-cli` link).
set -eu

skill_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
skill_md="$skill_root/SKILL.md"
app_release="$skill_root/app-release.json"

print_only=0
json=0
if [ "${1:-}" = --print ]; then
    print_only=1; shift
    if [ "${1:-}" = --json ]; then json=1; shift; fi
    if [ $# -gt 0 ]; then echo "bcut-cli: --print takes no command" >&2; exit 2; fi
fi

json_value() {
    # Flat lookup of a top-level string key in a small JSON file (no jq / python).
    sed -n "s/^[[:space:]]*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}[[:space:]]*$/\1/p" "$1" | head -n 1
}

metadata_value() {
    # A `  key: "value"` line inside SKILL.md's frontmatter (`version`,
    # `requireSpec`); the metadata there is the only copy of the range.
    sed -n "s/^[[:space:]]*$1:[[:space:]]*[\"']*\\([^\"']*\\)[\"']*[[:space:]]*$/\\1/p" "$skill_md" | head -n 1
}

cli=
cli_source=
app_bundle=

# 1. Explicit override.
for candidate in "${BAOCUT_CLI:-}" "${BCUT_EXECUTABLE:-}" "${BAOCUT_BIN:-}"; do
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then
        cli=$candidate; cli_source=env; break
    fi
done

# 2. This skill is the copy bundled inside an App.
if [ -z "$cli" ]; then
    case "$skill_root" in
        *.app/Contents/Resources/skills/baocut)
            app_bundle=${skill_root%/Contents/Resources/skills/baocut}
            candidate="$app_bundle/Contents/MacOS/bcut"
            if [ -x "$candidate" ]; then cli=$candidate; cli_source=bundle; fi ;;
    esac
fi

# 3. A copy the App placed carries a stamp naming the App CLI it came from.
if [ -z "$cli" ] && [ -f "$skill_root/.baocut-skill.json" ]; then
    candidate=$(json_value "$skill_root/.baocut-skill.json" cli)
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then cli=$candidate; cli_source=stamp; fi
fi

cargo_target_dir() {
    # `build.target-dir` from a Cargo config file, resolved like Cargo does: a
    # relative path is relative to the directory holding `.cargo/`.
    [ -f "$1" ] || return 0
    dir=$(sed -n "/^[[:space:]]*\[build\]/,/^[[:space:]]*\[[^b]/s/^[[:space:]]*target-dir[[:space:]]*=[[:space:]]*[\"']\([^\"']*\)[\"'].*$/\1/p" "$1" | head -n 1)
    case "$dir" in
        "") ;;
        /*) echo "$dir" ;;
        *) echo "$(dirname -- "$(dirname -- "$1")")/$dir" ;;
    esac
}

# 4. Development checkout around this skill (the workspace build). Cargo's
# own precedence decides where `cargo build` in core/ puts the binary:
# CARGO_TARGET_DIR, then core/.cargo, then the checkout's .cargo (whose
# config.toml overrides the config.local.toml it includes), then core/target.
# A machine that moved its target dir still has an old core/target around;
# looking there first would run a stale build. `bcut` hands every command to
# the `bcut-serve` runtime next to it, so a build counts only when both are
# there. With both release and debug present, take the fresher pair (the one
# whose older binary is newer; release on a tie): rebuilding one profile leaves
# the other's `bcut` behind, and an old `bcut` fails the spec gate or lacks
# newer subcommands.
older_of() { if [ "$1" -nt "$2" ]; then echo "$2"; else echo "$1"; fi; }
if [ -z "$cli" ] && [ "${BAOCUT_SKILL_NO_DEV:-0}" != 1 ]; then
    dev_root=$skill_root
    while [ "$dev_root" != / ] && [ -n "$dev_root" ]; do
        if [ -f "$dev_root/core/Cargo.toml" ] && [ -d "$dev_root/apps/cli" ]; then
            for target_dir in "${CARGO_TARGET_DIR:-}" \
                "$(cargo_target_dir "$dev_root/core/.cargo/config.toml")" \
                "$(cargo_target_dir "$dev_root/.cargo/config.toml")" \
                "$(cargo_target_dir "$dev_root/.cargo/config.local.toml")" \
                "$dev_root/core/target"; do
                [ -n "$target_dir" ] || continue
                pair_age=
                for build in release debug; do
                    [ -x "$target_dir/$build/bcut" ] && [ -x "$target_dir/$build/bcut-serve" ] || continue
                    age=$(older_of "$target_dir/$build/bcut" "$target_dir/$build/bcut-serve")
                    if [ -z "$cli" ] || [ "$age" -nt "$pair_age" ]; then
                        cli="$target_dir/$build/bcut"; cli_source=dev; pair_age=$age
                    fi
                done
                [ -z "$cli" ] || break
            done
            break
        fi
        dev_root=$(dirname -- "$dev_root")
    done
fi

# 5. An installed App (macOS).
if [ -z "$cli" ] && [ "$(uname -s)" = Darwin ]; then
    for bundle in "${BAOCUT_APP:-}" /Applications/BaoCut.app "$HOME/Applications/BaoCut.app"; do
        [ -n "$bundle" ] || continue
        if [ -x "$bundle/Contents/MacOS/bcut" ]; then
            app_bundle=$bundle; cli="$bundle/Contents/MacOS/bcut"; cli_source=app; break
        fi
    done
    if [ -z "$cli" ] && command -v mdfind >/dev/null 2>&1; then
        bundle=$(mdfind "kMDItemCFBundleIdentifier == 'com.jimliu.baocut'" 2>/dev/null | head -n 1)
        if [ -n "$bundle" ] && [ -x "$bundle/Contents/MacOS/bcut" ]; then
            app_bundle=$bundle; cli="$bundle/Contents/MacOS/bcut"; cli_source=app
        fi
    fi
fi

# 6. Linux / headless: the standalone CLI on PATH (there is no App to find).
if [ -z "$cli" ] && [ "$(uname -s)" != Darwin ]; then
    if candidate=$(command -v bcut 2>/dev/null) && [ -n "$candidate" ]; then
        cli=$candidate; cli_source=path
    fi
fi

if [ -z "$cli" ]; then
    download=
    case "$(uname -s)-$(uname -m)" in
        Darwin-arm64)
            download=$(sed -n '/"aarch64-apple-darwin"/,/}/p' "$app_release" \
                | sed -n 's/^[[:space:]]*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1) ;;
    esac
    [ -n "$download" ] || download=$(json_value "$app_release" page)
    appcast_version=$(json_value "$app_release" appVersion)
    echo "BaoCut App is not installed; the bcut runtime ships inside the App. Tell the user, then run $skill_root/bin/install-app (downloads and opens the installer; nothing is installed unattended)." >&2
    printf '{"ok":false,"code":"app_not_installed","message":"BaoCut App is not installed; the bcut runtime ships inside the App.","install":{"download":"%s","appcastVersion":"%s","helper":"bin/install-app"}}\n' "$download" "$appcast_version"
    exit 3
fi

# Resolve symlinks once: the `bcut-cli` link must point at the real file (a
# link to a link would move when the intermediate one does), and sibling
# resources (mlx.metallib) live next to the real file.
real_cli=$cli
while [ -L "$real_cli" ]; do
    target=$(readlink "$real_cli")
    case "$target" in
        /*) real_cli=$target ;;
        *) real_cli="$(dirname -- "$real_cli")/$target" ;;
    esac
done
real_cli=$(CDPATH= cd -- "$(dirname -- "$real_cli")" && pwd -P)/$(basename -- "$real_cli")

# The process name. A link named `bcut-cli` in the user's cache points at the
# resolved CLI; exec'ing it makes argv[0] / the kernel's process name
# `bcut-cli`. Nothing is copied. On macOS the kernel names a process after the
# last directory entry it exec'd — a symlink is resolved away (Activity Monitor
# would still say `bcut`), so a hard link is tried first and a symlink is the
# fallback (different volume). On Linux `comm` keeps the symlink's name. When
# no link can be made (read-only home) the CLI runs under its own name — same
# behaviour, plainer name.
entry=$real_cli
if [ "${BAOCUT_SKILL_NO_LINK:-0}" != 1 ]; then
    cache_home=${XDG_CACHE_HOME:-${HOME:-}/.cache}
    link_dir="$cache_home/baocut/bin"
    link="$link_dir/bcut-cli"
    if [ "$link" -ef "$real_cli" ] && ! [ -L "$link" ]; then
        : # hard link to the current binary, reuse
    elif [ "$(uname -s)" != Darwin ] && [ "$(readlink "$link" 2>/dev/null || true)" = "$real_cli" ]; then
        : # symlink to the current binary, reuse
    elif mkdir -p "$link_dir" 2>/dev/null; then
        # The new link is made under a private name and renamed over the old
        # one, so a launcher running in parallel never execs a missing file.
        # A macOS hard link fails across volumes (CLI on an external disk); the
        # symlink it falls back to is then kept as is rather than remade.
        tmp="$link.$$"
        rm -f "$tmp" 2>/dev/null
        if [ "$(uname -s)" = Darwin ] && ln "$real_cli" "$tmp" 2>/dev/null; then :
        elif [ "$(readlink "$link" 2>/dev/null || true)" = "$real_cli" ]; then tmp=
        elif ln -s "$real_cli" "$tmp" 2>/dev/null; then :
        else tmp=; link=; fi
        if [ -n "$tmp" ] && ! mv -f "$tmp" "$link" 2>/dev/null; then
            rm -f "$tmp" 2>/dev/null
            link=
        fi
    else
        link=
    fi
    [ -n "$link" ] && [ -x "$link" ] && entry=$link
fi

if [ "$print_only" = 1 ]; then
    if [ "$json" = 1 ]; then
        printf '{"ok":true,"cli":"%s","source":"%s","entry":"%s","appBundle":"%s"}\n' "$real_cli" "$cli_source" "$entry" "$app_bundle"
    else
        echo "$real_cli"
    fi
    exit 0
fi

# Sibling GPU library for the MLX backend (the CLI also resolves it by its
# real path; exporting keeps older CLIs working when run through the link).
if [ -z "${PMETAL_METALLIB_PATH:-}" ]; then
    for metallib in "$(dirname -- "$real_cli")/mlx.metallib" "$(dirname -- "$real_cli")/../Resources/mlx.metallib"; do
        if [ -s "$metallib" ]; then export PMETAL_METALLIB_PATH="$metallib"; break; fi
    done
fi
# Children the CLI spawns (Agent sessions, workers) must land on this same CLI.
[ -n "${BAOCUT_CLI:-}" ] || export BAOCUT_CLI="$real_cli"
# Where the real binary is when run through a hard link (the CLI cannot find
# that out by itself; it only trusts this when it is the same file).
export BCUT_REAL_EXE="$real_cli"
# Tells the CLI it runs under the skill: a command without `--llm` is pinned
# to `agent` (never a stored provider default).
skill_version=$(metadata_value version)
[ -n "$skill_version" ] && export BAOCUT_SKILL_VERSION="$skill_version"

# `serve` manages the runtime process (`bcut-serve`): run it under the CLI's
# own name. The runtime renames itself, but a CLI from before that change
# would otherwise leave a runtime called `bcut-cli` that its own
# `serve --ensure` reclaim does not recognise.
for arg in "$@"; do
    case "$arg" in
        --*) continue ;;
        serve) entry=$real_cli ;;
    esac
    break
done

required_spec=$(metadata_value requireSpec)
if [ -n "$required_spec" ]; then
    exec "$entry" --require-spec "$required_spec" "$@"
fi
exec "$entry" "$@"
