#!/bin/bash
set -euo pipefail

# macOS App Code Signing & Notarization Script
# For distribution outside the App Store (Developer ID)

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

info()    { echo -e "${CYAN}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn()    { echo -e "${YELLOW}[WARN]${NC} $1"; }
error()   { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }

prompt() {
    local var_name=$1 prompt_text=$2 default=${3:-}
    if [[ -n "$default" ]]; then
        read -rp "$(echo -e "${CYAN}$prompt_text${NC} [$default]: ")" value
        eval "$var_name=\"${value:-$default}\""
    else
        read -rp "$(echo -e "${CYAN}$prompt_text${NC}: ")" value
        [[ -z "$value" ]] && error "A value is required."
        eval "$var_name=\"$value\""
    fi
}

confirm() {
    read -rp "$(echo -e "${YELLOW}$1 (y/n)${NC}: ")" answer
    [[ "$answer" =~ ^[Yy]$ ]]
}

echo ""
echo "=============================================="
echo "  macOS App Signing & Notarization Tool"
echo "  (Developer ID - Outside App Store)"
echo "=============================================="
echo ""

# --- Step 1: Check prerequisites ---
info "Checking prerequisites..."

if ! command -v xcrun &>/dev/null; then
    error "Xcode command-line tools not found. Install with: xcode-select --install"
fi
success "Xcode CLI tools found."

if ! command -v codesign &>/dev/null; then
    error "codesign not found."
fi
success "codesign found."

# --- Step 2: List available signing identities ---
echo ""
info "Available Developer ID signing identities:"
echo ""

IDENTITIES=$(security find-identity -v -p codesigning 2>/dev/null | grep "Developer ID Application" || true)

if [[ -z "$IDENTITIES" ]]; then
    warn "No 'Developer ID Application' certificates found in keychain."
    echo ""
    echo "  You need a Developer ID Application certificate from Apple."
    echo "  1. Go to https://developer.apple.com/account/resources/certificates"
    echo "  2. Create a 'Developer ID Application' certificate"
    echo "  3. Download and install it in Keychain Access"
    echo ""
    if ! confirm "Continue anyway with manual identity entry?"; then
        exit 1
    fi
    prompt SIGNING_IDENTITY "Enter signing identity (name or SHA-1 hash)"
else
    echo "$IDENTITIES"
    echo ""

    IDENTITY_COUNT=$(echo "$IDENTITIES" | wc -l | tr -d ' ')
    if [[ "$IDENTITY_COUNT" -eq 1 ]]; then
        DEFAULT_IDENTITY=$(echo "$IDENTITIES" | sed -E 's/.*"(.*)".*/\1/')
        if confirm "Use '$DEFAULT_IDENTITY'?"; then
            SIGNING_IDENTITY="$DEFAULT_IDENTITY"
        else
            prompt SIGNING_IDENTITY "Enter signing identity (name or SHA-1 hash)"
        fi
    else
        prompt SIGNING_IDENTITY "Enter signing identity (name or SHA-1 hash)"
    fi
fi

success "Signing identity: $SIGNING_IDENTITY"

# --- Step 3: Select .app bundle ---
echo ""
prompt APP_PATH "Path to .app bundle (drag and drop works)"

# Strip trailing whitespace and quotes from drag-and-drop
APP_PATH=$(echo "$APP_PATH" | sed "s/^['\"]//;s/['\"]$//;s/ *$//")

[[ ! -d "$APP_PATH" ]] && error "'$APP_PATH' is not a valid directory."
[[ "$APP_PATH" != *.app ]] && error "'$APP_PATH' does not appear to be a .app bundle."

APP_NAME=$(basename "$APP_PATH" .app)
APP_DIR=$(dirname "$APP_PATH")
success "App: $APP_NAME ($APP_PATH)"

# --- Step 4: Entitlements ---
echo ""
ENTITLEMENTS_FILE=""
if confirm "Do you have an entitlements file (.entitlements/.plist)?"; then
    prompt ENTITLEMENTS_FILE "Path to entitlements file"
    ENTITLEMENTS_FILE=$(echo "$ENTITLEMENTS_FILE" | sed "s/^['\"]//;s/['\"]$//;s/ *$//")
    [[ ! -f "$ENTITLEMENTS_FILE" ]] && error "Entitlements file not found: $ENTITLEMENTS_FILE"
    success "Entitlements: $ENTITLEMENTS_FILE"
else
    if confirm "Generate a default entitlements file (hardened runtime)?"; then
        ENTITLEMENTS_FILE="$APP_DIR/${APP_NAME}_entitlements.plist"
        cat > "$ENTITLEMENTS_FILE" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <false/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <false/>
</dict>
</plist>
PLIST
        success "Generated entitlements at: $ENTITLEMENTS_FILE"
        echo ""
        warn "Review and edit this file if your app needs additional entitlements"
        warn "(e.g., network access, file access, JIT, unsigned memory)."
        if ! confirm "Continue with this entitlements file?"; then
            info "Edit the file and re-run the script."
            exit 0
        fi
    fi
fi

# --- Step 5: Sign embedded components first ---
echo ""
info "Scanning for embedded components to sign..."

FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
PLUGINS_DIR="$APP_PATH/Contents/PlugIns"
HELPERS_DIR="$APP_PATH/Contents/Helpers"
RESOURCES_DIR="$APP_PATH/Contents/Resources"

sign_item() {
    local item=$1
    local opts=(--force --timestamp --options runtime -s "$SIGNING_IDENTITY")
    [[ -n "$ENTITLEMENTS_FILE" ]] && opts+=(--entitlements "$ENTITLEMENTS_FILE")

    info "  Signing: $(basename "$item")"
    if codesign "${opts[@]}" "$item" 2>&1; then
        success "  Signed: $(basename "$item")"
    else
        error "Failed to sign: $item"
    fi
}

# Sign dylibs and frameworks
EMBEDDED_COUNT=0
for dir in "$FRAMEWORKS_DIR" "$PLUGINS_DIR" "$HELPERS_DIR"; do
    if [[ -d "$dir" ]]; then
        while IFS= read -r -d '' item; do
            sign_item "$item"
            ((EMBEDDED_COUNT++))
        done < <(find "$dir" \( -name "*.dylib" -o -name "*.framework" -o -name "*.app" -o -name "*.xpc" \) -print0 2>/dev/null)
    fi
done

# Sign helper tools / executables in MacOS dir (excluding the main binary)
MAIN_EXECUTABLE=$(defaults read "$APP_PATH/Contents/Info" CFBundleExecutable 2>/dev/null || echo "$APP_NAME")
if [[ -d "$APP_PATH/Contents/MacOS" ]]; then
    while IFS= read -r -d '' item; do
        if [[ "$(basename "$item")" != "$MAIN_EXECUTABLE" ]]; then
            sign_item "$item"
            ((EMBEDDED_COUNT++))
        fi
    done < <(find "$APP_PATH/Contents/MacOS" -type f -perm +111 -print0 2>/dev/null)
fi

if [[ $EMBEDDED_COUNT -eq 0 ]]; then
    info "  No embedded components found."
else
    success "Signed $EMBEDDED_COUNT embedded component(s)."
fi

# --- Step 6: Sign the main app bundle ---
echo ""
info "Signing the main app bundle..."

SIGN_OPTS=(--force --timestamp --options runtime --sign "$SIGNING_IDENTITY")
[[ -n "$ENTITLEMENTS_FILE" ]] && SIGN_OPTS+=(--entitlements "$ENTITLEMENTS_FILE")
SIGN_OPTS+=("$APP_PATH")

if codesign "${SIGN_OPTS[@]}" 2>&1; then
    success "App bundle signed successfully."
else
    error "Failed to sign the app bundle."
fi

# --- Step 7: Verify signature ---
echo ""
info "Verifying code signature..."

if codesign --verify --deep --strict --verbose=2 "$APP_PATH" 2>&1; then
    success "Signature verification passed."
else
    error "Signature verification failed."
fi

echo ""
if spctl --assess --type execute --verbose "$APP_PATH" 2>&1; then
    success "Gatekeeper assessment passed."
else
    warn "Gatekeeper assessment failed (this is expected before notarization)."
fi

# --- Step 8: Package for notarization ---
echo ""
info "Packaging for notarization..."

echo "  1) Create a ZIP archive"
echo "  2) Create a DMG disk image"
echo "  3) Skip packaging (already have a distributable)"
read -rp "$(echo -e "${CYAN}Choose packaging format [1]:${NC} ")" PKG_CHOICE
PKG_CHOICE=${PKG_CHOICE:-1}

ARCHIVE_PATH=""
case "$PKG_CHOICE" in
    1)
        ARCHIVE_PATH="$APP_DIR/${APP_NAME}.zip"
        info "Creating ZIP archive..."
        ditto -c -k --keepParent "$APP_PATH" "$ARCHIVE_PATH"
        success "Created: $ARCHIVE_PATH"
        ;;
    2)
        DMG_PATH="$APP_DIR/${APP_NAME}.dmg"
        ARCHIVE_PATH="$DMG_PATH"
        info "Creating DMG..."

        TEMP_DMG_DIR=$(mktemp -d)
        cp -R "$APP_PATH" "$TEMP_DMG_DIR/"
        ln -s /Applications "$TEMP_DMG_DIR/Applications"

        hdiutil create -volname "$APP_NAME" -srcfolder "$TEMP_DMG_DIR" \
            -ov -format UDZO "$DMG_PATH" 2>&1

        rm -rf "$TEMP_DMG_DIR"

        # Sign the DMG too
        codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG_PATH" 2>&1
        success "Created and signed: $DMG_PATH"
        ;;
    3)
        warn "Skipping packaging."
        prompt ARCHIVE_PATH "Path to existing archive (ZIP or DMG) for notarization"
        ARCHIVE_PATH=$(echo "$ARCHIVE_PATH" | sed "s/^['\"]//;s/['\"]$//;s/ *$//")
        [[ ! -f "$ARCHIVE_PATH" ]] && error "File not found: $ARCHIVE_PATH"
        ;;
    *)
        error "Invalid choice."
        ;;
