#!/bin/bash
# Smart Auto-Push: 分析 git diff 生成描述性 commit message
# 支持 debounce 合并 + 智能 scope + 跨平台文件锁 + 任意 repo gh-api fallback
# 用法:
#   smart-autopush.sh [repo_dir] ["commit message"] [status]
#   smart-autopush.sh /path/to/repo "feat(ui): 添加暗黑模式切换" wip

set -e

REPO_DIR="${1:-$(pwd)}"
INTENT_MSG_ARG="${2:-}"
INTENT_STATUS_ARG="${3:-wip}"
AMEND_MODE=false
FORCE_SAME_MSG=false

# Parse flags from any position
for arg in "$@"; do
    case "$arg" in
        --amend) AMEND_MODE=true ;;
        --force-same-message) FORCE_SAME_MSG=true ;;
    esac
done
DEBOUNCE_FILE="$HOME/.autopush.debounce.$(echo "$REPO_DIR" | tr '/' '_')"
LOCK_DIR="$HOME/.autopush.lock.$(echo "$REPO_DIR" | tr '/' '_')"
DEBOUNCE_SEC=5

cd "$REPO_DIR" 2>/dev/null || { echo "❌ 目录不存在: $REPO_DIR"; exit 1; }

# ============ Cross-platform Lock ============
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
    echo "⏳ Another autopush is running for $REPO_DIR, skipping"
    exit 0
fi
# Cleanup lock and debounce on exit
cleanup() {
    rmdir "$LOCK_DIR" 2>/dev/null || true
    rm -f "$DEBOUNCE_FILE"
}
trap cleanup EXIT

# ============ Debounce ============
check_debounce() {
    local now
    now=$(date +%s)
    local last_run=0

    if [ -f "$DEBOUNCE_FILE" ]; then
        last_run=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
    fi

    local elapsed=$((now - last_run))
    if [ $elapsed -lt $DEBOUNCE_SEC ]; then
        echo "⏳ Debounce: ${elapsed}s/${DEBOUNCE_SEC}s，等待中..."
        sleep $((DEBOUNCE_SEC - elapsed))
    fi

    date +%s > "$DEBOUNCE_FILE"
}

# ============ Intent File Support ============
INTENT_FILE="$REPO_DIR/.autopush-intent"
USE_INTENT=false
INTENT_MSG=""
INTENT_STATUS=""

