#!/bin/bash

# This script will take the last CHANGELOG.rst, pull information from the commit logs since last tagged version
# and update the content to create dedicated sections

SCRIPTS=$(dirname "$(realpath "$0")")

#shellcheck disable=SC1090
source "${SCRIPTS}/functions"

# Scopes used in this script. Keep this compatible with the Bash 3 version
# shipped by macOS, which does not support associative arrays.
SCOPES=("feat" "security" "fix" "perf" "style" "refactor" "docs" "tools")

scope_heading() {
    case "$1" in
        feat) echo "Features:" ;;
        security) echo "Security Updates:" ;;
        fix) echo "Bug Fixes:" ;;
        perf) echo "Performance Improvements:" ;;
        style) echo "Styles:" ;;
        refactor) echo "Refactoring and Cleanups:" ;;
        docs) echo "Documentation:" ;;
        tools) echo "Internal Tools:" ;;
        *) error "Unknown changelog scope: $1"; return 1 ;;
    esac
}

# Conventional commit types included in the generated changelog. Backward
# incompatible changes deliberately remain a manually maintained section.
CHANGELOG_TYPES=$(IFS='|'; echo "${SCOPES[*]}")
VERSION_FIELD_RE='^[0-9]+\.[0-9]+\.[0-9]+'

changelog_check_version() {
    if ! echo "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+[^ ]*$'; then
        error "Incorrect version format '$1', must be X.Y.Z[suffix]"
        exit 1
    fi
}

changelog_main_version() {
    echo "$1" | sed -E 's/^(([0-9]+\.){2}[0-9]+).*$/\1/'
}

changelog_version_suffix() {
    echo "$1" | sed -E 's/^([0-9]+\.){2}[0-9]+//'
}


# argument checks

if [ -z "$1" ] ; then
    echo "Usage $0 <version> [base_version]"
    echo
    echo "Update the CHANGELOG.rst to create <version>"
    echo "Version can be X.Y.Zsuffix"
    echo "suffix can be rc, or anything, X,Y,Z are numbers"
    exit 1
fi

FULL_VERSION="$1"
OLD_FULL_VERSION="$2"

# extracting version from the FULL_VERSION, and sanity checks
changelog_check_version "${FULL_VERSION}"

VERSION=$(changelog_main_version "${FULL_VERSION}")
VERSUFFIX=$(changelog_version_suffix "${FULL_VERSION}")


