#!/usr/bin/env bash
# ralph - Agentic iteration loop runner
#
# VERSION: 0.4.0
# CHANGELOG:
#   0.4.0 - Require consecutive COMPLETE detections before stopping (--complete-after)
#   0.3.0 - Add cleanup mode using prompts/cleanup.txt
#   0.2.0 - Fix second SIGINT to properly kill opencode process group
#   0.1.0 - Initial version with vision-expand, reorg, plan, build modes
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="${LOG_DIR:-.ralph-logs}"
ITERATION=0
AUTO_COMMIT=true
MODE="vision-expand"
MAX_ITERATIONS=""
MODEL=""
PROMPT_FILE=""
DRY_RUN=false
DELAY=5
COMPLETE_AFTER=""
COMPLETE_COUNT=0
STOP_REQUESTED=false
OPENCODE_PID=""

request_stop() {
    if [[ "$STOP_REQUESTED" == "true" ]]; then
        # Second interrupt - kill opencode immediately
        echo ""
        log_warn "Forcing immediate stop"
        if [[ -n "$OPENCODE_PID" ]] && kill -0 "$OPENCODE_PID" 2>/dev/null; then
            kill -INT -"$OPENCODE_PID" 2>/dev/null || true
        fi
        exit 130
    fi
    STOP_REQUESTED=true
    echo ""
    log_warn "Stop requested - will exit after current iteration (Ctrl+C again to force)"
}
trap request_stop SIGINT SIGTERM

usage() {
    cat <<'EOF'
Usage: ralph [MODE] [OPTIONS]

MODE:
  vision-expand | expand    Iteratively expand vision (default)
  vision-reorg  | reorg     Reorganize/prune core docs
  plan                     Create/update implementation plan
  build                    Implement specs; build/test loop
  cleanup                  Atomic cleanup/refactor loop

OPTIONS:
  --max-iterations N        Maximum iterations before stopping
  --complete-after N        Consecutive COMPLETEs required to stop (default: 2-3 per mode)
  --dry-run                 Print prompt + config, then exit
  --no-commit               Disable auto-commit
  --prompt-file FILE        Use a custom prompt file
  --delay SECONDS           Delay between iterations (default: 5)
  --log-dir DIR             Directory for iteration logs (default: .ralph-logs)
  -m, --model MODEL         Model to use
  -h, --help                Show this help message

ENVIRONMENT VARIABLES:
  MAX_ITERATIONS, LOG_DIR, MODEL
EOF
}

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

timestamp() {
    date +"%Y-%m-%dT%H:%M:%S%z"
}

log() {
    local ts
    ts="$(timestamp)"
    echo -e "${BLUE}[${ts}] [ralph] [${MODE}]${NC} $*"
}

log_success() {
    local ts
    ts="$(timestamp)"
    echo -e "${GREEN}[${ts}] [ralph] [${MODE}]${NC} $*"
}

log_warn() {
    local ts
    ts="$(timestamp)"
    echo -e "${YELLOW}[${ts}] [ralph] [${MODE}]${NC} $*"
}

log_error() {
    local ts
    ts="$(timestamp)"
    echo -e "${RED}[${ts}] [ralph] [${MODE}]${NC} $*" >&2
}

log_debug() {
    [[ "${RALPH_DEBUG:-}" != "1" ]] && return 0
    local ts
    ts="$(timestamp)"
    echo -e "${BLUE}[${ts}] [ralph] [${MODE}] DEBUG:${NC} $*"
}

if [[ $# -gt 0 ]] && [[ "$1" != -* ]]; then
    MODE="$1"
    shift
fi

case "$MODE" in
    vision-expand|expand)
        MODE="vision-expand"
        MAX_ITERATIONS="${MAX_ITERATIONS:-50}"
        COMPLETE_AFTER="${COMPLETE_AFTER:-2}"
        MODEL="${MODEL:-opencode/claude-opus-4-5}"
        PROMPT_FILE="${PROMPT_FILE:-$SCRIPT_DIR/prompts/vision-expansion.txt}"
        ;;
    vision-reorg|reorg)
        MODE="vision-reorg"
        MAX_ITERATIONS="${MAX_ITERATIONS:-5}"
        COMPLETE_AFTER="${COMPLETE_AFTER:-2}"
        MODEL="${MODEL:-opencode/claude-sonnet-4-5}"
        PROMPT_FILE="${PROMPT_FILE:-$SCRIPT_DIR/prompts/vision-reorg.txt}"
        ;;
    plan)
        MODE="plan"
        MAX_ITERATIONS="${MAX_ITERATIONS:-5}"
        COMPLETE_AFTER="${COMPLETE_AFTER:-2}"
        MODEL="${MODEL:-opencode/claude-opus-4-5}"
        PROMPT_FILE="${PROMPT_FILE:-$SCRIPT_DIR/prompts/plan.txt}"
        ;;
    build)
        MODE="build"
        MAX_ITERATIONS="${MAX_ITERATIONS:-200}"
        COMPLETE_AFTER="${COMPLETE_AFTER:-3}"
        MODEL="${MODEL:-opencode/claude-opus-4-5}"
        PROMPT_FILE="${PROMPT_FILE:-$SCRIPT_DIR/prompts/build.txt}"
        ;;
    cleanup)
        MODE="cleanup"
        MAX_ITERATIONS="${MAX_ITERATIONS:-200}"
        COMPLETE_AFTER="${COMPLETE_AFTER:-3}"
        MODEL="${MODEL:-opencode/claude-opus-4-5}"
        PROMPT_FILE="${PROMPT_FILE:-$SCRIPT_DIR/prompts/cleanup.txt}"
        ;;
    *)
        echo "Unknown mode: $MODE" >&2
        usage >&2
        exit 1
        ;;
