#!/usr/bin/env bash

# MCP servers, registered with the agent runtimes on this machine.
#
# An MCP server is a URL plus a bearer token (or, for a stdio server, a launcher
# script plus a token), and the token is the whole reason
# this is a stage rather than a manifest line. Panama is a public repository, so
# the tokens cannot live in it, and neither runtime keeps its server list in a
# file that could be symlinked anyway: Codex writes them into config.toml beside
# dozens of unrelated settings, and Claude Code into ~/.claude.json. There is no
# file to point at, so this registers them through the runtime's own CLI.
#
# What is tracked is user/agents/mcp/servers, which names each server and which
# variable carries its token. What is not tracked is user/agents/mcp/env, which
# holds the tokens. A new machine gets the servers by dropping its own env file
# beside the tracked one and re-running ./install.
#
# This is personal content, so it obeys the same interview decision link-user
# does. A machine that never said yes registers nothing.
#
# Claude Code and opencode are handled. Codex stores its servers inside
# config.toml, and rewriting a section of somebody's live TOML is a worse
# failure mode than leaving two lines for them to paste once.
#
# opencode has no CLI for this either, but its config is plain JSON, so this
# stage merges two keys into it with jq: one mcp entry per server row, and the
# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md
# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in
# that file is left as it was.

set -euo pipefail

log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; }

PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
MCP_DIR="$PANAMA_PATH/user/agents/mcp"
SERVERS="$MCP_DIR/servers"
ENV_FILE="$MCP_DIR/env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
DECISION="$STATE_DIR/user-content"

[[ -r "$SERVERS" ]] || { log "No MCP server list; nothing to register."; exit 0; }

# The same gate link-user uses, read the same way, so one answer governs all
# personal content rather than two stages disagreeing about it.
decision="$([[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no')"
if [[ "$decision" != "yes" ]]; then
    log "Personal content is not enabled on this machine; no MCP servers registered."
    exit 0
fi

has_claude=false
has_opencode=false
command -v claude >/dev/null 2>&1 && has_claude=true
command -v opencode >/dev/null 2>&1 && has_opencode=true
if ! $has_claude && ! $has_opencode; then
    log "Neither Claude Code nor opencode is installed; nothing to register."
    exit 0
fi

# Tokens are optional. A machine without the env file still registers any server
# that needs no header, and says which ones it skipped rather than failing.
if [[ -r "$ENV_FILE" ]]; then
    set -a
    # shellcheck source=/dev/null
    . "$ENV_FILE"
    set +a
else
    warn "No $ENV_FILE; servers needing a token will be skipped."
fi

registered=0
skipped=0
# The opencode mcp object, built up one server at a time.
opencode_mcp='{}'

