#!/usr/bin/env bash

set -euo pipefail

DEV_BRANCH="dev"
MAIN_BRANCH="main"
ORIGIN_REMOTE="origin"
DEFAULT_AGENT_DEVELOPMENT_PATHS=(
    "AGENTS.md"
    "SCRATCHPAD.md"
    "guidance"
    "requirements"
    "prompts"
    "process"
    "tools/merge_dev_into_main.sh"
)
AGENT_DEVELOPMENT_PATHS=("${DEFAULT_AGENT_DEVELOPMENT_PATHS[@]}")

CURRENT_BRANCH="${DEV_BRANCH}"
ASSUME_YES=0
DRY_RUN=0
SKIP_TESTS=0

usage() {
    cat <<EOF
Usage: $(basename "$0") [--yes] [--dry-run] [--skip-tests] [--help]

Merge the local ${DEV_BRANCH} branch into ${MAIN_BRANCH}, remove agent-development files
from ${MAIN_BRANCH}, auto-resolve merge conflicts limited to those cleanup paths,
create a release merge commit and tag, push to ${ORIGIN_REMOTE}/${MAIN_BRANCH}, and
switch back to ${DEV_BRANCH}.

Options:
  --yes         Skip the interactive confirmation prompt.
  --dry-run     Print the validated release plan without changing git state.
  --skip-tests  Skip the default pre-release test gate (./tools/test.sh).
  --help        Show this help text.

Environment overrides:
  FFX_RELEASE_CLEAN_PATHS  Colon-separated path list to remove from ${MAIN_BRANCH}
                           after merging ${DEV_BRANCH}. Defaults to:
                           ${DEFAULT_AGENT_DEVELOPMENT_PATHS[*]}
EOF
}

fail() {
    printf '%s\n' "$*" >&2
    exit 1
}

cleanup() {
    local exit_code="$1"

    trap - EXIT

    if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
        printf 'Merge is incomplete; aborting merge on %s...\n' "${CURRENT_BRANCH}" >&2
        git merge --abort >/dev/null 2>&1 || true
    fi

    if [ "${CURRENT_BRANCH}" != "${DEV_BRANCH}" ]; then
        printf 'Switching back to %s...\n' "${DEV_BRANCH}" >&2
        git switch "${DEV_BRANCH}" >/dev/null 2>&1 || true
        CURRENT_BRANCH="${DEV_BRANCH}"
    fi

    exit "${exit_code}"
}

load_cleanup_paths() {
    if [ -n "${FFX_RELEASE_CLEAN_PATHS:-}" ]; then
        IFS=':' read -r -a AGENT_DEVELOPMENT_PATHS <<< "${FFX_RELEASE_CLEAN_PATHS}"
    fi

    if [ "${#AGENT_DEVELOPMENT_PATHS[@]}" -eq 0 ]; then
        fail "Release cleanup path list is empty."
    fi
}