esac

while [[ $# -gt 0 ]]; do
    case "$1" in
        --max-iterations)
            MAX_ITERATIONS="$2"
            shift 2
            ;;
        --complete-after)
            COMPLETE_AFTER="$2"
            shift 2
            ;;
        --dry-run)
            DRY_RUN=true
            shift
            ;;
        --no-commit)
            AUTO_COMMIT=false
            shift
            ;;
        --delay)
            DELAY="$2"
            shift 2
            ;;
        --prompt-file)
            PROMPT_FILE="$2"
            shift 2
            ;;
        --log-dir)
            LOG_DIR="$2"
            shift 2
            ;;
        -m|--model)
            MODEL="$2"
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            log_error "Unknown option: $1"
            usage >&2
            exit 1
            ;;
    esac
done

if [[ ! -f "$PROMPT_FILE" ]]; then
    log_error "Prompt file not found: $PROMPT_FILE"
    exit 1
fi
PROMPT_LABEL="$(basename "$PROMPT_FILE")"
PROMPT_CONTENT="$(cat "$PROMPT_FILE")"

if ! command -v opencode >/dev/null 2>&1; then
    log_error "opencode command not found. Please install opencode first."
    exit 1
fi

OPENCODE_CMD=(opencode run)
if [[ -n "$MODEL" ]]; then
    OPENCODE_CMD+=( -m "$MODEL" )
fi

if [[ "$DRY_RUN" == "true" ]]; then
    log "Dry run mode"
    log "Mode: $MODE"
    log "Max iterations: $MAX_ITERATIONS"
    log "Complete after: $COMPLETE_AFTER consecutive"
    [[ -n "$MODEL" ]] && log "Model: $MODEL"
    log "Prompt: $PROMPT_LABEL"
    [[ "$AUTO_COMMIT" == "true" ]] && log "Auto-commit: enabled" || log "Auto-commit: disabled"
    log "Command: ${OPENCODE_CMD[*]} '<prompt>'"
    echo ""
    echo "---"
    echo "$PROMPT_CONTENT"
    echo "---"
    exit 0
fi

mkdir -p "$LOG_DIR"

IN_GIT_REPO=false
REPO_ROOT=""
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    IN_GIT_REPO=true
    REPO_ROOT="$(git rev-parse --show-toplevel)"
fi

