# Root-only Justfile for monorepo
# Per-crate justfiles have been removed; use crate-* commands instead

set shell := ["bash", "-euo", "pipefail", "-c"]

ci := env("CI", "false")
wrap := justfile_directory() + "/tools/agent-wrap.sh"
nextest_profile := if ci == "true" { "ci" } else if ci == "1" { "ci" } else { env("NEXTEST_PROFILE", "minimal") }
nextest_verbose := env("NEXTEST_VERBOSE", "0")
nextest_args := if nextest_verbose == "1" { "--status-level all --verbose" } else if nextest_verbose == "true" { "--status-level all --verbose" } else { "" }

# BEGIN:xtask:autogen justfile:mcp-servers

MCP_SERVERS := "agentic-mcp opencode-orchestrator-mcp"

# END:xtask:autogen

default: help

help:
    @echo "Workspace commands:"
    @echo "  just check            # fmt-check + clippy for entire workspace"
    @echo "  just fix              # auto-fix clippy warnings for entire workspace"
    @echo "  just test             # local: mcp-test + nextest (requires Node/npx)"
    @echo "  just test-ci          # CI-safe: frontend tests + nextest"
    @echo "  just desktop-install   # frozen Bun dependency install (explicit network lane)"
    @echo "  just desktop-check     # format, lint, and typecheck desktop frontend"
    @echo "  just desktop-test      # test Attention desktop frontend"
    @echo "  just desktop-build     # build Attention desktop frontend"
    @echo "  just desktop-audit     # audit Attention desktop dependencies (network lane)"
    @echo "  just build            # build entire workspace"
    @echo "  just clean            # clean workspace"
    @echo "  just fmt              # format entire workspace"
    @echo "  just fmt-check        # check formatting for entire workspace"
    @echo "  just test-integration # local: include #[ignore] integration tests"
    @echo "  just test-thoughts-ignored-ci # CI-safe: ignored-only thoughts tests"
    @echo ""
    @echo "Per-crate commands:"
    @echo "  just crate-check <c>  # check a single crate by name"
    @echo "  just crate-test <c>   # test a single crate by name"
    @echo "  just crate-build <c>  # build a single crate by name"
    @echo ""
    @echo "xtask commands:"
    @echo "  just xtask-sync       # sync autogen content (CLAUDE.md, release-plz.toml, README.md, justfile)"
    @echo "  just xtask-verify     # verify metadata, policy, and file freshness"
    @echo "  just xtask-sync-check # check if sync is needed (for CI)"
    @echo "  just xtask-verify-check # full verification including generated files"
    @echo ""
    @echo "Interactive local root recipes may show wrapper summaries; CI and non-TTY runs pass through raw output."
    @echo "Repeat the same cli_just_execute call for the next cached page, or use rerun=true for a fresh execution."
    @echo "Use AGENTIC_WRAP=0 for full local passthrough output."
    @echo "Use NEXTEST_PROFILE=<profile> or NEXTEST_VERBOSE=1 to adjust local nextest behavior."

# Workspace-wide commands

check: fmt-check-just fmt-check desktop-check
    AGENTIC_TASK_NAME=check {{ wrap }} cargo clippy --workspace --all-targets -- -D warnings

fix:
    AGENTIC_TASK_NAME=fix {{ wrap }} cargo clippy --workspace --all-targets --fix --allow-dirty

test: mcp-test
    AGENTIC_TASK_NAME=test {{ wrap }} cargo nextest run --workspace --profile {{ nextest_profile }} {{ nextest_args }}

test-ci: desktop-test
    AGENTIC_TASK_NAME=test-ci {{ wrap }} cargo nextest run --workspace --profile ci {{ nextest_args }}

desktop-install:
    cd apps/attention-desktop && AGENTIC_TASK_NAME=desktop-install {{ wrap }} bun install --frozen-lockfile

desktop-check:
    cd apps/attention-desktop && AGENTIC_TASK_NAME=desktop-check {{ wrap }} bun run check

desktop-test:
    cd apps/attention-desktop && AGENTIC_TASK_NAME=desktop-test {{ wrap }} bun run test

desktop-build:
    cd apps/attention-desktop && AGENTIC_TASK_NAME=desktop-build {{ wrap }} bun run build

desktop-audit:
    cd apps/attention-desktop && AGENTIC_TASK_NAME=desktop-audit {{ wrap }} bun audit