path_is_cleanup_target() {
    local candidate_path="$1"
    local cleanup_path=""

    for cleanup_path in "${AGENT_DEVELOPMENT_PATHS[@]}"; do
        case "${candidate_path}" in
            "${cleanup_path}"|"${cleanup_path}"/*)
                return 0
                ;;
        esac
    done

    return 1
}

auto_resolve_cleanup_conflicts() {
    local unmerged_paths=()
    local non_cleanup_conflicts=()
    local remaining_conflicts=()
    local conflicted_path=""

    mapfile -t unmerged_paths < <(git diff --name-only --diff-filter=U)
    if [ "${#unmerged_paths[@]}" -eq 0 ]; then
        return 1
    fi

    for conflicted_path in "${unmerged_paths[@]}"; do
        if ! path_is_cleanup_target "${conflicted_path}"; then
            non_cleanup_conflicts+=("${conflicted_path}")
        fi
    done

    if [ "${#non_cleanup_conflicts[@]}" -ne 0 ]; then
        printf 'Merge produced non-cleanup conflicts:\n' >&2
        for conflicted_path in "${non_cleanup_conflicts[@]}"; do
            printf '  - %s\n' "${conflicted_path}" >&2
        done
        return 1
    fi

    printf 'Auto-resolving merge conflicts for release-cleanup paths:\n'
    for conflicted_path in "${unmerged_paths[@]}"; do
        printf '  - %s\n' "${conflicted_path}"
    done

    git rm -r -f --ignore-unmatch "${AGENT_DEVELOPMENT_PATHS[@]}" >/dev/null

    mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U)
    if [ "${#remaining_conflicts[@]}" -ne 0 ]; then
        printf 'Cleanup conflict auto-resolution left unresolved paths:\n' >&2
        for conflicted_path in "${remaining_conflicts[@]}"; do
            printf '  - %s\n' "${conflicted_path}" >&2
        done
        return 1
    fi

    return 0
}

require_repo_state() {
    if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
        fail "This helper must be run inside a git repository."
    fi

    if ! git show-ref --verify --quiet "refs/heads/${DEV_BRANCH}"; then
        fail "Local branch '${DEV_BRANCH}' does not exist."
    fi

    if ! git show-ref --verify --quiet "refs/heads/${MAIN_BRANCH}"; then
        fail "Local branch '${MAIN_BRANCH}' does not exist."
    fi

    if ! git remote get-url "${ORIGIN_REMOTE}" >/dev/null 2>&1; then
        fail "Remote '${ORIGIN_REMOTE}' is not configured."
    fi
}

require_dev_checkout() {
    CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
    if [ "${CURRENT_BRANCH}" != "${DEV_BRANCH}" ]; then
        fail "Current branch is '${CURRENT_BRANCH}', but '${DEV_BRANCH}' is required."
    fi
}

require_clean_worktree() {
    if [ -n "$(git status --porcelain)" ]; then
        fail "Local '${DEV_BRANCH}' branch is dirty. Commit, stash, or clean changes first."
    fi
}

fetch_remote_state() {
    printf 'Fetching %s branch and tag state...\n' "${ORIGIN_REMOTE}"
    git fetch "${ORIGIN_REMOTE}" "${DEV_BRANCH}" "${MAIN_BRANCH}" --tags >/dev/null
}

require_branch_matches_remote() {
    local branch="$1"
    local local_sha=""
    local remote_sha=""

    if ! git show-ref --verify --quiet "refs/remotes/${ORIGIN_REMOTE}/${branch}"; then
        fail "Remote branch '${ORIGIN_REMOTE}/${branch}' does not exist."
    fi

    local_sha="$(git rev-parse "refs/heads/${branch}")"
    remote_sha="$(git rev-parse "refs/remotes/${ORIGIN_REMOTE}/${branch}")"

    if [ "${local_sha}" != "${remote_sha}" ]; then
        fail "Local branch '${branch}' is not up to date with '${ORIGIN_REMOTE}/${branch}'. Pull, rebase, or push first."
    fi
}

resolve_release_version() {
    local version_from_pyproject=""
    local version_from_constants=""

    version_from_pyproject="$(
        sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -n 1
    )"
    version_from_constants="$(
        sed -n "s/^VERSION='\(.*\)'$/\1/p" src/ffx/constants.py | head -n 1
    )"

    if [ -z "${version_from_pyproject}" ]; then
        fail "Could not resolve release version from pyproject.toml."
    fi

    if [ -z "${version_from_constants}" ]; then
        fail "Could not resolve release version from src/ffx/constants.py."
    fi

    if [ "${version_from_pyproject}" != "${version_from_constants}" ]; then
        fail "Version mismatch: pyproject.toml=${version_from_pyproject}, src/ffx/constants.py=${version_from_constants}."
    fi

    printf '%s\n' "${version_from_pyproject}"
}

require_release_tag_available() {
    local release_version="$1"
    local release_tag="v${release_version}"

    if git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null 2>&1; then
        fail "Tag '${release_tag}' already exists."
    fi

    if git rev-parse -q --verify "refs/tags/${release_version}" >/dev/null 2>&1; then
        fail "Bare tag '${release_version}' already exists; refusing to create ambiguous release tags."
    fi
}

run_pre_release_tests() {
    if [ "${SKIP_TESTS}" -eq 1 ]; then
        printf 'Skipping pre-release tests.\n'
        return 0
    fi

    if [ ! -x "./tools/test.sh" ]; then
        fail "Missing executable test runner at ./tools/test.sh."
    fi

    printf 'Running pre-release tests via ./tools/test.sh...\n'
    ./tools/test.sh
}

print_release_plan() {
    local release_version="$1"
    local release_tag="v${release_version}"
    local release_commit_message="Release ${release_tag}"

    printf 'Dry run only. Planned steps:\n'
    printf '1. Ensure current branch is %s and the worktree is clean.\n' "${DEV_BRANCH}"
    printf '2. Fetch %s and verify local %s and %s exactly match %s/%s and %s/%s.\n' \
        "${ORIGIN_REMOTE}" \
        "${DEV_BRANCH}" \
        "${MAIN_BRANCH}" \
        "${ORIGIN_REMOTE}" \
        "${DEV_BRANCH}" \
        "${ORIGIN_REMOTE}" \
        "${MAIN_BRANCH}"
    if [ "${SKIP_TESTS}" -eq 1 ]; then
        printf '3. Skip the pre-release test gate.\n'
    else
        printf '3. Run ./tools/test.sh as the pre-release test gate.\n'
    fi
    printf '4. Switch to %s and merge %s with --no-ff --no-commit.\n' "${MAIN_BRANCH}" "${DEV_BRANCH}"
    printf '5. Auto-resolve merge conflicts limited to release-cleanup paths and remove them from %s:\n' "${MAIN_BRANCH}"
    local cleanup_path=""
    for cleanup_path in "${AGENT_DEVELOPMENT_PATHS[@]}"; do
        printf '   - %s\n' "${cleanup_path}"
    done
    printf '6. Create merge commit: %s\n' "${release_commit_message}"
    printf '7. Create annotated tag: %s\n' "${release_tag}"
    printf '8. Push %s to %s/%s with --follow-tags.\n' "${MAIN_BRANCH}" "${ORIGIN_REMOTE}" "${MAIN_BRANCH}"
    printf '9. Switch back to %s.\n' "${DEV_BRANCH}"
}

trap 'cleanup $?' EXIT

while [ "$#" -gt 0 ]; do
    case "$1" in
        --yes)
            ASSUME_YES=1
            ;;
        --dry-run)
            DRY_RUN=1
            ;;
        --skip-tests)
            SKIP_TESTS=1
            ;;
        --help|-h)
            usage
            exit 0
            ;;
        *)
            usage >&2
            fail "Unknown option: $1"
            ;;
    esac
    shift
done

load_cleanup_paths
require_repo_state
require_dev_checkout
require_clean_worktree
fetch_remote_state
require_branch_matches_remote "${DEV_BRANCH}"
require_branch_matches_remote "${MAIN_BRANCH}"

RELEASE_VERSION="$(resolve_release_version)"
RELEASE_TAG="v${RELEASE_VERSION}"
RELEASE_COMMIT_MESSAGE="Release ${RELEASE_TAG}"
require_release_tag_available "${RELEASE_VERSION}"

printf 'This will merge %s into %s, remove agent-development files on %s,\n' "${DEV_BRANCH}" "${MAIN_BRANCH}" "${MAIN_BRANCH}"
printf 'auto-resolve cleanup-path conflicts, run the pre-release gate%s, create %s,\n' \
    "$([ "${SKIP_TESTS}" -eq 1 ] && printf ' (skipped)' || printf '')" \
    "${RELEASE_TAG}"
printf 'push to %s/%s, and switch back to %s.\n' \
    "${ORIGIN_REMOTE}" \
    "${MAIN_BRANCH}" \
    "${DEV_BRANCH}"

if [ "${ASSUME_YES}" -ne 1 ]; then
    printf 'Are you sure? [y/N] '
    read -r confirmation
    case "${confirmation}" in
        y|Y|yes|YES)
            ;;
        *)
            fail "Aborted by user."
            ;;
    esac
fi

if [ "${DRY_RUN}" -eq 1 ]; then
    print_release_plan "${RELEASE_VERSION}"
    exit 0
fi

run_pre_release_tests
require_clean_worktree
fetch_remote_state
require_branch_matches_remote "${DEV_BRANCH}"
require_branch_matches_remote "${MAIN_BRANCH}"
require_release_tag_available "${RELEASE_VERSION}"

git switch "${MAIN_BRANCH}" >/dev/null
CURRENT_BRANCH="${MAIN_BRANCH}"

printf 'Merging %s into %s...\n' "${DEV_BRANCH}" "${MAIN_BRANCH}"
if ! git merge --no-ff --no-commit "${DEV_BRANCH}"; then
    if ! auto_resolve_cleanup_conflicts; then
        fail "Merge from '${DEV_BRANCH}' into '${MAIN_BRANCH}' failed."
    fi
fi

if ! git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
    fail "'${MAIN_BRANCH}' is already up to date with '${DEV_BRANCH}'. Nothing to merge."
fi

printf 'Removing agent-development files from %s...\n' "${MAIN_BRANCH}"
git rm -r -f --ignore-unmatch "${AGENT_DEVELOPMENT_PATHS[@]}" >/dev/null

if git diff --cached --quiet; then
    fail "No staged changes are present after merging '${DEV_BRANCH}' into '${MAIN_BRANCH}'."
fi

printf 'Creating release merge commit: %s\n' "${RELEASE_COMMIT_MESSAGE}"
git commit -m "${RELEASE_COMMIT_MESSAGE}"

printf 'Creating annotated tag: %s\n' "${RELEASE_TAG}"
git tag -a "${RELEASE_TAG}" -m "FFX ${RELEASE_VERSION}"

printf 'Pushing %s and annotated tags to %s...\n' "${MAIN_BRANCH}" "${ORIGIN_REMOTE}"
git push "${ORIGIN_REMOTE}" "${MAIN_BRANCH}" --follow-tags

printf 'Switching back to %s...\n' "${DEV_BRANCH}"
git switch "${DEV_BRANCH}" >/dev/null
CURRENT_BRANCH="${DEV_BRANCH}"

printf 'Release merge complete: %s pushed to %s/%s and tagged as %s.\n' \
    "${RELEASE_COMMIT_MESSAGE}" \
    "${ORIGIN_REMOTE}" \
    "${MAIN_BRANCH}" \
    "${RELEASE_TAG}"
