#!/bin/bash

# AI Project Guide - IDE Setup Script
# Copies project rules to your IDE configuration directory
# Supports: Cursor, Claude Code

set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Script configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Managed-section markers. The compiled composites (CLAUDE.md, AGENTS.md,
# copilot-instructions.md) wrap their generated content in a BEGIN/END pair so
# re-runs replace only that span and leave hand-written content alone.
#
# The HTML-comment form is deliberate: the older link-reference form
# ([//]: # (...)) depends on renderers discarding unused link reference
# definitions, which lightweight non-CommonMark parsers get wrong — it leaks as
# visible text. It also matches the shape Next.js uses for its generated
# AGENTS.md block, so it reads as a familiar convention rather than a custom one.
MANAGED_BEGIN='<!-- BEGIN:context-forge -->'
MANAGED_END='<!-- END:context-forge -->'

# Pre-merge marker. Files stamped with this have generated content running from
# the marker to EOF (nothing was ever written after it), which is what lets
# migration find the span without parsing content. Still emitted by the
# single-file artifacts under .github/, where it is a provenance tag rather than
# a region delimiter.
MANAGED_MARKER='[//]: # (context-forge:managed)'

# Provenance stamp for the single-file artifacts under .github/. Those are
# generated wholesale from one source rule each — there is no user-authored
# region to protect — so they carry a standalone marker rather than a pair.
# Shares the HTML-comment form so a project never contains two marker syntaxes.
GENERATED_MARKER='<!-- context-forge:generated -->'

# Suffix for the one-time backup taken when converting or adopting a file that
# was not previously managed. Deliberately distinct from cf's own ".bak" slot so
# the two never contend for the same path.
PREMANAGED_BACKUP_SUFFIX='.pre-context-forge'

# Comma-separated globs of scoped rule files to leave uninstalled, matched
# against the basename (e.g. 'dart.md,swift*.md'). Empty means install
# everything, which is the default.
#
# Read from cf's `rules.exclude` config when cf is on PATH, since that is where
# the value belongs — it describes the project, not the invocation. The env var
# is the fallback for the standalone case: this script is plain bash that can be
# run directly, with no cf present, so config alone would leave direct callers
# no way to set it at all.
#
# Skip-only by design: a newly added exclusion stops future copies but never
# deletes a file an earlier run installed, since a project may have come to
# depend on it. alwaysApply rules are deliberately not excludable — they are
# compiled into the managed block rather than copied, so excluding one would
# silently drop general.md or git.md.
#
# Which tier supplied the active value, reported when exclusions are in effect:
# an env var left set in a shell would otherwise shadow the project's config
# invisibly, and the only symptom would be a rule quietly missing.
RULES_EXCLUDE_SOURCE=""

# Assigns RULES_EXCLUDE and RULES_EXCLUDE_SOURCE directly rather than printing,
# so both survive — a command substitution would run this in a subshell and
# discard the source.
read_rules_exclude() {
    RULES_EXCLUDE=""
    RULES_EXCLUDE_SOURCE=""

    # An explicit env var wins, so a one-off run can override project config.
    if [ -n "${CONTEXT_FORGE_RULES_EXCLUDE:-}" ]; then
        RULES_EXCLUDE="$CONTEXT_FORGE_RULES_EXCLUDE"
        RULES_EXCLUDE_SOURCE="CONTEXT_FORGE_RULES_EXCLUDE"
        return 0
    fi

    command -v cf >/dev/null 2>&1 || return 0

    # A non-zero exit means the key is unknown (a cf predating this feature) or
    # cf could not answer. Either way: no exclusions, never a hard error, so the
    # script works on both sides of that cf release.
    local output
    output=$(cf config get rules.exclude 2>/dev/null) || return 0

    # `cf config get` prints a labeled block, not a bare value:
    #   Key:     rules.exclude
    #   Value:   dart.md,swift.md
    #   Source:  project
    # Take the Value line and strip its label and surrounding whitespace.
    RULES_EXCLUDE=$(printf '%s' "$output" | awk '
        /^Value:/ {
            sub(/^Value:[[:space:]]*/, "")
            sub(/[[:space:]]+$/, "")
            print
            exit
        }
    ')

    [ -n "$RULES_EXCLUDE" ] && RULES_EXCLUDE_SOURCE="cf config rules.exclude"
    return 0
}

read_rules_exclude

# Patterns that matched at least one file, used to warn about typos.
RULES_EXCLUDE_MATCHED=""

# True when a scoped rule file should be skipped. Args: filename (basename)
rule_is_excluded() {
    local filename=$1

    [ -n "$RULES_EXCLUDE" ] || return 1

    local pattern
    local old_ifs=$IFS
    IFS=','
    for pattern in $RULES_EXCLUDE; do
        # Tolerate spaces after commas rather than silently failing to match.
        pattern="${pattern#"${pattern%%[![:space:]]*}"}"
        pattern="${pattern%"${pattern##*[![:space:]]}"}"
        [ -n "$pattern" ] || continue

        case "$filename" in
            $pattern)
                IFS=$old_ifs
                case ",$RULES_EXCLUDE_MATCHED," in
                    *",$pattern,"*) ;;
                    *) RULES_EXCLUDE_MATCHED="$RULES_EXCLUDE_MATCHED,$pattern" ;;
                esac
                return 0
                ;;
        esac
    done
    IFS=$old_ifs
    return 1
}

# Warn about exclusion patterns that skipped nothing — almost always a typo,
# and otherwise invisible: the user just sees the rule still installed.
#
# Distinguishes a pattern that matched no file at all from one that named an
# alwaysApply rule, which exists but cannot be excluded. Reporting the latter as
# "matched nothing" would send the user hunting for a typo that isn't there.
warn_unmatched_exclusions() {
    [ -n "$RULES_EXCLUDE" ] || return 0

    # Name the source: an env var left set in a shell shadows project config,
    # and the only other symptom is a rule quietly missing.
    print_status $BLUE "ℹ️  Rule exclusions active from $RULES_EXCLUDE_SOURCE: $RULES_EXCLUDE" >&2

    local pattern
    local old_ifs=$IFS
    IFS=','
    for pattern in $RULES_EXCLUDE; do
        pattern="${pattern#"${pattern%%[![:space:]]*}"}"
        pattern="${pattern%"${pattern##*[![:space:]]}"}"
        [ -n "$pattern" ] || continue

        case ",$RULES_EXCLUDE_MATCHED," in
            *",$pattern,"*) continue ;;
        esac

        IFS=$old_ifs
        if always_apply_rule_matches "$pattern"; then
            print_status $YELLOW "⚠️  warning: rules.exclude pattern '$pattern' names an always-on rule and was ignored" >&2
            print_status $YELLOW "   Always-on rules are compiled into the managed block, not installed as files." >&2
        else
            print_status $YELLOW "⚠️  warning: rules.exclude pattern '$pattern' matched no rule files" >&2
        fi
        IFS=','
    done
    IFS=$old_ifs
}