LOG_DIR_GIT_PATH=""
if [[ "$IN_GIT_REPO" == "true" ]]; then
    LOG_DIR_ABS="$(cd "$LOG_DIR" && pwd)"
    if [[ "$LOG_DIR_ABS" == "$REPO_ROOT"/* ]]; then
        LOG_DIR_GIT_PATH="${LOG_DIR_ABS#$REPO_ROOT/}"
    fi
fi

if [[ "$AUTO_COMMIT" == "true" ]]; then
    if [[ "$IN_GIT_REPO" != "true" ]]; then
        log_error "Auto-commit enabled but not in a git repository. Re-run with --no-commit."
        exit 1
    fi

    if ! git -C "$REPO_ROOT" config user.name >/dev/null 2>&1 || ! git -C "$REPO_ROOT" config user.email >/dev/null 2>&1; then
        log_error "Git user.name/user.email not configured. Configure git or re-run with --no-commit."
        exit 1
    fi

    if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
        log_error "Working tree is not clean. Commit/stash changes or re-run with --no-commit."
        exit 1
    fi
fi

auto_commit_iteration() {
    local iteration="$1"

    if [[ "$AUTO_COMMIT" != "true" ]]; then
        return 0
    fi

    local msg="ralph [${MODE}]: iteration ${iteration} (${PROMPT_LABEL}${MODEL:+; model=${MODEL}})"

    git -C "$REPO_ROOT" add -A
    if [[ -n "$LOG_DIR_GIT_PATH" ]]; then
        git -C "$REPO_ROOT" reset --quiet -- "$LOG_DIR_GIT_PATH" || true
    fi

    if git -C "$REPO_ROOT" diff --cached --quiet; then
        return 0
    fi

    if ! git -C "$REPO_ROOT" commit -m "$msg"; then
        log_error "git commit failed; aborting loop"
        return 1
    fi

    log_success "Committed changes: $msg"
}

working_tree_changed() {
    if [[ "$IN_GIT_REPO" != "true" ]]; then
        echo "unknown"
        return 0
    fi

    if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
        echo "true"
        return 0
    fi

    echo "false"
}

log "Starting loop"
log "Max iterations: $MAX_ITERATIONS"
log "Complete after: $COMPLETE_AFTER consecutive"
log "Prompt: $PROMPT_LABEL"
log "Logs will be saved to: $LOG_DIR"
[[ -n "$MODEL" ]] && log "Model: $MODEL"
[[ "$AUTO_COMMIT" == "true" ]] && log "Auto-commit: enabled" || log "Auto-commit: disabled"

while [[ $ITERATION -lt $MAX_ITERATIONS ]]; do
    ITERATION=$((ITERATION + 1))
    TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
    LOG_FILE="$LOG_DIR/${MODE}_iteration_${ITERATION}_${TIMESTAMP}.log"

    log "--- Iteration $ITERATION of $MAX_ITERATIONS ---"

    TEMP_OUTPUT="$(mktemp)"
    # Run opencode with SIGINT ignored via signal mask inheritance
    # Using setsid to create new process group so parent's SIGINT doesn't reach child
    FIFO="$(mktemp -u)"
    mkfifo "$FIFO"
    tee "$TEMP_OUTPUT" < "$FIFO" &
    TEE_PID=$!
    
    # Use setsid to put opencode in its own process group, isolating it from terminal SIGINT
    log_debug "Starting opencode"
    setsid "${OPENCODE_CMD[@]}" "$PROMPT_CONTENT" > "$FIFO" 2>&1 &
    OPENCODE_PID=$!
    log_debug "opencode started with PID $OPENCODE_PID (new session)"
    
    # wait in loop - it returns early on trapped signals
    while kill -0 "$OPENCODE_PID" 2>/dev/null; do
        log_debug "waiting for PID $OPENCODE_PID..."
        wait "$OPENCODE_PID" 2>/dev/null || true
    done
    wait "$OPENCODE_PID" 2>/dev/null
    OPENCODE_EXIT=$?
    log_debug "opencode exited with code $OPENCODE_EXIT"
    
    OPENCODE_PID=""
    wait "$TEE_PID" 2>/dev/null || true
    rm -f "$FIFO"
    if [[ $OPENCODE_EXIT -eq 0 ]]; then
        cp "$TEMP_OUTPUT" "$LOG_FILE"

        HAS_CHANGES="$(working_tree_changed)"

        auto_commit_iteration "$ITERATION"

        if grep -q "^COMPLETE$" "$TEMP_OUTPUT"; then
            if [[ "$HAS_CHANGES" == "true" ]]; then
                COMPLETE_COUNT=0
                log_warn "COMPLETE detected but changes were made - resetting count"
            else
                COMPLETE_COUNT=$((COMPLETE_COUNT + 1))
                log_success "COMPLETE detected ($COMPLETE_COUNT/$COMPLETE_AFTER)"

                if [[ $COMPLETE_COUNT -ge $COMPLETE_AFTER ]]; then
                    log_success "Consecutive COMPLETE threshold reached"
                    rm -f "$TEMP_OUTPUT"
                    exit 0
                fi
            fi
        else
            if [[ $COMPLETE_COUNT -gt 0 ]]; then
                log_warn "COMPLETE streak broken - resetting count"
            fi
            COMPLETE_COUNT=0
        fi
    else
        STATUS=$?
        log_warn "opencode failed (exit $STATUS); continuing"
        cp "$TEMP_OUTPUT" "$LOG_FILE" || true
        COMPLETE_COUNT=0
    fi

    rm -f "$TEMP_OUTPUT"

    if [[ "$STOP_REQUESTED" == "true" ]]; then
        log_success "Stopping as requested after iteration $ITERATION"
        exit 0
    fi

    if [[ $ITERATION -lt $MAX_ITERATIONS ]]; then
        if [[ "$DELAY" -gt 0 ]]; then
            log "Iteration $ITERATION complete. Waiting ${DELAY}s before next..."
            sleep "$DELAY"
        else
            log "Iteration $ITERATION complete."
        fi
    else
        log "Iteration $ITERATION complete."
    fi
done

log_warn "Maximum iterations ($MAX_ITERATIONS) reached"
exit 2