if [ -n "$INTENT_MSG_ARG" ]; then
    USE_INTENT=true
    INTENT_MSG="$INTENT_MSG_ARG"
    INTENT_STATUS=$(echo "$INTENT_STATUS_ARG" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')
elif [ -f "$INTENT_FILE" ]; then
    USE_INTENT=true
    INTENT_MSG=$(sed -n '1p' "$INTENT_FILE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    INTENT_STATUS=$(sed -n '2p' "$INTENT_FILE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')
fi

# ============ 0. 过滤 ============
check_debounce

IGNORE_PATTERNS='\.omc/state/|\.omc/sessions/|\.json\.wakatime|node_modules/|\.DS_Store|\.env$|\.cache/|session-stats\.json|usage-cache\.json|projects/.*\.jsonl|history\.jsonl|\.autopush\.lock|\.autopush\.debounce\.'

ALL_CHANGES=$(git status --porcelain | grep -Ev "$IGNORE_PATTERNS" || true)
if [ -z "$ALL_CHANGES" ]; then
    echo "⏭️  无有效改动，跳过"
    exit 0
fi

CHANGED_FILES=$(echo "$ALL_CHANGES" | grep "^.M" | awk '{print $2}' || true)
NEW_FILES=$(echo "$ALL_CHANGES" | grep "^??" | awk '{print $2}' || true)
DELETED_FILES=$(echo "$ALL_CHANGES" | grep "^.D" | awk '{print $2}' || true)
RENAMED_FILES=$(echo "$ALL_CHANGES" | grep "^.R" | awk '{print $4}' || true)

ALL_FILES="$CHANGED_FILES $NEW_FILES $DELETED_FILES $RENAMED_FILES"
ALL_FILES=$(echo "$ALL_FILES" | tr ' ' '\n' | grep -v '^$' | tr '\n' ' ' | sed 's/ $//')

# ============ Diff-aware keyword detection ============
diff_keywords() {
    local diff_output
    diff_output=$(git diff --cached 2>/dev/null || git diff 2>/dev/null || true)

    if [ -z "$diff_output" ]; then
        echo ""
        return
    fi

    local keywords=""
    if echo "$diff_output" | grep -qiE '\b(fix|bug|patch|hotfix)\b'; then
        keywords+="fix "
    fi
    if echo "$diff_output" | grep -qiE '\b(test|spec|describe|it\(|expect\()'; then
        keywords+="test "
    fi
    if echo "$diff_output" | grep -qiE '\b(refactor|extract|rename|move|clean|dead|remove|delete)\b'; then
        keywords+="refactor "
    fi
    if echo "$diff_output" | grep -qiE '\b(feat|add|implement|support|enable)\b'; then
        keywords+="feat "
    fi
    if echo "$diff_output" | grep -qiE '\b(docs|comment|readme)\b'; then
        keywords+="docs "
    fi
    if echo "$diff_output" | grep -qiE '\b(style|css|ui|layout|font|color|tailwind)\b'; then
        keywords+="style "
    fi

    echo "$keywords"
}

# ============ 1. 智能 Scope ============
infer_scope() {
    local files="$1"
    local scope=""
    local priority=0

    for f in $files; do
        case "$f" in
            # .claude specific paths (high priority)
            skills/*)       scope="skills"; priority=70 ;;
            hooks/*)        [ $priority -lt 65 ] && { scope="hooks"; priority=65; } ;;
            scripts/*)      [ $priority -lt 65 ] && { scope="scripts"; priority=65; } ;;
            rules/*)        [ $priority -lt 60 ] && { scope="rules"; priority=60; } ;;
            memory/*)       [ $priority -lt 60 ] && { scope="memory"; priority=60; } ;;
            knowledge/*)    [ $priority -lt 60 ] && { scope="knowledge"; priority=60; } ;;
            plugins/*)      [ $priority -lt 55 ] && { scope="plugins"; priority=55; } ;;
            # Astro/web paths
            cv/*|*/cv/*)    [ $priority -lt 50 ] && { scope="cv"; priority=50; } ;;
            zh/*|*/zh/*)    [ $priority -lt 45 ] && { scope="cv"; priority=45; } ;;
            en/*|*/en/*)    [ $priority -lt 45 ] && { scope="cv"; priority=45; } ;;
            components/*)   [ $priority -lt 45 ] && { scope="components"; priority=45; } ;;
            layouts/*)      [ $priority -lt 45 ] && { scope="layouts"; priority=45; } ;;
            pages/*)        [ $priority -lt 45 ] && { scope="pages"; priority=45; } ;;
            # Generic
            src/*)          [ $priority -lt 30 ] && { scope="src"; priority=30; } ;;
            public/*)       [ $priority -lt 30 ] && { scope="public"; priority=30; } ;;
            tests/*|test/*|__tests__/*) [ $priority -lt 30 ] && { scope="tests"; priority=30; } ;;
        esac
    done

    [ -z "$scope" ] && scope="misc"
    echo "$scope"
}

# ============ 2. Type ============
infer_type() {
    local changed="$1"
    local new="$2"
    local deleted="$3"
    local keywords="$4"

    # Diff keywords override heuristics
    case "$keywords" in
        *fix*)      echo "fix"; return ;;
        *test*)     echo "test"; return ;;
        *refactor*) echo "refactor"; return ;;
        *feat*)     echo "feat"; return ;;
        *docs*)     echo "docs"; return ;;
        *style*)    echo "style"; return ;;
    esac

    if [ -n "$deleted" ] && [ -z "$changed" ] && [ -z "$new" ]; then
        echo "chore"
        return
    fi

    if [ -n "$new" ]; then
        echo "$new" | grep -qE '\.(ts|tsx|js|jsx|astro|py|go|rs)$' && echo "feat" || echo "chore"
    elif echo "$changed" | grep -qE 'package\.json|\.config|pnpm-lock|yarn\.lock|package-lock'; then
        echo "chore"
    else
        echo "chore"
    fi
}

# ============ 3. Description ============
generate_description() {
    local changed="$1"
    local new="$2"
    local deleted="$3"
    local keywords="$4"
    local descriptions=()

    # Helper: classify file action
    classify_file() {
        local f="$1"
        local action="$2"
        local bname ext
        bname=$(basename "$f")
        ext="${bname##*.}"
        case "$ext" in
            astro)      echo "$action $bname" ;;
            ts|tsx|js|jsx|py|go|rs)
                        echo "$action $bname" ;;
            css|scss)   echo "$action styles" ;;
            md|mdx)     echo "$action docs" ;;
            sh)         echo "$action scripts" ;;
            json)       echo "$action config" ;;
            yaml|yml)   echo "$action config" ;;
            *)          echo "$action $bname" ;;
        esac
    }

    # Handle new files
    for f in $new; do
        [ -z "$f" ] && continue
        descriptions+=("$(classify_file "$f" "add")")
    done

    # Handle deleted files
    for f in $deleted; do
        [ -z "$f" ] && continue
        descriptions+=("$(classify_file "$f" "remove")")
    done

    # Handle changed files
    for f in $changed; do
        [ -z "$f" ] && continue
        descriptions+=("$(classify_file "$f" "update")")
    done

    # Keyword-based overrides for clearer messages
    case "$keywords" in
        *fix*)      descriptions=("fix issues") ;;
        *refactor*) descriptions=("refactor code") ;;
        *test*)     descriptions=("update tests") ;;
        *docs*)     descriptions=("update docs") ;;
        *style*)    descriptions=("update styles") ;;
    esac

    printf '%s\n' "${descriptions[@]}" 2>/dev/null | sort -u | head -3 | tr '\n' ' ' | sed 's/ $//'
}

# Check if message is low-quality (generic filename-only)
is_low_quality_msg() {
    local msg="$1"
    # Patterns like "update filename", "add filename", "remove filename", "update config/styles"
    if echo "$msg" | grep -qE '^(update|add|remove) [A-Za-z0-9_./-]+( (update|add|remove) [A-Za-z0-9_./-]+)*$'; then
        echo "true"
    elif echo "$msg" | grep -qE '^(update (config|styles|scripts|docs))$'; then
        echo "true"
    else
        echo "false"
    fi
}

# ============ Amend Safety Check ============
# When amending, verify the commit message still describes the actual changes
# Also generates a semantic suggestion based on actual diff content
check_amend_message_match() {
    local msg="$1"
    local force="$2"

    # Get previous commit message
    local prev_msg
    prev_msg=$(git log -1 --format=%s 2>/dev/null || echo "")

    # If message is identical to previous, warn
    if [ "$msg" = "$prev_msg" ]; then
        # Check if there are new changes beyond the previous commit
        local prev_tree current_tree
        prev_tree=$(git rev-parse HEAD^{tree} 2>/dev/null || echo "")
        current_tree=$(git write-tree 2>/dev/null || echo "")

        if [ "$prev_tree" != "$current_tree" ]; then
            # Generate semantic suggestion from actual diff
            local suggestion
            suggestion=$(generate_amend_suggestion)

            echo ""
            echo "🚨 AMEND SAFETY CHECK FAILED"
            echo "   You are amending with the SAME message as the previous commit."
            echo "   But the tree has changed — new files or modifications were added."
            echo ""
            echo "   Previous message: $prev_msg"
            echo ""
            echo "   💡 Suggested message based on actual changes:"
            echo "      $suggestion"
            echo ""
            echo "   Options:"
            echo "     A. Use suggested message: ap-intent '$suggestion'"
            echo "     B. Write your own:        ap-intent 'feat(x): your description'"
            echo "     C. Force anyway:          smart-autopush.sh . \"$msg\" --amend --force-same-message"
            echo ""

            if [ "$force" != "true" ]; then
                return 1
            else
                echo "   ⚠️  --force-same-message passed, proceeding despite mismatch..."
            fi
        fi
    fi

    return 0
}

# Generate a semantic commit suggestion from the actual diff content
# This analyzes the current staged+unstaged changes (the delta since last commit)
generate_amend_suggestion() {
    # Get the delta: diff between HEAD and current working tree
    local diff_output
    diff_output=$(git diff HEAD 2>/dev/null || true)

    if [ -z "$diff_output" ]; then
        echo "chore(misc): update"
        return
    fi

    # Analyze diff for semantic keywords
    local keywords=""
    if echo "$diff_output" | grep -qiE '\b(fix|bug|patch|hotfix|error|crash|fail)\b'; then
        keywords+="fix "
    fi
    if echo "$diff_output" | grep -qiE '\b(feat|add|implement|support|enable|new)\b'; then
        keywords+="feat "
    fi
    if echo "$diff_output" | grep -qiE '\b(refactor|extract|rename|move|clean|dead|remove|delete|simplify)\b'; then
        keywords+="refactor "
    fi
    if echo "$diff_output" | grep -qiE '\b(test|spec|describe|it\(|expect\()'; then
        keywords+="test "
    fi
    if echo "$diff_output" | grep -qiE '\b(docs|comment|readme|guide)\b'; then
        keywords+="docs "
    fi
    if echo "$diff_output" | grep -qiE '\b(style|css|ui|layout|font|color|tailwind|design)\b'; then
        keywords+="style "
    fi
    if echo "$diff_output" | grep -qiE '\b(perf|optimize|speed|fast|slow|cache|memory|lazy)\b'; then
        keywords+="perf "
    fi

    # Determine type from keywords
    local type="chore"
    case "$keywords" in
        *fix*)      type="fix" ;;
        *feat*)     type="feat" ;;
        *refactor*) type="refactor" ;;
        *test*)     type="test" ;;
        *docs*)     type="docs" ;;
        *style*)    type="style" ;;
        *perf*)     type="perf" ;;
    esac

    # Analyze which files changed (the delta, not the whole commit)
    local delta_files
    delta_files=$(git diff --name-only HEAD 2>/dev/null || true)

    local scope="misc"
    local file_count=0
    for f in $delta_files; do
        file_count=$((file_count + 1))
        case "$f" in
            src/components/*)    scope="components" ;;
            src/layouts/*)       scope="layouts" ;;
            src/pages/*)         scope="pages" ;;
            src/script/*|src/scripts/*) scope="scripts" ;;
            src/styles/*)        scope="styles" ;;
            public/*)            scope="assets" ;;
            scripts/*)           scope="scripts" ;;
            *.config.*|package.json) scope="config" ;;
        esac
    done

    # Generate description from diff content
    local description=""

    # Check for specific patterns
    if echo "$diff_output" | grep -qiE '\blocalStorage\b.*\bgetItem\b'; then
        description="add localStorage persistence"
    elif echo "$diff_output" | grep -qiE '\blocalStorage\b.*\bsetItem\b'; then
        description="persist state to localStorage"
    elif echo "$diff_output" | grep -qiE '\bfetch\b.*\bjson\b|\bmanifest\b'; then
        description="add dynamic manifest fetching"
    elif echo "$diff_output" | grep -qiE '\bdefault\b.*\bfavicon\b|\bfavicon.*default\b'; then
        description="set default favicon"
    elif echo "$diff_output" | grep -qiE '\brandom\b.*\bMath\b|\bMath\.random\b'; then
        description="add random selection"
    elif echo "$diff_output" | grep -qiE '\bfilter\b|\bexclude\b|\bskip\b|\ballowed\b'; then
        description="filter available options"
    elif echo "$diff_output" | grep -qiE '\bgitSha\b|\bcdnBase\b|\bicon-manifest\b'; then
        description="update manifest generation"
    elif [ "$file_count" -eq 1 ]; then
        description="update $(basename "$delta_files")"
    else
        description="update $scope"
    fi

    # If there are added lines that look like new features
    local added_lines
    added_lines=$(echo "$diff_output" | grep -c '^+' || echo "0")
    local removed_lines
    removed_lines=$(echo "$diff_output" | grep -c '^-' || echo "0")

    if [ "$added_lines" -gt "$removed_lines" ] && [ "$added_lines" -gt 5 ]; then
        case "$type" in
            fix) description="${description} and improve handling" ;;
            feat) description="${description}" ;;
            *) description="${description}" ;;
        esac
    fi

    echo "${type}(${scope}): ${description}"
}

# ============ 4. 组装 ============
if [ "$USE_INTENT" = true ] && [ -n "$INTENT_MSG" ]; then
    case "$INTENT_STATUS" in
        done|已实现|完成)
            INTENT_STATUS="done" ;;
        wip|进行中|未实现|未完成|todo)
            INTENT_STATUS="wip" ;;
        *)
            INTENT_STATUS="" ;;
    esac

    if [ -n "$INTENT_STATUS" ]; then
        COMMIT_MSG="${INTENT_MSG} (${INTENT_STATUS})"
    else
        COMMIT_MSG="$INTENT_MSG"
    fi
else
    KEYWORDS=$(diff_keywords)
    SCOPE=$(infer_scope "$ALL_FILES")
    TYPE=$(infer_type "$CHANGED_FILES" "$NEW_FILES" "$DELETED_FILES" "$KEYWORDS")
    DESCRIPTION=$(generate_description "$CHANGED_FILES" "$NEW_FILES" "$DELETED_FILES" "$KEYWORDS")

    if [ -n "$DESCRIPTION" ]; then
        COMMIT_MSG="${TYPE}(${SCOPE}): ${DESCRIPTION}"
    else
        FIRST_FILE=$(echo "$ALL_FILES" | awk '{print $1}')
        if [ -n "$FIRST_FILE" ]; then
            COMMIT_MSG="${TYPE}(${SCOPE}): update $(basename "$FIRST_FILE")"
        else
            echo "⏭️  无有效文件"
            exit 0
        fi
    fi
fi

# Warn about low-quality auto-generated messages
LOW_QUALITY=false
if [ "$USE_INTENT" = false ]; then
    if [ "$(is_low_quality_msg "${COMMIT_MSG##*: }")" = "true" ]; then
        LOW_QUALITY=true
    fi
fi

# ============ 4.5 Agent Review Gate ============
# Count only files that will actually be committed
if git diff --cached --quiet; then
    # Nothing staged yet; commit will include all changes
    TOTAL_FILES=$(echo "$ALL_CHANGES" | awk '{print ($1=="R"?$4:$2)}' | sort -u | grep -v '^$' | grep -c .)
else
    # Files already staged; commit will only include staged files
    TOTAL_FILES=$(git diff --cached --name-only | grep -c .)
fi
SKIP_REVIEW_FILE="$REPO_DIR/.autopush-skip-review"
if [ "$TOTAL_FILES" -gt 3 ] && [ ! -f "$SKIP_REVIEW_FILE" ]; then
    echo "❌ Commit rejected: $TOTAL_FILES files will be committed."
    echo "   behavioral-execution-discipline.md requires code-reviewer for >3 files."
    echo ""
    echo "Options:"
    echo "   1. Run code-reviewer agent, then create skip marker:"
    echo "      touch \"$SKIP_REVIEW_FILE\""
    echo "      smart-autopush.sh \"$REPO_DIR\" \"...\""
    echo "   2. Break into smaller commits (<4 files each)"
    echo "   3. If reviewed already, pass --skip-review flag"
    exit 1
fi
rm -f "$SKIP_REVIEW_FILE"

# ============ 5. 执行 ============
if [ "$LOW_QUALITY" = true ]; then
    echo "❌ Commit rejected: message is too generic."
    echo "   Generated: $COMMIT_MSG"
    echo ""
    echo "You must provide intent. Choose one:"
    echo "   ap-intent 'feat(scope): 你在干什么' [done|wip]"
    echo "   smart-autopush.sh /repo \"feat(scope): description\" [done|wip]"
    exit 1
fi

echo "📝 Commit: $COMMIT_MSG"
# Only git add -A if nothing is already staged
if git diff --cached --quiet; then
    git add -A
fi

# Amend safety check
if [ "$AMEND_MODE" = true ]; then
    if ! check_amend_message_match "$COMMIT_MSG" "$FORCE_SAME_MSG"; then
        exit 1
    fi
    git commit --amend -m "$COMMIT_MSG"
else
    git commit -m "$COMMIT_MSG"
fi

if git remote get-url origin &>/dev/null; then
    GIT_PUSH_OUT=$(git push origin HEAD 2>&1)
    GIT_PUSH_CODE=$?
    echo "$GIT_PUSH_OUT" | head -3
    if [ $GIT_PUSH_CODE -eq 0 ]; then
        echo "✅ Pushed"
        if [ "$USE_INTENT" = true ]; then
            rm -f "$INTENT_FILE"
            echo "🗑️  Removed .autopush-intent"
        fi
    else
        # Determine if gh-api fallback should be used
        USE_GH_FALLBACK=false
        if [ "$REPO_DIR" = "$HOME/.claude" ]; then
            USE_GH_FALLBACK=true
        elif git config --get claude.ghApiFallback >/dev/null 2>&1; then
            USE_GH_FALLBACK=true
        elif [ -n "$GH_API_FALLBACK_REPOS" ] && echo "$GH_API_FALLBACK_REPOS" | grep -qF "$REPO_DIR"; then
            USE_GH_FALLBACK=true
        fi

        if [ "$USE_GH_FALLBACK" = true ] && gh auth status &>/dev/null; then
            echo "⚠️ git push failed, trying gh api fallback..."
            bash "$HOME/.claude/scripts/gh-api-push.sh"
            if [ "$USE_INTENT" = true ]; then
                rm -f "$INTENT_FILE"
                echo "🗑️  Removed .autopush-intent"
            fi
        else
            # TLS/SSL errors in stash proxy mode → exit 0 fast, don't retry
            if echo "$GIT_PUSH_OUT" | grep -qiE '(SSL_ERROR_SYSCALL|LibreSSL|connection reset|certificate|fatal: unable to access|GnuTLS)'; then
                echo "⚠️ TLS/SSL error (stash proxy). Push skipped silently."
                echo "   (GitHub remote may be unreachable via git; try gh CLI or verify proxy settings)"
                # Exit 0 — this is expected network condition, not a real failure
                exit 0
            fi
            echo "❌ Push failed"
            exit 1
        fi
    fi
else
    echo "✅ Committed (no remote)"
    if [ "$USE_INTENT" = true ]; then
        rm -f "$INTENT_FILE"
        echo "🗑️  Removed .autopush-intent"
    fi
fi