# Run integration tests (includes ignored tests that require git setup)
test-integration: mcp-test
    THOUGHTS_INTEGRATION_TESTS=1 AGENTIC_TASK_NAME=test-integration {{ wrap }} cargo nextest run --workspace --profile {{ nextest_profile }} {{ nextest_args }} -- --include-ignored

# Run ignored-only thoughts integration tests in CI without duplicating normal runs.
test-thoughts-ignored-ci:
    THOUGHTS_INTEGRATION_TESTS=1 AGENTIC_TASK_NAME=test-thoughts-ignored-ci {{ wrap }} cargo nextest run -p thoughts-tool -p thoughts-bin --profile ci {{ nextest_args }} -- --ignored

build: desktop-build
    AGENTIC_TASK_NAME=build {{ wrap }} cargo build --workspace

clean: clean-workspace

clean-workspace:
    AGENTIC_TASK_NAME=clean-workspace {{ wrap }} cargo clean --workspace

fmt:
    AGENTIC_TASK_NAME=fmt-rust {{ wrap }} cargo +nightly fmt --all
    AGENTIC_TASK_NAME=fmt-toml {{ wrap }} taplo fmt $(git ls-files '*.toml')
    cd apps/attention-desktop && bun run format

fmt-check:
    AGENTIC_TASK_NAME=fmt-check-rust {{ wrap }} cargo +nightly fmt --all -- --check
    AGENTIC_TASK_NAME=fmt-check-toml {{ wrap }} taplo fmt --check $(git ls-files '*.toml')
    cd apps/attention-desktop && bun run format:check

# Security audit with cargo-deny
deny:
    AGENTIC_TASK_NAME=deny {{ wrap }} cargo deny check

# Check justfile formatting
fmt-check-just:
    @just --fmt --check --unstable

# Per-crate commands
crate-check crate:
    AGENTIC_TASK_NAME=crate-check-fmt {{ wrap }} cargo +nightly fmt -p {{ crate }} -- --check
    AGENTIC_TASK_NAME=crate-check-clippy {{ wrap }} cargo clippy -p {{ crate }} --all-targets -- -D warnings

crate-test crate:
    AGENTIC_TASK_NAME=crate-test {{ wrap }} cargo nextest run --profile {{ nextest_profile }} {{ nextest_args }} -E 'package({{ crate }})'

crate-build crate:
    AGENTIC_TASK_NAME=crate-build {{ wrap }} cargo build -p {{ crate }}

crate-run crate:
    cargo run -p {{ crate }}

# xtask commands

xtask-sync:
    AGENTIC_TASK_NAME=xtask-sync {{ wrap }} cargo run -p xtask -- sync

xtask-verify:
    AGENTIC_TASK_NAME=xtask-verify {{ wrap }} cargo run -p xtask -- verify

xtask-sync-check:
    AGENTIC_TASK_NAME=xtask-sync-check {{ wrap }} cargo run -p xtask -- sync --check

xtask-verify-check:
    AGENTIC_TASK_NAME=xtask-verify-check {{ wrap }} cargo run -p xtask -- verify --check

# Endpoint coverage commands for opencode-rs SDK
endpoint-coverage:
    cargo run -p xtask -- endpoint-coverage

endpoint-coverage-check:
    AGENTIC_TASK_NAME=endpoint-coverage-check {{ wrap }} cargo run -p xtask -- endpoint-coverage --check

endpoint-coverage-json:
    cargo run -p xtask -- endpoint-coverage --json

# Utility commands

thoughts_sync:
    AGENTIC_TASK_NAME=thoughts_sync {{ wrap }} thoughts sync