# extracting the base version from changelog
if [ -z "${OLD_FULL_VERSION}" ]; then
    OLD_FULL_VERSION=$(awk -v version_re="${VERSION_FIELD_RE}" '
        $1 ~ version_re && $2 ~ /^\(/ && $2 != "(unpublished)" { print $1; exit }
    ' CHANGELOG.rst)
else
    changelog_check_version "${OLD_FULL_VERSION}"
fi

# Sanity checks
if [ -z "$(git tag -l "${OLD_FULL_VERSION}")" ]; then
    error "Can not find tag for version ${OLD_FULL_VERSION} aborting ..."
    exit 1
fi

if [ "${FULL_VERSION}" = "${OLD_FULL_VERSION}" ]; then
    error "New version must be different than old version!"
    exit 1
fi

# Computing dates

if echo "${VERSUFFIX}" | grep -qi '^rc'; then
    DATE="unpublished"
else
    DATE=$(date +"%Y-%m-%d")
fi
title="${FULL_VERSION} (${DATE})"
#shellcheck disable=SC2001
line=$(echo "${title}" | sed 's/./=/g')

# When promoting a prerelease, retain its accumulated changelog content and
# split the legacy changelog at the preceding release line instead.
CUT_VERSION="${OLD_FULL_VERSION}"
if [ "$(changelog_main_version "${OLD_FULL_VERSION}")" = "${VERSION}" ]; then
    CUT_VERSION=$(awk -v version_re="${VERSION_FIELD_RE}" -v version="${VERSION}" '
        $1 ~ version_re && $2 ~ /^\(/ {
            candidate=$1
            normalized=candidate
            sub(/[^0-9.].*$/, "", normalized)
            if (normalized != version) { print candidate; exit }
        }
    ' CHANGELOG.rst)
fi
if [ -z "${CUT_VERSION}" ] || [ -z "$(git tag -l "${CUT_VERSION}")" ]; then
    error "Can not determine the previous release tag for ${FULL_VERSION}"
    exit 1
fi

# Cutting the Changelog into OLD and CURRENT
legacy="$(mktemp)"
current="$(mktemp)"

awk -v version="${CUT_VERSION}" '
    $1 == version && $2 ~ /^\(/ { show=1 }
    show { print }
' CHANGELOG.rst > "${legacy}"
awk -v version="${CUT_VERSION}" '
    $1 == version && $2 ~ /^\(/ { exit }
    /^Statistics:$/ { exit }
    { print }
' CHANGELOG.rst > "${current}"

LAST_FULL_VERSION=$(awk -v version_re="${VERSION_FIELD_RE}" '
    $1 ~ version_re && $2 ~ /^\(/ { print $1; exit }
' CHANGELOG.rst)
LAST_VERSION=$(changelog_main_version "${LAST_FULL_VERSION}")

# Statistics cover the complete release line, including all prereleases. Find
# the first preceding changelog version with a different normalized version.
RELEASE_BASE_VERSION="${CUT_VERSION}"

# Sanity checks
if [ -z "$(git tag -l "${LAST_FULL_VERSION}")" ]; then
    error "Can not find tag for version ${LAST_FULL_VERSION} aborting ..."
    exit 1
fi

# making sure the headings are right:

if [ "${VERSION}" = "${LAST_VERSION}" ]; then
    # Releasing a final version from a prerelease: replace its heading while
    # preserving the entries accumulated by earlier prereleases.
    updated=$(mktemp)
    awk -v previous="${LAST_FULL_VERSION}" -v title="${title}" -v line="${line}" '
        $1 == previous && $2 ~ /^\(/ {
            print title
            print line
            getline
            next
        }
        { print }
    ' "${current}" > "${updated}"
    mv "${updated}" "${current}"
else
    # This is the first release for a new version line. Discard a legacy
    # "unreleased" marker if present and prepend a deterministic heading.
    previous=$(mktemp)
    awk '
        /^unreleased$/ { getline; next }
        { print }
    ' "${current}" > "${previous}"
    {
        echo "${title}"
        echo "${line}"
        echo
        echo "Highlights:"
        echo "-----------"
        echo
        cat "${previous}"
    } > "${current}"
    rm "${previous}"
fi

# computing / updating each section

for scope in "${SCOPES[@]}"; do
    content=$(mktemp)
    heading=$(scope_heading "${scope}")

    # only checking on the last version *in the changelog" (ex:rc1 to rc2, the changelog already includes old to rc1)
    # Accept conventional commit scopes and breaking-change markers, for
    # example feat(admin): and fix!:, not only the unscoped "feat:" form.
    git log "${LAST_FULL_VERSION}..HEAD" --pretty="format:%s (%h) -- %aN" \
        | awk -v scope="${scope}" '
            BEGIN { pattern="^" scope "(\\([^)]*\\))?!?:[[:space:]]*" }
            tolower($0) ~ pattern {
                sub(/^[^:]*:[[:space:]]*/, "", $0)
                print "* " $0
            }
        ' > "${content}"
    if [ -z "$(cat "${content}")" ]; then
        continue
    fi

    # making sure the heading is there:
    if ! grep -q "^${heading}\$" "${current}"; then
        # shellcheck disable=SC2129
        echo "${heading}" >> "${current}"
        # shellcheck disable=SC2001
        echo "${heading}" | sed -e 's/./-/g' >> "${current}"
        echo "" >> "${current}"
    fi

    updated=$(mktemp)
    awk -v heading="${heading}" -v content="${content}" '
        $0 == heading {
            print
            if (getline > 0) print
            while ((getline entry < content) > 0) print entry
            close(content)
            next
        }
        { print }
    ' "${current}" > "${updated}"
    mv "${updated}" "${current}"

   rm "${content}"
done

# Generate statistics from the same conventional commits that produce the
# changelog. Calling every commit a pull request caused incorrect totals when
# release commits, merges, or uncategorized maintenance commits were present.
statistics=$(mktemp)
git log "${RELEASE_BASE_VERSION}..HEAD" --pretty="format:%s%x09%aN" \
    | awk -F '\t' -v types="${CHANGELOG_TYPES}" '
        BEGIN { pattern="^(" types ")(\\([^)]*\\))?!?:[[:space:]]*" }
        function automated(name, normalized) {
            normalized=tolower(name)
            return normalized ~ /\[bot\]/ || normalized ~ /(^|[^a-z])copilot([^a-z]|$)/ \
                || normalized ~ /(^|[^a-z])claude([^a-z]|$)/ || normalized ~ /sourcery/ \
                || normalized ~ /dependabot/ || normalized ~ /github release action/ \
                || normalized ~ /django cms release/ || normalized == "unknown"
        }
        tolower($1) ~ pattern && !automated($2) { print $2 }
    ' > "${statistics}"
CHANGE_NUMBER=$(wc -l < "${statistics}" | tr -d ' ')
if [ "${CHANGE_NUMBER}" -eq 1 ]; then
    CHANGE_LABEL="entry"
else
    CHANGE_LABEL="entries"
fi

cat >> "${current}" << EOF
Statistics:
-----------

This release includes ${CHANGE_NUMBER} changelog ${CHANGE_LABEL}, created with the help of the following contributors
(in alphabetical order):

EOF

while read -r name; do
    count=$(grep -Fxc "${name}" "${statistics}")
    if [ "${count}" -gt 1 ]; then
        pl="s"
    else
        pl=""
    fi
    echo "* ${name} (${count} changelog entr${pl:+ies}${pl:-y})" >> "${current}"
done < <(sort -u "${statistics}")

# shellcheck disable=SC2129
cat >> "${current}" << EOF

With the review help of the following contributors:

EOF

git log "${RELEASE_BASE_VERSION}..HEAD" | awk '
    function automated(name, normalized) {
        normalized=tolower(name)
        return normalized ~ /\[bot\]/ || normalized ~ /(^|[^a-z])copilot([^a-z]|$)/ \
            || normalized ~ /(^|[^a-z])claude([^a-z]|$)/ || normalized ~ /sourcery/ \
            || normalized ~ /dependabot/ || normalized ~ /github release action/ \
            || normalized ~ /django cms release/ || normalized == "unknown"
    }
    tolower($0) ~ /^[[:space:]]*co-authored-by:/ {
        sub(/^[^:]*:[[:space:]]*/, "* ")
        sub(/ <.*$/, "")
        name=$0
        sub(/^\* /, "", name)
        if (!automated(name)) print
    }
' | sort -fu >> "${current}"

cat >> "${current}" << EOF

Thanks to all contributors for their efforts!

EOF

cat "${current}" "${legacy}" > CHANGELOG.rst
rm "${current}" "${legacy}" "${statistics}"

# now computing sections
