#!/bin/sh

set -eu

# Error helper. See EXIT STATUS below.
die() {
    printf "fpkgremove: %b\n" "$*" >&2
    exit 1
}

## NAME
##     fpkgremove - remove an installed package from a project directory
##
## SYNOPSIS
##     fpkgremove [--skip-dependencies-check]
##                [--skip-all-checks]
##                [-v|--verbose]
##                NAME TARGET
##

usage() {
    die "Usage: fpkgremove [--skip-dependencies-check] [--skip-all-checks] [-v|--verbose] NAME TARGET"
}

## DESCRIPTION
##     fpkgremove removes all files belonging to a package NAME from
##     TARGET. It uses the MANIFEST recorded by fpkginstall, deleting
##     files first, then directories.
##
## OPTIONS
##
##     --skip-dependencies-check
##         Do not verify that no other installed packages depend on NAME
##         before removal.
##
##     --skip-all-checks
##         Skip all pre-removal checks (equivalent to enabling every
##         --skip-* option). Use with caution, as it may corrupt the
##         project's package database or leave dependent packages broken.
##
##     -v, --verbose
##         Print additional progress messages to stderr. Useful for tracing
##         what fpkgremove is doing internally (dependency checks, manifest
##         removals, directory cleanup). By default fpkgremove is silent
##         except for errors.
##

skip_dependencies=0
verbose=0

while test $# -gt 0; do
    case "$1" in
        --skip-dependencies-check)
            skip_dependencies=1
            shift
            ;;
        --skip-all-checks)
            skip_dependencies=1
            shift
            ;;
        -v|--verbose)
            verbose=1
            shift
            ;;
        -*)
            usage
            ;;
        *)
            break
            ;;
    esac
done

## OPERANDS
##     fpkgremove requires two operands.
##

if test $# -ne 2; then
    usage
fi

##     NAME
##         Package name (without version).
##
##         Examples:
##             notes
##             editor-tools
##

name=$1

##     TARGET
##         Directory where the package is installed.
##
##         Examples:
##             .
##             ~/sites/example.com
##             /opt/example.com
##

target=$2

if test ! -d "$target"; then
    die "target not a directory"
fi
if test ! -w "$target"; then
    die "target not writable"
fi

## EXIT STATUS
##     0   Success. Package removed.
##     1   Failure. Invalid arguments, error accessing TARGET,
##         Package not installed or corrupted.
##

metadir="$target/fpkg/$name"

if test ! -d "$metadir"; then
    die "package not installed: $name"
fi

manifest="$metadir/MANIFEST"

if test ! -e "$manifest"; then
    die "missing manifest: $manifest"
fi
if test ! -r "$manifest"; then
    die "unreadable manifest: $manifest"
fi

## PRE-REMOVAL
##     Before removing the package, fpkgremove validates the TARGET
##     environment and checks for dependencies.
##
##     If another installed package depends on NAME, then exit. Skip with
##     --skip-dependencies-check or --skip-all-checks.
##
##     Before removal, fpkgremove ensures no other installed packages declare
##     NAME as a dependency. Otherwise, fpkgremove aborts. Skip checks with
##     --skip-dependencies-check or --skip-all-checks.
##
if test $skip_dependencies -eq 0; then

    errors=

    for other in "$target"/fpkg/*; do

        base=$(basename "$other")

        # Skip the package we are removing
        if test "$base" = "$name"; then
            continue
        fi

        # Ignore staging dirs with ~ in name
        case $base in *~*) continue ;; esac

        # Must be a directory and readable
        if ! test -d "$other"; then
            die "corrupted database: not a directory: $other"
        fi
        if ! test -r "$other"; then
            die "corrupted database: unreadable directory: $other"
        fi

        depfile="$other/DEPENDENCIES"
        if test -e "$depfile"; then
            if ! test -f "$depfile"; then
                die "corrupted database: DEPENDENCIES not a regular file: $depfile"
            fi
            if ! test -r "$depfile"; then
                die "corrupted database: DEPENDENCIES not readable: $depfile"
            fi

            while IFS= read -r dep; do
                case $dep in ''|\#*) continue ;; esac
                set -- $dep
                dep_pkg=$1
                if test "$dep_pkg" = "$name"; then
                    errors="$errors\n    $base depends on $name"
                fi
            done <"$depfile"
        fi
    done

    if test -n "$errors"; then
        die "cannot remove: package is required by others:$errors"
    fi

    if test "$verbose" -eq 1; then
        echo "No other packages depend on $name." >&2
    fi
else
    if test "$verbose" -eq 1; then
        echo "Skip dependency checks: proceeding without validation." >&2
    fi
fi

# ============================================================================

# Debugging
if test "$verbose" -eq 1; then
    echo "All pre-removal checks passed." >&2
    echo "Remove the package!" >&2
fi

# Remove files listed in MANIFEST, in reverse order (files first, dirs last).
while IFS= read -r path; do
    if test -d "$target/$path"; then
        # Only remove empty directories.
        rmdir "$target/$path" 2>/dev/null || true
    else
        rm -f "$target/$path"
    fi
done <<EOF
$(sort -r "$manifest")
EOF

# Remove metadata directory
rm -rf "$metadir"

exit 0

## EXAMPLES
##     Remove the package "notes" from ./myproject:
##
##         fpkgremove notes myproject/
##
##     Remove "notes" but skip dependency checks:
##
##         fpkgremove --skip-dependencies-check notes myproject/
##
## SEE ALSO
##     fpkginstall, fpkglist