shellcheck:
    #!/usr/bin/env bash
    set -euo pipefail
    mapfile -t scripts < <(git ls-files '*.sh')
    if [ ${#scripts[@]} -gt 0 ]; then
      shellcheck "${scripts[@]}"
    else
      echo "No .sh files found"
    fi

actionlint:
    actionlint -color

# Copy a file
cp src dst:
    cp "{{ src }}" "{{ dst }}"

# Remove a file
rm path:
    rm -f "{{ path }}"

# Create a directory (with parents)
mkdir path:
    mkdir -p "{{ path }}"

# Set file executable
chmod-x path:
    chmod +x "{{ path }}"

# ------------------------------------------------------------------------------
# Git - Read-only Navigation (for agents without shell access)
#
# These recipes expose safe, read-only git inspection commands via just.
# All commands use --no-pager to avoid interactive hangs.
# ------------------------------------------------------------------------------

# git-context: Repo snapshot - root, branch/HEAD, remotes, status, last N commits. n defaults to 5.
git-context n="5":
    #!/usr/bin/env bash
    set -euo pipefail

    N="{{ n }}"
    if ! [[ "$N" =~ ^[0-9]+$ ]]; then
      echo "Invalid commit count '$N', defaulting to 5." >&2
      N=5
    fi

    if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
      echo "Not inside a git repository." >&2
      exit 1
    fi

    echo "Repo root:"
    git rev-parse --show-toplevel
    echo

    current_branch="$(git symbolic-ref --short -q HEAD 2>/dev/null || true)"
    if [ -n "$current_branch" ]; then
      echo "Branch: $current_branch"
    else
      head_sha="$(git rev-parse --short HEAD 2>/dev/null || true)"
      if [ -n "$head_sha" ]; then
        echo "HEAD detached at $head_sha"
      else
        echo "No commits yet."
      fi
    fi
    echo

    echo "Remotes:"
    git --no-pager remote -v || true
    echo

    echo "Status (short):"
    git --no-pager status --porcelain -b || true
    echo

    echo "Recent commits:"
    git --no-pager log --decorate --oneline -n "$N" 2>/dev/null || echo "(no commits yet)"

# git-log: Commit history, optionally scoped to a path. n defaults to 20.
git-log n="20" path="":
    #!/usr/bin/env bash
    set -euo pipefail

    N="{{ n }}"
    if ! [[ "$N" =~ ^[0-9]+$ ]]; then
      echo "Invalid commit count '$N', defaulting to 20." >&2
      N=20
    fi

    P="{{ path }}"

    if [ -z "$P" ]; then
      git --no-pager log --decorate --graph --date=short -n "$N" \
        --pretty=format:'%C(auto)%h %ad %an%d %s' 2>/dev/null \
        || echo "(no commits yet)"
    else
      git --no-pager log --decorate --graph --date=short -n "$N" \
        --pretty=format:'%C(auto)%h %ad %an%d %s' -- "$P" 2>/dev/null \
        || echo "(no commits for path or path invalid)"
    fi

# git-diff: Diffs for staged/working tree. area: both|working|staged|head. format: stat|patch|name-only|name-status.
git-diff area="both" format="stat" path="":
    #!/usr/bin/env bash
    set -euo pipefail

    AREA="{{ area }}"
    FORMAT="{{ format }}"
    P="{{ path }}"

    case "$AREA" in
      both|working|staged|head) ;;
      *) echo "Invalid area: '$AREA'. Allowed: both|working|staged|head" >&2; exit 2 ;;
    esac

    fmt_flags=()
    case "$FORMAT" in
      stat)        fmt_flags+=(--stat) ;;
      patch)       fmt_flags+=(-p) ;;
      name-only)   fmt_flags+=(--name-only) ;;
      name-status) fmt_flags+=(--name-status) ;;
      *) echo "Invalid format: '$FORMAT'. Allowed: stat|patch|name-only|name-status" >&2; exit 2 ;;
    esac

    run_diff() {
      local cached="$1"
      if [ -z "$P" ]; then
        if [ "$cached" = "cached" ]; then
          git --no-pager diff --cached "${fmt_flags[@]}" || true
        else
          git --no-pager diff "${fmt_flags[@]}" || true
        fi
      else
        if [ "$cached" = "cached" ]; then
          git --no-pager diff --cached "${fmt_flags[@]}" -- "$P" || true
        else
          git --no-pager diff "${fmt_flags[@]}" -- "$P" || true
        fi
      fi
    }

    case "$AREA" in
      working)
        echo "=== Unstaged changes (working tree) ==="
        run_diff "working"
        ;;
      staged)
        echo "=== Staged changes (index) ==="
        run_diff "cached"
        ;;
      both)
        echo "=== Staged changes (index) ==="
        run_diff "cached"
        echo
        echo "=== Unstaged changes (working tree) ==="
        run_diff "working"
        ;;
      head)
        if [ -z "$P" ]; then
          git --no-pager diff "${fmt_flags[@]}" HEAD || true
        else
          git --no-pager diff "${fmt_flags[@]}" HEAD -- "$P" || true
        fi
        ;;
    esac