esac

# --- Step 9: Notarization ---
echo ""
if confirm "Submit for notarization with Apple?"; then

    info "Notarization requires an App Store Connect API key or Apple ID credentials."
    echo ""
    echo "  1) Apple ID (email + app-specific password)"
    echo "  2) App Store Connect API Key"
    read -rp "$(echo -e "${CYAN}Authentication method [1]:${NC} ")" AUTH_CHOICE
    AUTH_CHOICE=${AUTH_CHOICE:-1}

    prompt TEAM_ID "Apple Developer Team ID"

    NOTARIZE_ARGS=()

    case "$AUTH_CHOICE" in
        1)
            prompt APPLE_ID "Apple ID (email)"
            info "You need an app-specific password from https://appleid.apple.com"
            echo "  Tip: store it in Keychain with:"
            echo "    xcrun notarytool store-credentials \"notarytool\" --apple-id YOUR_EMAIL --team-id YOUR_TEAM"
            echo ""

            if confirm "Use stored keychain credentials profile?"; then
                prompt KEYCHAIN_PROFILE "Keychain profile name" "notarytool"
                NOTARIZE_ARGS=(--keychain-profile "$KEYCHAIN_PROFILE")
            else
                prompt APP_PASSWORD "App-specific password"
                NOTARIZE_ARGS=(--apple-id "$APPLE_ID" --password "$APP_PASSWORD" --team-id "$TEAM_ID")
            fi
            ;;
        2)
            prompt API_KEY "API Key ID"
            prompt API_ISSUER "API Issuer ID"
            prompt API_KEY_PATH "Path to .p8 key file"
            API_KEY_PATH=$(echo "$API_KEY_PATH" | sed "s/^['\"]//;s/['\"]$//;s/ *$//")
            NOTARIZE_ARGS=(--key "$API_KEY_PATH" --key-id "$API_KEY" --issuer "$API_ISSUER")
            ;;
        *)
            error "Invalid choice."
            ;;
    esac

    echo ""
    info "Submitting '$ARCHIVE_PATH' for notarization..."
    info "This may take several minutes..."

    if xcrun notarytool submit "$ARCHIVE_PATH" "${NOTARIZE_ARGS[@]}" --wait 2>&1; then
        success "Notarization succeeded!"

        # --- Step 10: Staple the ticket ---
        echo ""
        info "Stapling notarization ticket..."

        if [[ "$ARCHIVE_PATH" == *.dmg ]]; then
            xcrun stapler staple "$ARCHIVE_PATH" 2>&1
            success "Stapled ticket to DMG."
        else
            xcrun stapler staple "$APP_PATH" 2>&1
            success "Stapled ticket to app bundle."

            if [[ "$ARCHIVE_PATH" == *.zip ]]; then
                info "Re-creating ZIP with stapled app..."
                rm -f "$ARCHIVE_PATH"
                ditto -c -k --keepParent "$APP_PATH" "$ARCHIVE_PATH"
                success "Updated ZIP: $ARCHIVE_PATH"
            fi
        fi
    else
        error "Notarization failed. Run 'xcrun notarytool log <submission-id> ${NOTARIZE_ARGS[*]}' for details."
    fi
else
    warn "Skipping notarization. Your app will trigger Gatekeeper warnings without it."
fi

# --- Done ---
echo ""
echo "=============================================="
echo -e "  ${GREEN}Done!${NC}"
echo "=============================================="
echo ""
echo "  App:     $APP_PATH"
[[ -n "$ARCHIVE_PATH" ]] && echo "  Archive: $ARCHIVE_PATH"
echo ""
echo "  Verify on another machine with:"
echo "    spctl --assess --type execute -v \"$APP_PATH\""
echo "    codesign -dv --verbose=4 \"$APP_PATH\""
echo ""