# True when a pattern matches an alwaysApply rule — i.e. one deliberately beyond
# the reach of exclusions. Args: pattern
always_apply_rule_matches() {
    local pattern=$1

    [ -d "$RULES_SOURCE_DIR" ] || return 1

    local file
    for file in "$RULES_SOURCE_DIR"/*.md; do
        [ -f "$file" ] || continue
        has_always_apply "$file" || continue

        local filename
        filename=$(basename "$file")
        case "$filename" in
            $pattern) return 0 ;;
        esac
    done
    return 1
}

# Find the actual project root by looking for project-documents directory
find_project_root() {
    local current_dir="$(pwd)"

    # Look for project-documents directory going up the directory tree
    local search_dir="$current_dir"
    while [[ "$search_dir" != "/" ]]; do
        if [[ -d "$search_dir/project-documents" ]]; then
            # Found project-documents, this is the project root
            echo "$search_dir"
            return
        fi
        search_dir="$(dirname "$search_dir")"
    done

    # If we're in the ai-project-guide standalone repo (no project-documents parent)
    if [[ "$SCRIPT_DIR" == */ai-project-guide/scripts ]]; then
        echo "$(dirname "$SCRIPT_DIR")"
        return
    fi

    # Fallback: assume current directory is project root
    echo "$current_dir"
}

PROJECT_ROOT="$(find_project_root)"
TARGET_ROOT="$PROJECT_ROOT"

# Set source directories
if [[ "$SCRIPT_DIR" == */ai-project-guide/scripts ]]; then
    # We're in the ai-project-guide source repository
    RULES_SOURCE_DIR="$SCRIPT_DIR/../project-guides/rules"
    AGENTS_SOURCE_DIR="$SCRIPT_DIR/../project-guides/agents"
    SKILLS_SOURCE_DIR="$SCRIPT_DIR/../project-guides/skills"
else
    # We're in a project that includes ai-project-guide as submodule
    RULES_SOURCE_DIR="$PROJECT_ROOT/project-documents/ai-project-guide/project-guides/rules"
    AGENTS_SOURCE_DIR="$PROJECT_ROOT/project-documents/ai-project-guide/project-guides/agents"
    SKILLS_SOURCE_DIR="$PROJECT_ROOT/project-documents/ai-project-guide/project-guides/skills"
fi