# git-blame: Annotate a file to see who last modified each line. Optional line range (start <= end).
git-blame file start="" end="":
    #!/usr/bin/env bash
    set -euo pipefail

    FILE="{{ file }}"
    START="{{ start }}"
    END="{{ end }}"

    if [ -z "$FILE" ]; then
      echo "Usage: just git-blame <file> [start] [end]" >&2
      exit 2
    fi

    range_args=()
    if [ -n "$START" ] || [ -n "$END" ]; then
      if ! [[ "$START" =~ ^[0-9]+$ ]] || ! [[ "$END" =~ ^[0-9]+$ ]]; then
        echo "Line range must be integers: start and end" >&2
        exit 2
      fi
      if [ "$START" -gt "$END" ]; then
        echo "Invalid range: start ($START) > end ($END)" >&2
        exit 2
      fi
      range_args=(-L "${START},${END}")
    fi

    git --no-pager blame -w "${range_args[@]}" -- "$FILE" || true

# git-show: Show commit details (ref only) or file contents at a ref (ref + path).
git-show ref path="":
    #!/usr/bin/env bash
    set -euo pipefail

    REF="{{ ref }}"
    P="{{ path }}"

    if [ -z "$REF" ]; then
      echo "Usage: just git-show <ref> [path]" >&2
      exit 2
    fi

    if [ -z "$P" ]; then
      git --no-pager show --stat --decorate --pretty=fuller "$REF" || true
    else
      git --no-pager show "${REF}:${P}" || true
    fi

# git-files: List tracked files, optionally filtered by whitespace-separated git pathspec patterns (paths containing spaces not supported).
git-files patterns="":
    #!/usr/bin/env bash
    set -euo pipefail

    if [ -z "{{ patterns }}" ]; then
      git --no-pager ls-files
      exit 0
    fi

    # Disable glob expansion so patterns are passed literally to git
    set -f
    git --no-pager ls-files -- {{ patterns }}

# ------------------------------------------------------------------------------
# Schema Generation
# ------------------------------------------------------------------------------

# Generate agentic.schema.json from Rust types
schema-generate:
    cargo run -p agentic-bin -- config schema > agentic.schema.json

# ------------------------------------------------------------------------------
# Git - Write Operations (for agents without shell access)
#
# These recipes enforce git-aware move/remove semantics via git mv / git rm.
# They fail loudly on unsuitable git state instead of falling back to plain mv/rm.
# ------------------------------------------------------------------------------

# git-mv: Move or rename a tracked path with git-aware semantics. Optionally create dst parent first.
git-mv src dst mkdir_parents="true":
    #!/usr/bin/env bash
    set -euo pipefail

    SRC="{{ src }}"
    DST="{{ dst }}"
    MKDIR_PARENTS="{{ mkdir_parents }}"

    if [ -z "$SRC" ] || [ -z "$DST" ]; then
      echo "Usage: just git-mv <src> <dst> [mkdir_parents]" >&2
      exit 2
    fi

    case "$MKDIR_PARENTS" in
      true|false) ;;
      *) echo "Invalid mkdir_parents: '$MKDIR_PARENTS'. Allowed: true|false" >&2; exit 2 ;;
    esac

    if [ "$MKDIR_PARENTS" = "true" ]; then
      mkdir -p "$(dirname "$DST")"
    fi

    git mv -- "$SRC" "$DST"

# git-rm: Remove a tracked path with git-aware semantics. force: true|false. recursive: auto|true|false.
git-rm path force="false" recursive="auto":
    #!/usr/bin/env bash
    set -euo pipefail

    P="{{ path }}"
    FORCE="{{ force }}"
    RECURSIVE="{{ recursive }}"

    if [ -z "$P" ]; then
      echo "Usage: just git-rm <path> [force] [recursive]" >&2
      exit 2
    fi

    case "$FORCE" in
      true|false) ;;
      *) echo "Invalid force: '$FORCE'. Allowed: true|false" >&2; exit 2 ;;
    esac

    case "$RECURSIVE" in
      auto|true|false) ;;
      *) echo "Invalid recursive: '$RECURSIVE'. Allowed: auto|true|false" >&2; exit 2 ;;
    esac

    rm_flags=()
    if [ "$FORCE" = "true" ]; then
      rm_flags+=(-f)
    fi

    case "$RECURSIVE" in
      true)
        rm_flags+=(-r)
        ;;
      auto)
        if [ -d "$P" ]; then
          rm_flags+=(-r)
        fi
        ;;
    esac

    git rm "${rm_flags[@]}" -- "$P"