while read -r name transport target token_var _rest; do
    case "${name:-}" in ''|'#'*) continue ;; esac
    if [[ -z "${transport:-}" || -z "${target:-}" ]]; then
        warn "Ignoring malformed row for '$name'."
        continue
    fi

    token=""
    if [[ -n "${token_var:-}" ]]; then
        token="${!token_var:-}"
        if [[ -z "$token" ]]; then
            warn "Skipping $name: $token_var is not set in $ENV_FILE."
            skipped=$((skipped + 1))
            continue
        fi
    fi

    # An http server's token normally travels as an Authorization header. One
    # that only reads it from the query string (Penpot's userToken) marks the
    # spot with {TOKEN} in its URL instead, and gets no header. A stdio server's
    # target is a launcher under user/agents/mcp/, run from this checkout so
    # every machine runs the same script, and its token arrives as MCP_TOKEN.
    url="$target"
    args=()
    if [[ "$transport" == stdio ]]; then
        launcher="$MCP_DIR/$target"
        if [[ ! -x "$launcher" ]]; then
            warn "Skipping $name: $launcher is not an executable launcher."
            skipped=$((skipped + 1))
            continue
        fi
        [[ -n "$token" ]] && args=(-e "MCP_TOKEN=$token")
    elif [[ -n "$token" ]]; then
        if [[ "$url" == *'{TOKEN}'* ]]; then
            url="${url//\{TOKEN\}/$token}"
        else
            args=(-H "Authorization: $token")
        fi
    fi

    auth=""
    [[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"

    if [[ "$transport" == stdio ]]; then
        entry="$(jq -cn --arg c "$launcher" --arg t "$token" \
            '{type: "local", command: [$c]} + (if $t == "" then {} else {environment: {MCP_TOKEN: $t}} end)')"
    else
        entry="$(jq -cn --arg u "$url" --arg a "$auth" \
            '{type: "remote", url: $u} + (if $a == "" then {} else {headers: {Authorization: $a}} end)')"
    fi
    opencode_mcp="$(jq -c --arg n "$name" --argjson e "$entry" '.[$n] = $e' <<<"$opencode_mcp")"

    $has_claude || continue

    # A server already registered exactly as this row would register it is left
    # alone. Removing and re-adding it would throw away the OAuth session of a
    # server that signs in on its own (bill-tracker, atlassian), so every
    # upgrade would sign them out again.
    if [[ "$transport" == stdio ]]; then
        want="$(jq -cn --arg c "$launcher" --arg t "$token" \
            '{command: $c, env: (if $t == "" then null else {MCP_TOKEN: $t} end)}')"
        have="$(jq -c --arg n "$name" \
            '.mcpServers[$n] // {} | {command, env: (if (.env // {}) == {} then null else .env end)}' \
            "$HOME/.claude.json" 2>/dev/null || true)"
    else
        want="$(jq -cn --arg u "$url" --arg a "$auth" \
            '{url: $u, auth: (if $a == "" then null else $a end)}')"
        have="$(jq -c --arg n "$name" \
            '.mcpServers[$n] // {} | {url, auth: (.headers.Authorization // null)}' \
            "$HOME/.claude.json" 2>/dev/null || true)"
    fi
    if [[ -n "$have" && "$have" == "$want" ]]; then
        log "Already registered $name."
        registered=$((registered + 1))
        continue
    fi

    # Re-registering is how a new server or a rotated token reaches the
    # runtime. Removing first avoids the CLI refusing a name it already knows.
    # Neither call may print the token, so both are quiet unless they fail.
    claude mcp remove "$name" -s user >/dev/null 2>&1 || true
    if [[ "$transport" == stdio ]]; then
        add=(claude mcp add "$name" -s user "${args[@]}" -- "$launcher")
    else
        add=(claude mcp add --transport "$transport" "$name" "$url" "${args[@]}" -s user)
    fi
    if "${add[@]}" >/dev/null 2>&1; then
        log "Registered $name."
        registered=$((registered + 1))
    else
        warn "Could not register $name."
        skipped=$((skipped + 1))
    fi
done <"$SERVERS"

if $has_claude; then
    log "MCP servers: $registered registered, $skipped skipped."
    log "Claude Code loads them at start, so restart a running session to pick them up."
fi

$has_opencode || exit 0

# opencode reads opencode.jsonc or opencode.json, whichever this machine has.
# The file holds tokens once this runs, so it is written private and swapped in
# whole, the way ~/.claude.json already holds them for Claude Code.
OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
opencode_config="$OPENCODE_DIR/opencode.jsonc"
[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json"
rules="$HOME/.claude/rules/*.md"

mkdir -p "$OPENCODE_DIR"
current='{"$schema": "https://opencode.ai/config.json"}'
[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")"

# jq cannot read JSONC comments. A file that has them is left alone rather
# than rewritten without them.
if ! merged="$(jq --argjson mcp "$opencode_mcp" --arg rules "$rules" \
    '.mcp = ((.mcp // {}) + $mcp) | .instructions = (((.instructions // []) - [$rules]) + [$rules])' \
    <<<"$current" 2>/dev/null)"; then
    warn "Could not parse $opencode_config as JSON; opencode MCP servers not written."
    exit 0
fi

tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")"
chmod 600 "$tmp"
printf '%s\n' "$merged" >"$tmp"
mv "$tmp" "$opencode_config"
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config."