# Function to check if we're in the right directory
check_directory() {
    local current_dir=$(pwd)
    
    print_status $BLUE "📍 Project root detected: $PROJECT_ROOT"
    print_status $BLUE "📍 IDE directories will be created in: $TARGET_ROOT"
    echo ""
    
    # Warn if we're deep in subdirectories (might be confusing)
    if [[ "$current_dir" != "$PROJECT_ROOT" ]] && [[ "$current_dir" == */project-documents/* ]]; then
        print_status $YELLOW "💡 Note: You're running from inside project-documents/, but IDE files will be created in the project root."
        echo ""
    fi
}

# Function to show usage
show_usage() {
    echo "Usage: $0 <ide>"
    echo ""
    echo "Supported targets:"
    echo "  cursor    - Always-on rules → AGENTS.md; scoped rules → .cursor/rules/ (paths→globs)"
    echo "  claude    - Embed alwaysApply rules in CLAUDE.md, copy others to .claude/rules/"
    echo "  copilot   - Compile rules/skills to .github/ for VS Code Copilot (also writes AGENTS.md)"
    echo "  agents    - Write AGENTS.md, plus skills to .agents/skills/ if present"
    echo "              aliases: openai, codex"
    echo ""
    echo "Examples:"
    echo "  $0 cursor"
    echo "  $0 claude"
    echo "  $0 copilot"
    echo "  $0 agents"
    echo ""
    echo "This script copies AI project rules from project-guides/ to your IDE's"
    echo "configuration directory and validates frontmatter requirements."
}

# Function to print colored output
print_status() {
    local color=$1
    local message=$2
    echo -e "${color}${message}${NC}"
}

# Function to validate frontmatter
validate_frontmatter() {
    local file=$1
    local filename=$(basename "$file")
    
    if ! grep -q "^---" "$file"; then
        print_status $YELLOW "⚠️  WARNING: $filename missing frontmatter"
        return 1
    fi
    
    if ! grep -q "description:" "$file"; then
        print_status $YELLOW "⚠️  WARNING: $filename missing 'description' in frontmatter"
        return 1
    fi
    
    if ! grep -q "paths:" "$file" && ! grep -q "globs:" "$file" && ! grep -q "alwaysApply:" "$file"; then
        print_status $YELLOW "⚠️  WARNING: $filename missing 'paths', 'globs', or 'alwaysApply' in frontmatter"
        return 1
    fi
    
    return 0
}

# Check if a rules file has alwaysApply: true in its frontmatter
has_always_apply() {
    local file=$1
    awk '
    BEGIN { in_frontmatter = 0 }
    /^---$/ {
        if (!in_frontmatter) { in_frontmatter = 1; next }
        else { exit 1 }
    }
    in_frontmatter && /^alwaysApply:[ ]*true/ { exit 0 }
    ' "$file"
}

# Convert paths frontmatter to Cursor globs format
# Input: source file with paths: YAML list
# Output: file with globs: JSON array format
convert_paths_to_globs() {
    local source_file=$1
    local target_file=$2

    awk '
    BEGIN { in_frontmatter = 0; in_paths = 0; path_count = 0 }
    /^---$/ {
        if (!in_frontmatter) {
            in_frontmatter = 1
            print
            next
        } else {
            # End of frontmatter — output collected paths as globs
            if (path_count > 0) {
                printf "globs: ["
                for (i = 1; i <= path_count; i++) {
                    if (i > 1) printf ", "
                    printf "%s", paths[i]
                }
                printf "]\n"
            }
            in_frontmatter = 0
            in_paths = 0
            print
            next
        }
    }
    in_frontmatter {
        # Strip name field (not supported by Cursor)
        if (/^name:/) { next }
        if (/^paths:[ ]*$/) {
            # Start collecting paths entries
            in_paths = 1
            next
        }
        if (in_paths && /^[ ]+-[ ]+/) {
            # Collect path entry (keep quotes)
            path_count++
            val = $0
            sub(/^[ ]+-[ ]+/, "", val)
            paths[path_count] = val
            next
        }
        if (in_paths && !/^[ ]+-/) {
            # End of paths list, new field
            in_paths = 0
        }
        print
        next
    }
    { print }
    ' "$source_file" > "$target_file"
}

# Copy files with optional .mdc rename (used for agents and simple copies)
copy_files() {
    local source_dir=$1
    local target_dir=$2
    local rename_extension=$3
    local file_type=$4

    if [ ! -d "$source_dir" ]; then
        print_status $RED "❌ Source directory not found: $source_dir"
        return 1
    fi

    local copied_count=0

    for file in "$source_dir"/*.md; do
        if [ ! -f "$file" ]; then
            continue
        fi

        local filename=$(basename "$file")
        local target_filename="$filename"

        if [ "$rename_extension" = true ]; then
            target_filename="${filename%.md}.mdc"
        fi

        cp "$file" "$target_dir/$target_filename"
        print_status $GREEN "✅ Copied $filename → $target_filename"
        copied_count=$((copied_count + 1))
    done

    print_status $BLUE "📋 Copied $copied_count $file_type files"
}

# Copy skill directories (preserving name/SKILL.md structure)
copy_skills() {
    local source_dir=$1
    local target_dir=$2

    if [ ! -d "$source_dir" ]; then
        return 0
    fi

    local copied_count=0

    for skill_dir in "$source_dir"/*/; do
        if [ ! -d "$skill_dir" ]; then
            continue
        fi

        local skill_name=$(basename "$skill_dir")
        local target_skill_dir="$target_dir/$skill_name"

        # Skip empty skill dirs explicitly: an unmatched glob would reach cp,
        # fail, and abort the whole script under set -e.
        if ! compgen -G "$skill_dir*" > /dev/null; then
            print_status $YELLOW "⚠️  Skipping empty skill directory: $skill_name"
            continue
        fi

        mkdir -p "$target_skill_dir"
        cp -R "$skill_dir"* "$target_skill_dir/"
        print_status $GREEN "✅ Copied skill: $skill_name"
        copied_count=$((copied_count + 1))
    done

    # The skills standard requires each skill to be a directory containing
    # SKILL.md, so the loop above only matches directories. Warn about any flat
    # .md file rather than letting it vanish from the install without a trace.
    for stray in "$source_dir"/*.md; do
        [ -f "$stray" ] || continue
        print_status $YELLOW "⚠️  Not a skill: $(basename "$stray") — skills must be a directory containing SKILL.md"
    done

    if [ $copied_count -gt 0 ]; then
        local display_dir="${target_dir#"$TARGET_ROOT"/}"
        print_status $BLUE "📋 Copied $copied_count skill(s) to $display_dir/"
    fi
}

# Copy SCOPED rules for Cursor: rename .md→.mdc and convert paths→globs in
# frontmatter. Always-on (alwaysApply) rules are skipped here — they go in
# AGENTS.md instead, since .cursor/rules/*.mdc already carries its own scoping
# via globs and doesn't need a second copy of the always-on content.
copy_cursor_rules() {
    local source_dir=$1
    local target_dir=$2

    if [ ! -d "$source_dir" ]; then
        print_status $RED "❌ Source directory not found: $source_dir"
        return 1
    fi

    local copied_count=0
    local warning_count=0

    for file in "$source_dir"/*.md; do
        if [ ! -f "$file" ]; then
            continue
        fi

        has_always_apply "$file" && continue

        local filename=$(basename "$file")

        if rule_is_excluded "$filename"; then
            print_status $YELLOW "⊘ Skipped $filename (excluded)"
            continue
        fi

        local target_filename="${filename%.md}.mdc"
        local target_path="$target_dir/$target_filename"

        # Convert paths to globs format for Cursor
        convert_paths_to_globs "$file" "$target_path"
        print_status $GREEN "✅ Converted $filename → $target_filename"

        if ! validate_frontmatter "$target_path"; then
            warning_count=$((warning_count + 1))
        fi

        copied_count=$((copied_count + 1))
    done

    print_status $BLUE "📋 Converted $copied_count scoped rule files for Cursor"
    if [ $warning_count -gt 0 ]; then
        print_status $YELLOW "⚠️  $warning_count files have frontmatter issues"
    fi
}

# Compile alwaysApply rules into CLAUDE.md format
# Only processes files with alwaysApply: true in frontmatter
compile_claude_rules() {
    local source_dir=$1
    local target_file=$2

    if [ ! -d "$source_dir" ]; then
        print_status $RED "❌ Source directory not found: $source_dir"
        return 1
    fi

    print_status $BLUE "📄 Compiling alwaysApply rules into CLAUDE.md..."

    # Compile into a scratch file, then merge into the target's managed block so
    # hand-written content in CLAUDE.md survives the run.
    local content_file="${target_file}.context-forge.content"
    cat > "$content_file" << 'EOF'
# Project Guidelines for Claude

EOF

    local compiled_count=0

    for file in "$source_dir"/*.md; do
        if [ ! -f "$file" ]; then
            continue
        fi

        # Only embed alwaysApply files in CLAUDE.md
        if ! has_always_apply "$file"; then
            continue
        fi

        local filename=$(basename "$file")

        # Extract the main heading from the file to use as section title
        local section_title=$(awk '
        BEGIN { past_frontmatter = 0; in_frontmatter = 0; in_code = 0 }
        /^---$/ {
            if (!past_frontmatter) {
                in_frontmatter = !in_frontmatter
                if (!in_frontmatter) past_frontmatter = 1
                next
            }
        }
        in_frontmatter { next }
        /^```/ { in_code = !in_code; next }
        in_code { next }
        past_frontmatter && /^#{1,6} / {
            sub(/^#{1,6} /, "")
            print
            exit
        }
        ' "$file")

        # Add section heading
        if [ -n "$section_title" ]; then
            echo "## $section_title" >> "$content_file"
        else
            local fallback_title=$(echo "$filename" | sed 's/\.md$//' | sed 's/\b\w/\U&/g' | sed 's/-/ /g')
            echo "## $fallback_title Rules" >> "$content_file"
        fi

        # Extract content after frontmatter and heading, promoting heading levels
        awk '
        BEGIN { in_frontmatter = 0; past_frontmatter = 0; past_heading = 0; in_code = 0 }
        /^---$/ {
            if (!past_frontmatter) {
                in_frontmatter = !in_frontmatter
                if (!in_frontmatter) past_frontmatter = 1
                next
            }
        }
        in_frontmatter { next }
        /^```/ { in_code = !in_code }
        in_code { print; next }
        past_frontmatter && !past_heading && /^#{1,6} / { past_heading = 1; next }
        past_frontmatter && past_heading {
            if (/^#{2,6} /) {
                sub(/^#/, "")
            }
            print
        }
        ' "$file" >> "$content_file"

        echo "" >> "$content_file"
        print_status $GREEN "✅ Embedded $(basename "$filename" .md) rules in CLAUDE.md"
        compiled_count=$((compiled_count + 1))
    done

    if ! write_managed_section "$target_file" "$content_file" "CLAUDE.md"; then
        rm -f "$content_file"
        return 1
    fi
    rm -f "$content_file"

    print_status $BLUE "📋 Embedded $compiled_count alwaysApply rules in CLAUDE.md"
    return 0
}

# Strip name field from frontmatter (not supported by Claude or Cursor rules)
strip_name_field() {
    local source_file=$1
    local target_file=$2

    awk '
    BEGIN { in_frontmatter = 0; past_frontmatter = 0 }
    /^---$/ {
        if (!past_frontmatter) {
            in_frontmatter = !in_frontmatter
            if (!in_frontmatter) past_frontmatter = 1
        }
        print
        next
    }
    in_frontmatter && /^name:/ { next }
    { print }
    ' "$source_file" > "$target_file"
}

# Copy non-alwaysApply rules to .claude/rules/ as modular rule files
copy_claude_modular_rules() {
    local source_dir=$1
    local target_dir=$2

    if [ ! -d "$source_dir" ]; then
        return 1
    fi

    mkdir -p "$target_dir"

    local copied_count=0

    for file in "$source_dir"/*.md; do
        if [ ! -f "$file" ]; then
            continue
        fi

        # Skip alwaysApply files (they go in CLAUDE.md)
        if has_always_apply "$file"; then
            continue
        fi

        local filename=$(basename "$file")

        if rule_is_excluded "$filename"; then
            print_status $YELLOW "⊘ Skipped $filename (excluded)"
            continue
        fi

        strip_name_field "$file" "$target_dir/$filename"
        print_status $GREEN "✅ Copied $filename → .claude/rules/$filename"
        copied_count=$((copied_count + 1))
    done

    print_status $BLUE "📋 Copied $copied_count modular rules to .claude/rules/"
}

# ─── Copilot helpers ──────────────────────────────────────────────────────────

# Convert a rules file's paths: YAML list to a comma-separated applyTo string.
# Prints the result; defaults to "**" when paths is absent.
get_apply_to() {
    local file=$1
    awk '
    BEGIN { in_fm = 0; in_paths = 0; path_count = 0 }
    /^---$/ {
        if (!in_fm) { in_fm = 1; next }
        else {
            # End of frontmatter — emit result
            if (path_count > 0) {
                result = ""
                for (i = 1; i <= path_count; i++) {
                    if (i > 1) result = result ","
                    result = result paths[i]
                }
                print result
            } else {
                print "**"
            }
            exit
        }
    }
    in_fm && /^paths:[ ]*$/ { in_paths = 1; next }
    in_fm && in_paths && /^[ ]+-[ ]+/ {
        val = $0
        sub(/^[ ]+-[ ]+/, "", val)
        gsub(/^"/, "", val); gsub(/"$/, "", val)
        paths[++path_count] = val
        next
    }
    in_fm && in_paths && !/^[ ]+-/ { in_paths = 0 }
    END {
        # File ended without closing frontmatter (unusual) — still emit default
        if (path_count == 0) print "**"
    }
    ' "$file"
}

# Extract a single frontmatter field value (e.g. "name" or "description").
# Prints the value; empty string if absent.
get_frontmatter_field() {
    local file=$1
    local field=$2
    awk -v field="$field" '
    BEGIN { in_frontmatter = 0 }
    /^---$/ {
        if (!in_frontmatter) { in_frontmatter = 1; next }
        else { exit }
    }
    in_frontmatter {
        pattern = "^" field ":[ ]*"
        if ($0 ~ pattern) {
            val = $0
            sub(pattern, "", val)
            gsub(/^"/, "", val); gsub(/"$/, "", val)
            print val
            exit
        }
    }
    ' "$file"
}

# Compile alwaysApply rules into a single always-on instructions file.
# Vendor-neutral: used for both .github/copilot-instructions.md and AGENTS.md.
# Args: source_dir, out_file, artifact_label, [h1_title], [trailing_content]
# Emits an "# <h1_title>" heading only when h1_title is non-empty.
# trailing_content is appended inside the managed block, after the compiled
# rules — callers pass generated sections here rather than appending them to the
# file afterwards, which would strand them outside the block.
compile_always_on_rules() {
    local source_dir=$1
    local out_file=$2
    local artifact_label=$3
    local h1_title=$4
    local trailing_content=$5

    if [ ! -d "$source_dir" ]; then
        print_status $RED "❌ Source directory not found: $source_dir"
        return 1
    fi

    mkdir -p "$(dirname "$out_file")"

    # Compile into a scratch file, then merge it into the managed block of the
    # real target. The H1 lives inside the block: it is generated content, and a
    # project that wants its own heading can put one above the block.
    local out_file_content="${out_file}.context-forge.content"
    : > "$out_file_content"

    if [ -n "$h1_title" ]; then
        {
            echo "# $h1_title"
            echo ""
        } >> "$out_file_content"
    fi

    local compiled_count=0

    for file in "$source_dir"/*.md; do
        [ -f "$file" ] || continue
        has_always_apply "$file" || continue

        local filename
        filename=$(basename "$file")

        # Extract section title from first heading
        local section_title
        section_title=$(awk '
        BEGIN { past_fm = 0; in_fm = 0; in_code = 0 }
        /^---$/ { if (!past_fm) { in_fm = !in_fm; if (!in_fm) past_fm = 1 } next }
        in_fm { next }
        /^```/ { in_code = !in_code; next }
        in_code { next }
        past_fm && /^#{1,6} / { sub(/^#{1,6} /, ""); print; exit }
        ' "$file")

        if [ -n "$section_title" ]; then
            echo "## $section_title" >> "$out_file_content"
        else
            local fallback_title
            fallback_title=$(echo "$filename" | sed 's/\.md$//' | sed 's/-/ /g')
            echo "## $fallback_title" >> "$out_file_content"
        fi

        # Append content (strip frontmatter and first heading, promote heading levels)
        awk '
        BEGIN { in_fm = 0; past_fm = 0; past_h = 0; in_code = 0 }
        /^---$/ {
            if (!past_fm) { in_fm = !in_fm; if (!in_fm) past_fm = 1 }
            next
        }
        in_fm { next }
        /^```/ { in_code = !in_code }
        in_code { print; next }
        past_fm && !past_h && /^#{1,6} / { past_h = 1; next }
        past_fm && past_h {
            if (/^#{2,6} /) sub(/^#/, "")
            print
        }
        ' "$file" >> "$out_file_content"

        echo "" >> "$out_file_content"
        print_status $GREEN "✅ Embedded $(basename "$filename" .md) in $artifact_label"
        compiled_count=$((compiled_count + 1))
    done

    if [ -n "$trailing_content" ]; then
        printf '%s' "$trailing_content" >> "$out_file_content"
    fi

    if ! write_managed_section "$out_file" "$out_file_content" "$artifact_label"; then
        rm -f "$out_file_content"
        return 1
    fi
    rm -f "$out_file_content"

    print_status $BLUE "📋 Compiled $compiled_count alwaysApply rules → $artifact_label"
}

# ─── Managed-section merge ────────────────────────────────────────────────────

# Classify a target file's managed-section state. Prints one of:
#   absent   — no file
#   unmanaged — a file we have never written to
#   legacy   — carries the pre-merge single marker
#   managed  — carries a well-formed BEGIN/END pair
#   broken   — markers present but unusable (unpaired, out of order, duplicated)
managed_state() {
    local file=$1

    [ -f "$file" ] || { echo "absent"; return 0; }

    local begin_count end_count
    begin_count=$(grep -cFx "$MANAGED_BEGIN" "$file" || true)
    end_count=$(grep -cFx "$MANAGED_END" "$file" || true)

    if [ "$begin_count" -eq 0 ] && [ "$end_count" -eq 0 ]; then
        if grep -qFx "$MANAGED_MARKER" "$file"; then
            echo "legacy"
        else
            echo "unmanaged"
        fi
        return 0
    fi

    # Exactly one of each, in the right order, is the only usable shape.
    if [ "$begin_count" -ne 1 ] || [ "$end_count" -ne 1 ]; then
        echo "broken"
        return 0
    fi

    local begin_line end_line
    begin_line=$(grep -nFx "$MANAGED_BEGIN" "$file" | head -1 | cut -d: -f1)
    end_line=$(grep -nFx "$MANAGED_END" "$file" | head -1 | cut -d: -f1)

    if [ "$begin_line" -ge "$end_line" ]; then
        echo "broken"
    else
        echo "managed"
    fi
}

# Take a one-time backup before we first touch a file we did not write.
# Skipped when a backup already exists, so the true original always survives —
# a second run must not overwrite the first run's copy.
backup_premanaged_file() {
    local file=$1
    local backup="${file}${PREMANAGED_BACKUP_SUFFIX}"

    [ -f "$backup" ] && return 0

    cp "$file" "$backup"
    print_status $YELLOW "💾 Backed up existing $(basename "$file") → $(basename "$backup")"
}

# Merge compiled content into a target file, preserving anything outside the
# managed block. Args: target_file, content_file, artifact_label
#
# Bytes outside the managed block are passed through untouched — line endings
# and a missing trailing newline included — so this stays safe on files the
# project also edits by hand.
write_managed_section() {
    local target_file=$1
    local content_file=$2
    local artifact_label=$3

    local state
    state=$(managed_state "$target_file")

    if [ "$state" = "broken" ]; then
        print_status $RED "❌ $artifact_label has unbalanced context-forge markers"
        print_status $YELLOW "   Expected exactly one $MANAGED_BEGIN and one $MANAGED_END, in that order."
        print_status $YELLOW "   Left unchanged — fix the markers by hand, then re-run."
        return 1
    fi

    mkdir -p "$(dirname "$target_file")"

    local tmp_file="${target_file}.context-forge.tmp"

    case "$state" in
        absent)
            {
                printf '%s\n' "$MANAGED_BEGIN"
                cat "$content_file"
                printf '%s\n' "$MANAGED_END"
            } > "$tmp_file"
            ;;

        unmanaged)
            # Never written by us: the whole file is the project's. Keep it and
            # append our block, rather than destroying hand-written guidance.
            backup_premanaged_file "$target_file"
            {
                cat "$target_file"
                # Guarantee separation even if the file lacks a trailing newline.
                printf '\n'
                printf '%s\n' "$MANAGED_BEGIN"
                cat "$content_file"
                printf '%s\n' "$MANAGED_END"
            } > "$tmp_file"
            print_status $YELLOW "📎 Preserved existing $artifact_label; appended managed block"
            ;;

        legacy)
            # Generated content ran from the marker to EOF, so everything above
            # the marker is the project's and everything below it is ours.
            #
            # The H1 above the marker was also ours (we emitted it), and the
            # compiled content carries its own copy — so drop a leading heading
            # that the new block is about to restate, rather than leaving the
            # title duplicated.
            backup_premanaged_file "$target_file"
            local generated_h1=""
            generated_h1=$(head -1 "$content_file")
            case "$generated_h1" in
                '# '*) ;;            # a heading the block will re-emit
                *) generated_h1="" ;; # anything else: leave the file's first line alone
            esac
            {
                awk -v marker="$MANAGED_MARKER" -v h1="$generated_h1" '
                    $0 == marker { exit }
                    # Only the first line, and only when it is exactly the
                    # heading the managed block re-emits.
                    NR == 1 && h1 != "" && $0 == h1 { skipped_h1 = 1; next }
                    # Swallow the blank line that followed that heading.
                    NR == 2 && skipped_h1 && $0 == "" { next }
                    { print }
                ' "$target_file"
                printf '%s\n' "$MANAGED_BEGIN"
                cat "$content_file"
                printf '%s\n' "$MANAGED_END"
            } > "$tmp_file"
            print_status $YELLOW "🔄 Migrated $artifact_label to BEGIN/END markers"
            print_status $YELLOW "   Content below the old marker was generated and has been replaced."
            ;;

        managed)
            awk -v begin="$MANAGED_BEGIN" -v end="$MANAGED_END" -v content="$content_file" '
                $0 == begin {
                    print
                    while ((getline line < content) > 0) print line
                    close(content)
                    in_block = 1
                    next
                }
                $0 == end { in_block = 0; print; next }
                !in_block { print }
            ' "$target_file" > "$tmp_file"
            ;;
    esac

    mv "$tmp_file" "$target_file"
    return 0
}

# Path to the rules directory, relative to the project root when it lives under
# it (the normal submodule layout); absolute otherwise. Used for AGENTS.md links.
rules_dir_display_path() {
    local abs
    abs="$(cd "$RULES_SOURCE_DIR" && pwd)"
    printf '%s\n' "${abs#"$TARGET_ROOT"/}"
}

# Emit AGENTS.md: always-on rules inline, plus — when include_scoped_index is
# "true" — an index of the scoped rules.
#
# The index exists because there is no applyTo/paths mechanism in the AGENTS.md
# format to scope rules back down, so inlining them would put Python rules in
# front of a React project; they are instead listed with the paths they already
# occupy, for the agent to read on demand. That reasoning only holds for a
# target with no scoped-rule surface of its own. copilot (.github/instructions/
# *.instructions.md, applyTo) and cursor (.cursor/rules/*.mdc, globs) already
# deliver scoped rules with working scoping, so they pass include_scoped_index
# false — a third, weaker copy pointing at source paths would add nothing.
#
# Args: source_dir, target_root, include_scoped_index ("true" or "false")
emit_agents_md() {
    local source_dir=$1
    local target_root=$2
    local include_scoped_index=$3

    local out_file="$target_root/AGENTS.md"

    if [ "$include_scoped_index" != "true" ]; then
        compile_always_on_rules "$source_dir" "$out_file" "AGENTS.md" "Project Guidelines" || return 1
        return 0
    fi

    # Build the index before compiling: it is generated content, so it has to go
    # inside the managed block along with everything else. Appending it after the
    # merge would strand it below the END marker, where the next run would not
    # replace it and duplicate copies would accumulate.
    local rules_path
    rules_path=$(rules_dir_display_path)

    local scoped_count=0
    local index_body=""

    for file in "$source_dir"/*.md; do
        [ -f "$file" ] || continue
        has_always_apply "$file" && continue

        local filename
        filename=$(basename "$file")

        # An excluded rule is not installed, so listing it here would point the
        # agent at a file the project deliberately left out.
        rule_is_excluded "$filename" && continue

        local description
        description=$(get_frontmatter_field "$file" "description")

        local apply_to
        apply_to=$(get_apply_to "$file")

        index_body+="- \`$rules_path/$filename\`"
        [ -n "$description" ] && index_body+=" — $description"
        index_body+=$'\n'
        index_body+="  Applies to: \`$apply_to\`"$'\n'

        scoped_count=$((scoped_count + 1))
    done

    local trailing=""
    if [ $scoped_count -gt 0 ]; then
        trailing+="## Additional Rules"$'\n\n'
        trailing+="The rules above always apply. The following are scoped to specific"$'\n'
        trailing+="languages and tools and are deliberately not inlined here — read the"$'\n'
        trailing+="relevant file when working in that area:"$'\n\n'
        trailing+="$index_body"
    fi

    compile_always_on_rules "$source_dir" "$out_file" "AGENTS.md" "Project Guidelines" "$trailing" || return 1

    if [ $scoped_count -gt 0 ]; then
        print_status $GREEN "✅ Indexed $scoped_count scoped rules in AGENTS.md"
    fi
}

# Run AGENTS.md-only setup (OpenAI Codex and other AGENTS.md consumers).
setup_agents() {
    print_status $BLUE "🚀 Setting up AGENTS.md..."
    echo ""

    if [ ! -d "$RULES_SOURCE_DIR" ]; then
        print_status $RED "❌ Error: Rules source directory not found: $RULES_SOURCE_DIR"
        print_status $YELLOW "💡 Make sure you're running this from a project that includes ai-project-guide"
        exit 1
    fi

    # true: the agents target emits no scoped-rule files of its own, so the
    # scoped index in AGENTS.md is the only place those rules are discoverable.
    emit_agents_md "$RULES_SOURCE_DIR" "$TARGET_ROOT" true
    echo ""

    # Copy skills if directory exists — the source layout (<name>/SKILL.md)
    # matches the destination, so copy_skills needs no translation here.
    if [ -d "$SKILLS_SOURCE_DIR" ]; then
        local agents_skills_dir="$TARGET_ROOT/.agents/skills"
        mkdir -p "$agents_skills_dir"
        print_status $BLUE "⚡ Copying skill files..."
        copy_skills "$SKILLS_SOURCE_DIR" "$agents_skills_dir"
        echo ""
    fi

    print_status $GREEN "✅ Setup complete for AGENTS.md!"
    echo ""
    print_status $BLUE "💡 AGENTS.md setup notes:"
    echo "   • AGENTS.md contains always-on rules, read by Codex and other AGENTS.md tools"
    echo "   • Scoped language/tool rules are indexed by path, not inlined"
    if [ -d "$SKILLS_SOURCE_DIR" ]; then
        echo "   • Skills copied to .agents/skills/"
    else
        echo "   • No vendor-specific files were created"
    fi
}

# Emit scoped (non-alwaysApply) rules as .github/instructions/*.instructions.md.
emit_copilot_instruction_files() {
    local source_dir=$1
    local target_root=$2

    if [ ! -d "$source_dir" ]; then
        return 0
    fi

    local target_dir="$target_root/.github/instructions"
    mkdir -p "$target_dir"

    local copied_count=0

    for file in "$source_dir"/*.md; do
        [ -f "$file" ] || continue
        has_always_apply "$file" && continue   # alwaysApply goes to copilot-instructions.md

        local filename
        filename=$(basename "$file")

        if rule_is_excluded "$filename"; then
            print_status $YELLOW "⊘ Skipped $filename (excluded)"
            continue
        fi

        local stem="${filename%.md}"

        local name
        name=$(get_frontmatter_field "$file" "name")
        [ -z "$name" ] && name="$stem"

        local description
        description=$(get_frontmatter_field "$file" "description")

        local apply_to
        apply_to=$(get_apply_to "$file")

        local out_file="$target_dir/${stem}.instructions.md"

        # Write translated frontmatter + managed marker + body
        {
            echo "---"
            echo "name: $name"
            [ -n "$description" ] && echo "description: $description"
            echo "applyTo: \"$apply_to\""
            echo "---"
            echo "$GENERATED_MARKER"
            echo ""
            # Extract body after frontmatter
            awk '
            BEGIN { in_fm = 0; past_fm = 0 }
            /^---$/ {
                if (!past_fm) { in_fm = !in_fm; if (!in_fm) past_fm = 1 }
                next
            }
            in_fm { next }
            past_fm { print }
            ' "$file"
        } > "$out_file"

        print_status $GREEN "✅ $filename → .github/instructions/${stem}.instructions.md (applyTo: $apply_to)"
        copied_count=$((copied_count + 1))
    done

    print_status $BLUE "📋 Emitted $copied_count scoped rule files to .github/instructions/"
}

# Emit skill files as .github/prompts/*.prompt.md.
emit_copilot_prompt_files() {
    local source_dir=$1
    local target_root=$2

    if [ ! -d "$source_dir" ]; then
        return 0
    fi

    local target_dir="$target_root/.github/prompts"
    mkdir -p "$target_dir"

    local copied_count=0

    # Skills may be flat .md files or skill_name/SKILL.md directories
    for entry in "$source_dir"/*; do
        local skill_file=""

        if [ -f "$entry" ] && [[ "$entry" == *.md ]]; then
            skill_file="$entry"
        elif [ -d "$entry" ]; then
            # Find first .md file inside (e.g. SKILL.md or skill-name.md)
            local first_md
            first_md=$(find "$entry" -maxdepth 1 -name "*.md" | head -1)
            [ -n "$first_md" ] && skill_file="$first_md"
        fi

        [ -n "$skill_file" ] || continue

        local filename
        filename=$(basename "$skill_file")
        local stem="${filename%.md}"
        # Use directory name as stem for nested skills
        if [ -d "$entry" ]; then
            stem=$(basename "$entry")
        fi

        local name
        name=$(get_frontmatter_field "$skill_file" "name")
        [ -z "$name" ] && name="$stem"

        local description
        description=$(get_frontmatter_field "$skill_file" "description")

        local out_file="$target_dir/${stem}.prompt.md"

        {
            echo "---"
            echo "name: $name"
            [ -n "$description" ] && echo "description: $description"
            echo "---"
            echo "$GENERATED_MARKER"
            echo ""
            awk '
            BEGIN { in_fm = 0; past_fm = 0 }
            /^---$/ {
                if (!past_fm) { in_fm = !in_fm; if (!in_fm) past_fm = 1 }
                next
            }
            in_fm { next }
            past_fm { print }
            ' "$skill_file"
        } > "$out_file"

        print_status $GREEN "✅ $stem → .github/prompts/${stem}.prompt.md"
        copied_count=$((copied_count + 1))
    done

    if [ $copied_count -gt 0 ]; then
        print_status $BLUE "📋 Emitted $copied_count skill(s) to .github/prompts/"
    fi
}

# Run full Copilot IDE setup.
setup_copilot() {
    print_status $BLUE "🚀 Setting up VS Code Copilot rules..."
    echo ""

    if [ ! -d "$RULES_SOURCE_DIR" ]; then
        print_status $RED "❌ Error: Rules source directory not found: $RULES_SOURCE_DIR"
        print_status $YELLOW "💡 Make sure you're running this from a project that includes ai-project-guide"
        exit 1
    fi

    # Always-on rules → .github/copilot-instructions.md
    compile_always_on_rules "$RULES_SOURCE_DIR" \
        "$TARGET_ROOT/.github/copilot-instructions.md" "copilot-instructions.md" ""
    echo ""

    # AGENTS.md for cross-tool compatibility (same artifact the agents target emits).
    # false: .github/instructions/*.instructions.md already carry applyTo, so the
    # scoped index in AGENTS.md would be a third, weaker copy pointing at source paths.
    emit_agents_md "$RULES_SOURCE_DIR" "$TARGET_ROOT" false
    echo ""

    # Scoped rules → .github/instructions/
    print_status $BLUE "📄 Emitting scoped instruction files..."
    emit_copilot_instruction_files "$RULES_SOURCE_DIR" "$TARGET_ROOT"
    echo ""

    # Skills → .github/prompts/
    if [ -d "$SKILLS_SOURCE_DIR" ]; then
        print_status $BLUE "⚡ Emitting prompt files from skills..."
        emit_copilot_prompt_files "$SKILLS_SOURCE_DIR" "$TARGET_ROOT"
        echo ""
    fi

    print_status $GREEN "✅ Setup complete for VS Code Copilot!"
    echo ""
    print_status $BLUE "💡 Copilot setup notes:"
    echo "   • .github/copilot-instructions.md contains always-on rules (workspace-wide)"
    echo "   • AGENTS.md mirrors copilot-instructions.md for cross-tool compatibility —"
    echo "     enabling VS Code's experimental chat.useAgentsMdFile setting loads the"
    echo "     same always-on rules twice"
    echo "   • .github/instructions/ contains scoped rules (per-language/per-path)"
    if [ -d "$SKILLS_SOURCE_DIR" ]; then
        echo "   • .github/prompts/ contains skill prompt files"
    fi
}

# ─── Main ────────────────────────────────────────────────────────────────────

# Dispatch to the per-target setup. Wrapped by main(), which reports on the
# exclusion list afterwards — one call site rather than one per target exit.
run_target_setup() {
    # Check parameters
    if [ $# -eq 0 ]; then
        print_status $RED "❌ Error: IDE parameter required"
        echo ""
        show_usage
        exit 1
    fi
    
    local ide=$(echo "$1" | tr '[:upper:]' '[:lower:]')
    
    # Check if we're in the right directory
    check_directory "$1"
    
    # Normalize aliases: AGENTS.md is a vendor-neutral format, so the vendor
    # names are aliases of the format-named target rather than targets of their own.
    case "$ide" in
        openai|codex)
            ide="agents"
            ;;
    esac

    # Validate target parameter
    case "$ide" in
        cursor|claude|copilot|agents)
            ;;
        *)
            print_status $RED "❌ Error: Unsupported target '$1'"
            echo ""
            show_usage
            exit 1
            ;;
    esac

    # Handle AGENTS.md-only
    if [ "$ide" = "agents" ]; then
        setup_agents
        return 0
    fi

    # Handle Copilot
    if [ "$ide" = "copilot" ]; then
        setup_copilot
        return 0
    fi

    # Handle Claude separately
    if [ "$ide" = "claude" ]; then
        print_status $BLUE "🚀 Setting up Claude Code rules..."
        echo ""
        
        # Validate source directory exists
        if [ ! -d "$RULES_SOURCE_DIR" ]; then
            print_status $RED "❌ Error: Rules source directory not found: $RULES_SOURCE_DIR"
            print_status $YELLOW "💡 Make sure you're running this from a project that includes ai-project-guide"
            exit 1
        fi
        
        local claude_file="$TARGET_ROOT/CLAUDE.md"
        local claude_rules_dir="$TARGET_ROOT/.claude/rules"
        local claude_agents_dir="$TARGET_ROOT/.claude/agents"

        # Compile alwaysApply rules into CLAUDE.md
        if compile_claude_rules "$RULES_SOURCE_DIR" "$claude_file"; then
            echo ""

            # Copy modular rules to .claude/rules/
            print_status $BLUE "📄 Copying modular rules..."
            copy_claude_modular_rules "$RULES_SOURCE_DIR" "$claude_rules_dir"
            echo ""

            # Copy agents if directory exists
            if [ -d "$AGENTS_SOURCE_DIR" ]; then
                mkdir -p "$claude_agents_dir"
                print_status $GREEN "📁 Created directory: $claude_agents_dir"
                print_status $BLUE "🤖 Copying agent files..."
                copy_files "$AGENTS_SOURCE_DIR" "$claude_agents_dir" false "agent"
                echo ""
            fi

            # Copy skills if directory exists
            if [ -d "$SKILLS_SOURCE_DIR" ]; then
                local claude_skills_dir="$TARGET_ROOT/.claude/skills"
                mkdir -p "$claude_skills_dir"
                print_status $BLUE "⚡ Copying skill files..."
                copy_skills "$SKILLS_SOURCE_DIR" "$claude_skills_dir"
                echo ""
            fi

            print_status $GREEN "✅ Setup complete for Claude Code!"
            echo ""
            print_status $BLUE "💡 Claude Code setup notes:"
            echo "   • CLAUDE.md contains alwaysApply rules (general, git)"
            echo "   • Modular rules copied to .claude/rules/"
            if [ -d "$AGENTS_SOURCE_DIR" ]; then
                echo "   • Agents copied to .claude/agents/"
            fi
            if [ -d "$SKILLS_SOURCE_DIR" ]; then
                echo "   • Skills copied to .claude/skills/"
            fi
        else
            print_status $RED "❌ Failed to compile CLAUDE.md"
            exit 1
        fi
        
        return 0
    fi
    
    # Cursor setup
    local rules_target_dir="$TARGET_ROOT/.cursor/rules"

    print_status $BLUE "🚀 Setting up Cursor IDE rules..."
    echo ""

    # Validate source directories exist
    if [ ! -d "$RULES_SOURCE_DIR" ]; then
        print_status $RED "❌ Error: Rules source directory not found: $RULES_SOURCE_DIR"
        print_status $YELLOW "💡 Make sure you're running this from a project that includes ai-project-guide"
        exit 1
    fi

    mkdir -p "$rules_target_dir"
    print_status $GREEN "📁 Created directory: $rules_target_dir"

    # Always-on rules → AGENTS.md. false: .cursor/rules/*.mdc already carries
    # its own scoping via globs, so the scoped index would be a third, weaker copy.
    emit_agents_md "$RULES_SOURCE_DIR" "$TARGET_ROOT" false
    echo ""

    # Migration: remove .mdc files a prior run of this script wrote for rules
    # that are alwaysApply — those rules now live in AGENTS.md instead of
    # .cursor/rules/. The stem is derived from the source filename, so this only
    # ever removes exactly what a previous run wrote from those same sources;
    # it never touches a .mdc whose stem doesn't match a current always-on rule.
    for file in "$RULES_SOURCE_DIR"/*.md; do
        [ -f "$file" ] || continue
        has_always_apply "$file" || continue

        local stem
        stem="$(basename "$file" .md)"
        local superseded="$rules_target_dir/${stem}.mdc"

        if [ -f "$superseded" ]; then
            rm -f "$superseded"
            print_status $YELLOW "🗑️  Removed superseded always-on rule: .cursor/rules/${stem}.mdc"
        fi
    done

    # Scoped rules → .cursor/rules/ (paths→globs conversion)
    print_status $BLUE "📄 Converting and copying scoped rules files..."
    copy_cursor_rules "$RULES_SOURCE_DIR" "$rules_target_dir"

    echo ""

    # .cursor/agents/ is no longer written. It is not deleted: those files carry
    # no managed marker, so this script cannot distinguish its own past output
    # from user content.
    if [ -d "$TARGET_ROOT/.cursor/agents" ]; then
        print_status $YELLOW "⚠️  .cursor/agents/ is no longer managed by this script — remove it by hand if no longer needed"
    fi

    print_status $GREEN "✅ Setup complete for Cursor!"
    echo ""
    print_status $BLUE "💡 Cursor setup notes:"
    echo "   • AGENTS.md contains always-on rules (no scoped index — .cursor/rules/ has its own)"
    echo "   • Scoped rules converted to .cursor/rules/ as .mdc files (paths→globs)"
    echo "   • Restart Cursor to ensure rules are loaded"
}

main() {
    run_target_setup "$@"
    local status=$?
    warn_unmatched_exclusions
    return $status
}

# Run main function with all arguments
main "$@" 