# ------------------------------------------------------------------------------
# MCP Inspector Recipes
# ------------------------------------------------------------------------------
# Interactive MCP Inspector for troubleshooting
# Usage:
#   just mcp-inspector              # default: tools/list method

# just mcp-inspector resources/list
mcp-inspector method="tools/list":
    #!/usr/bin/env bash
    set -euo pipefail
    cargo build -p agentic-mcp
    BIN="./target/debug/agentic-mcp"
    if [ ! -x "$BIN" ]; then
      echo "agentic-mcp binary not found: $BIN" >&2
      exit 1
    fi
    echo "Launching MCP Inspector with method: {{ method }}"
    npx -y @modelcontextprotocol/inspector --cli --transport stdio --method "{{ method }}" "$BIN"

# CI-friendly MCP schema validation (validates all MCP servers in MCP_SERVERS)
mcp-test:
    tools/mcp-validate.sh {{ MCP_SERVERS }}

# ------------------------------------------------------------------------------
# PR Description Management
# ------------------------------------------------------------------------------

# Update PR body with autogen content, preserving human notes outside markers
pr-update-autogen number body_file:
    #!/usr/bin/env bash
    set -euo pipefail

    PR="{{ number }}"
    AUTOGEN_FILE="{{ body_file }}"

    if [ -z "$PR" ] || [ -z "$AUTOGEN_FILE" ]; then
      echo "Usage: just pr-update-autogen <pr-number> <autogen-body-file>" >&2
      exit 2
    fi

    if [ ! -f "$AUTOGEN_FILE" ]; then
      echo "Autogen body file not found: $AUTOGEN_FILE" >&2
      exit 2
    fi

    tmp_dir="$(mktemp -d)"
    trap 'rm -rf "$tmp_dir"' EXIT

    current="$tmp_dir/current.md"
    merged="$tmp_dir/merged.md"

    gh pr view "$PR" --json body -q .body > "$current"

    perl -0777 -we '
      use strict;
      use warnings;

      my ($current_path, $autogen_path) = @ARGV;

      local $/;
      open my $cfh, "<", $current_path or die "Failed to read current PR body: $!\n";
      my $body = <$cfh>;
      close $cfh;

      open my $afh, "<", $autogen_path or die "Failed to read autogen body file: $!\n";
      my $autogen = <$afh>;
      close $afh;

      $body    //= "";
      $body = "" if $body eq "null";  # gh outputs literal "null" for empty PR body
      $autogen //= "";
      $autogen =~ s/\n+\z//;

      my $begin = "<!-- BEGIN:describe_pr:autogen pr-description -->";
      my $end   = "<!-- END:describe_pr:autogen -->";
      my $replacement = "$begin\n$autogen\n$end";

      (my $trim = $body) =~ s/\s+//g;
      if ($trim eq "") {
        print "## My Notes (preserved)\n\n";
        print "<!-- Add any scratch notes, todos, and observations here. This section is preserved when /describe_pr runs. -->\n\n";
        print $replacement . "\n";
        exit 0;
      }

      my $has_begin = $body =~ /<!--\s*BEGIN:describe_pr:autogen\s+pr-description\s*-->/;
      my $has_end   = $body =~ /<!--\s*END:describe_pr:autogen\s*-->/;

      if (!$has_begin && !$has_end) {
        warn "WARNING: No describe_pr autogen markers found in PR body; appending autogenerated block to the end.\n";
        $body .= "\n" unless $body =~ /\n\z/;
        print $body . "\n" . $replacement . "\n";
        exit 0;
      }

      if ($has_begin xor $has_end) {
        die "ERROR: Malformed describe_pr autogen markers: expected both BEGIN and END. Refusing to update PR body.\n";
      }

      my $pattern = qr/(?s)<!--\s*BEGIN:describe_pr:autogen\s+pr-description\s*-->.*?<!--\s*END:describe_pr:autogen\s*-->/;
      if ($body !~ $pattern) {
        die "ERROR: Found describe_pr markers but could not match a BEGIN..END block (possible ordering issue). Refusing to update.\n";
      }

      $body =~ s/$pattern/$replacement/;
      print $body;
    ' "$current" "$AUTOGEN_FILE" > "$merged"

    gh pr edit "$PR" --body-file "$merged"
