#!/bin/sh
# architecture.manifest.audit.sh
# Read-only POSIX architecture audit driven by an external JSON manifest.
#
# Usage:
#   sh architecture.manifest.audit.sh system [options]
#   sh architecture.manifest.audit.sh app APP_NAME [options]
#
# The manifest is the standard. Update the manifest, not this engine, when
# directory requirements or naming policy change.

set -eu

DEFAULT_MANIFEST=${ARCH_MANIFEST:-/home/intel145/websites/system/policies/architecture.manifest.json}
APPS_ROOT=${APPS_ROOT:-/home/intel145/websites/apps}
SYSTEM_ROOT=${SYSTEM_ROOT:-/home/intel145/websites/system}

MODE=
APP_NAME=
MANIFEST=$DEFAULT_MANIFEST
DETAILS=0
SUMMARY=0
MISSING_ONLY=0
INCLUDE_BACKUPS=0
REPORT_FILE=

PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
ERROR_COUNT=0

usage() {
    cat <<'EOF'
Usage:
  architecture.manifest.audit.sh system [options]
  architecture.manifest.audit.sh app APP_NAME [options]

Options:
  --manifest FILE    JSON architecture manifest.
  --details          Show rule, purpose, action, and exact paths/lines.
  --summary          Show PASS/WARN/FAIL/ERROR totals.
  --missing-only     Show only FAIL and ERROR findings during the main output.
  --backup           Include backup/editor-residue checks.
  --report FILE      Write a complete text report to FILE.
  -h, --help         Show this help.

Environment:
  APPS_ROOT          Default: /home/intel145/websites/apps
  SYSTEM_ROOT        Default: /home/intel145/websites/system
  ARCH_MANIFEST      Default manifest path.

The audit is read-only. It does not change files, permissions, services,
Nginx configuration, databases, or sessions.
EOF
}

if [ "$#" -eq 0 ]; then
    usage
    exit 2
fi

MODE=$1
shift

case "$MODE" in
    system)
        ;;
    app)
        if [ "$#" -eq 0 ]; then
            echo 'ERROR: app mode requires APP_NAME.' >&2
            exit 2
        fi
        APP_NAME=$1
        shift
        ;;
    -h|--help)
        usage
        exit 0
        ;;
    *)
        echo 'ERROR: first argument must be system or app.' >&2
        exit 2
        ;;
esac

while [ "$#" -gt 0 ]; do
    case "$1" in
        --manifest)
            if [ "$#" -lt 2 ]; then
                echo 'ERROR: --manifest requires FILE.' >&2
                exit 2
            fi
            MANIFEST=$2
            shift 2
            ;;
        --details)
            DETAILS=1
            shift
            ;;
        --summary)
            SUMMARY=1
            shift
            ;;
        --missing-only)
            MISSING_ONLY=1
            shift
            ;;
        --backup)
            INCLUDE_BACKUPS=1
            shift
            ;;
        --report)
            if [ "$#" -lt 2 ]; then
                echo 'ERROR: --report requires FILE.' >&2
                exit 2
            fi
            REPORT_FILE=$2
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            echo "ERROR: unknown option: $1" >&2
            usage >&2
            exit 2
            ;;
    esac
done

if [ ! -f "$MANIFEST" ]; then
    echo "ERROR: manifest not found: $MANIFEST" >&2
    exit 2
fi

if ! command -v python3 >/dev/null 2>&1; then
    echo 'ERROR: python3 is required to parse the JSON manifest.' >&2
    exit 2
fi

json_value() {
    python3 - "$MANIFEST" "$@" <<'PY'
import json
import sys

path = sys.argv[1]
keys = sys.argv[2:]

with open(path, encoding="utf-8") as handle:
    value = json.load(handle)

for key in keys:
    value = value[key]

if isinstance(value, list):
    for item in value:
        print(item)
elif isinstance(value, bool):
    print("true" if value else "false")
else:
    print(value)
PY
}

if [ "$MODE" = 'app' ]; then
    APP_REGEX=$(json_value naming app_name_regex)

    if ! printf '%s\n' "$APP_NAME" | grep -Eq "$APP_REGEX"; then
        echo "ERROR: app name does not match manifest policy: $APP_REGEX" >&2
        exit 2
    fi

    TARGET_ROOT=$APPS_ROOT/$APP_NAME
else
    TARGET_ROOT=$SYSTEM_ROOT
fi

if [ ! -d "$TARGET_ROOT" ]; then
    echo "ERROR: target root not found: $TARGET_ROOT" >&2
    exit 2
fi

TARGET_ROOT=$(cd "$TARGET_ROOT" && pwd -P)

if [ "$MODE" = 'app' ]; then
    APPS_ROOT_REAL=$(cd "$APPS_ROOT" && pwd -P)

    case "$TARGET_ROOT/" in
        "$APPS_ROOT_REAL/"*)
            ;;
        *)
            echo 'ERROR: resolved app path escapes APPS_ROOT.' >&2
            exit 2
            ;;
    esac
else
    SYSTEM_ROOT_REAL=$(cd "$SYSTEM_ROOT" && pwd -P)

    if [ "$TARGET_ROOT" != "$SYSTEM_ROOT_REAL" ]; then
        echo 'ERROR: resolved system path does not match SYSTEM_ROOT.' >&2
        exit 2
    fi
fi

TMP_BASE=${TMPDIR:-/tmp}/architecture_manifest_audit_$$
TMP_DETAILS=$TMP_BASE.details
TMP_REPORT=$TMP_BASE.report
TMP_NAMING=$TMP_BASE.naming
TMP_WRAPPERS=$TMP_BASE.wrappers
TMP_BACKUPS=$TMP_BASE.backups

cleanup() {
    rm -f \
        "$TMP_DETAILS" \
        "$TMP_REPORT" \
        "$TMP_NAMING" \
        "$TMP_WRAPPERS" \
        "$TMP_BACKUPS"
}

trap cleanup EXIT HUP INT TERM

: > "$TMP_DETAILS"
: > "$TMP_REPORT"
: > "$TMP_NAMING"
: > "$TMP_WRAPPERS"
: > "$TMP_BACKUPS"

record() {
    printf '%s\n' "$1" >> "$TMP_REPORT"
}

emit() {
    status=$1
    finding_id=$2
    message=$3
    detail=${4:-}

    line=$(printf '%-7s %-16s %s' "$status" "$finding_id" "$message")

    if [ "$MISSING_ONLY" -eq 0 ] || \
       [ "$status" = 'FAIL' ] || \
       [ "$status" = 'ERROR' ]; then
        printf '%s\n' "$line"
    fi

    record "$line"

    case "$status" in
        PASS)
            PASS_COUNT=$((PASS_COUNT + 1))
            ;;
        WARN)
            WARN_COUNT=$((WARN_COUNT + 1))
            ;;
        FAIL)
            FAIL_COUNT=$((FAIL_COUNT + 1))
            ;;
        ERROR)
            ERROR_COUNT=$((ERROR_COUNT + 1))
            ;;
    esac

    if [ -n "$detail" ]; then
        printf '[%s] %s\n%s\n' \
            "$finding_id" \
            "$message" \
            "$detail" \
            >> "$TMP_DETAILS"
    fi

    return 0
}

count_lines() {
    if [ -s "$1" ]; then
        wc -l < "$1" | tr -d ' '
    else
        printf '0'
    fi
}

manifest_list() {
    json_value "$@" 2>/dev/null || true
}

check_directory_list() {
    kind=$1
    section=$2
    key=$3
    status_missing=$4

    manifest_list "$section" "$key" | while IFS= read -r directory; do
        [ -n "$directory" ] || continue

        if [ -d "$TARGET_ROOT/$directory" ]; then
            case "$kind" in
                required)
                    emit PASS "DIR-$directory" "Required directory exists: $directory/."
                    ;;
                recommended)
                    emit PASS "RECOMMEND-$directory" "Recommended directory exists: $directory/."
                    ;;
                forbidden)
                    emit FAIL "FORBID-$directory" "Forbidden directory exists: $directory/." "Rule: manifest forbids $directory/ for this root type.
Purpose: keep system and application ownership boundaries clear.
Action: move the content to an approved directory or update the manifest after review."
                    ;;
            esac
        else
            case "$kind" in
                required)
                    emit "$status_missing" "DIR-$directory" "Required directory missing: $directory/." "Rule: manifest requires $directory/.
Purpose: this is part of the approved architecture baseline.
Action: create it only if the current approved standard truly requires it."
                    ;;
                recommended)
                    emit WARN "RECOMMEND-$directory" "Recommended directory not present: $directory/." "Rule: manifest recommends $directory/.
Purpose: future/optional capability; it is not a baseline requirement.
Action: create it only when that capability is adopted."
                    ;;
                forbidden)
                    emit PASS "FORBID-$directory" "Forbidden directory absent: $directory/."
                    ;;
            esac
        fi
    done
}

check_any_entry_file() {
    found=0
    expected=

    manifest_list application required_any_entry_files | while IFS= read -r entry; do
        [ -n "$entry" ] || continue
        entry=$(printf '%s' "$entry" | sed "s/{app}/$APP_NAME/g")
        printf '%s\n' "$entry"
    done > "$TMP_BASE.entries"

    while IFS= read -r entry; do
        [ -n "$entry" ] || continue
        expected=${expected}${expected:+\n}$entry

        if [ -f "$TARGET_ROOT/$entry" ]; then
            found=1
        fi
    done < "$TMP_BASE.entries"

    if [ "$found" -eq 1 ]; then
        emit PASS ENTRY 'At least one approved entry file exists.'
    else
        emit FAIL ENTRY 'No approved entry file exists.' "Rule: an app must contain at least one registered/conventional public entry file.
Expected one of:
$expected"
    fi
}

check_naming() {
    python3 - "$MANIFEST" "$TARGET_ROOT" > "$TMP_NAMING" <<'PY'
import json
import os
import re
import sys

manifest_path, root = sys.argv[1:]
with open(manifest_path, encoding="utf-8") as handle:
    manifest = json.load(handle)

name = manifest["naming"]
dir_re = re.compile(name["directory_name_regex"])
file_re = re.compile(name["file_base_name_regex"])
exception_dirs = set(name.get("exception_directories", []))
exception_paths = set(name.get("exception_file_paths", []))

for base, directories, files in os.walk(root):
    rel_base = os.path.relpath(base, root)
    parts = [] if rel_base == "." else rel_base.split(os.sep)
    inherited_exception = any(part in exception_dirs for part in parts)

    for directory in directories:
        rel = os.path.relpath(os.path.join(base, directory), root)
        if inherited_exception or directory in exception_dirs:
            print(f"DIR_EXCEPTION|{rel}")
        elif not dir_re.fullmatch(directory):
            print(f"DIR|{rel}")

    for filename in files:
        rel = os.path.relpath(os.path.join(base, filename), root)
        stem = filename.rsplit(".", 1)[0] if "." in filename else filename
        if rel in exception_paths or inherited_exception:
            print(f"FILE_EXCEPTION|{rel}")
        elif not file_re.fullmatch(stem):
            print(f"FILE|{rel}")
PY

    directory_count=$(grep -c '^DIR|' "$TMP_NAMING" 2>/dev/null || true)
    file_count=$(grep -c '^FILE|' "$TMP_NAMING" 2>/dev/null || true)
    exception_count=$(grep -c '_EXCEPTION|' "$TMP_NAMING" 2>/dev/null || true)

    directory_regex=$(json_value naming directory_name_regex)
    file_regex=$(json_value naming file_base_name_regex)

    if [ "$directory_count" -eq 0 ]; then
        emit PASS NAME-DIR 'Directory names comply with manifest policy.'
    else
        emit WARN NAME-DIR "$directory_count directory naming violations found." "Rule: $directory_regex
Purpose: consistent portable directory naming.
Action: register a legacy exception or migrate carefully.
Matches:
$(grep '^DIR|' "$TMP_NAMING" | sed 's/^DIR|//')"
    fi

    if [ "$file_count" -eq 0 ]; then
        emit PASS NAME-FILE 'File base names comply with manifest policy.'
    else
        emit WARN NAME-FILE "$file_count file naming violations found." "Rule: $file_regex
Purpose: consistent portable filename naming.
Action: register a legacy exception or migrate carefully.
Matches:
$(grep '^FILE|' "$TMP_NAMING" | sed 's/^FILE|//')"
    fi

    if [ "$exception_count" -gt 0 ]; then
        emit WARN NAME-LEGACY "$exception_count paths are covered by legacy naming exception directories." "Rule: exception paths remain visible and require periodic migration review.
Matches:
$(grep '_EXCEPTION|' "$TMP_NAMING" | sed 's/^[A-Z_]*|//')"
    fi
}

check_component_rules() {
    max_wrappers=$(json_value application component_rules max_active_app_wrapper_files)

    grep -RIl \
        --include='*.php' \
        -E 'class="[^"]*app-wrapper' \
        "$TARGET_ROOT" \
        2>/dev/null \
        | grep -vE '/(old|test|tests|fixture|fixtures|example|examples|backup|moved|tmp)/' \
        > "$TMP_WRAPPERS" \
        || true

    wrapper_count=$(count_lines "$TMP_WRAPPERS")

    if [ "$wrapper_count" -le "$max_wrappers" ]; then
        emit PASS WRAPPER 'Active app-wrapper ownership is within manifest limit.'
    else
        emit FAIL WRAPPER "$wrapper_count active files declare app-wrapper; manifest limit is $max_wrappers." "Rule: only the primary application layout owns .app-wrapper.
Purpose: prevent nested AdminLTE grids and duplicate global layout roots.
Action: remove the wrapper from active partials or move non-active examples outside the served app root.
Matches:
$(cat "$TMP_WRAPPERS")"
    fi

    manifest_list application component_rules fragment_directories | while IFS= read -r fragment; do
        [ -n "$fragment" ] || continue
        [ -d "$TARGET_ROOT/$fragment" ] || continue

        fragment_file=$TMP_BASE.fragment.$fragment

        grep -RIn \
            --include='*.php' \
            -E 'class="[^"]*(app-wrapper|app-main)' \
            "$TARGET_ROOT/$fragment" \
            2>/dev/null \
            > "$fragment_file" \
            || true

        fragment_count=$(count_lines "$fragment_file")

        if [ "$fragment_count" -eq 0 ]; then
            emit PASS "FRAG-$fragment" "$fragment/ contains no global layout markers."
        else
            emit FAIL "FRAG-$fragment" "$fragment/ contains global layout markers." "Rule: fragment directories may not declare .app-wrapper or .app-main.
Purpose: fragments must not create a second global page layout.
Action: retain only local component markup in the fragment.
Matches:
$(cat "$fragment_file")"
        fi
    done
}

check_backup_files() {
    : > "$TMP_BACKUPS"

    manifest_list audit backup_file_patterns | while IFS= read -r pattern; do
        [ -n "$pattern" ] || continue

        find "$TARGET_ROOT" \
            -type f \
            -name "$pattern" \
            -print \
            2>/dev/null \
            >> "$TMP_BACKUPS"
    done

    backup_count=$(count_lines "$TMP_BACKUPS")

    if [ "$backup_count" -eq 0 ]; then
        emit PASS BACKUP 'No backup/editor-residue files found.'
    else
        emit WARN BACKUP "$backup_count backup/editor-residue files found." "Rule: backups/editor residue should not remain in a served source tree.
Action: delete unneeded copies, move retained copies outside the served root, or document an exception.
Matches:
$(cat "$TMP_BACKUPS")"
    fi
}

printf 'Manifest architecture audit\n'
printf 'Mode: %s\n' "$MODE"
printf 'Target: %s\n' "$TARGET_ROOT"
printf 'Manifest: %s\n\n' "$MANIFEST"

record 'Manifest architecture audit'
record "Mode: $MODE"
record "Target: $TARGET_ROOT"
record "Manifest: $MANIFEST"
record ''

if [ "$MODE" = 'system' ]; then
    SECTION=system
else
    SECTION=application
fi

check_directory_list required "$SECTION" required_directories FAIL
check_directory_list recommended "$SECTION" recommended_directories WARN
check_directory_list forbidden "$SECTION" forbidden_directories PASS

if [ "$MODE" = 'app' ]; then
    check_any_entry_file
    check_component_rules
fi

check_naming

if [ "$INCLUDE_BACKUPS" -eq 1 ]; then
    check_backup_files
fi

if [ "$DETAILS" -eq 1 ] && [ -s "$TMP_DETAILS" ]; then
    printf '\nDetails\n'
    printf '%s\n' '-------'

    record ''
    record 'Details'
    record '-------'

    cat "$TMP_DETAILS"
    cat "$TMP_DETAILS" >> "$TMP_REPORT"
fi

final_summary=$(printf 'Summary: PASS=%s WARN=%s FAIL=%s ERROR=%s' \
    "$PASS_COUNT" \
    "$WARN_COUNT" \
    "$FAIL_COUNT" \
    "$ERROR_COUNT")

printf '\n%s\n' "$final_summary"
record ''
record "$final_summary"

if [ "$SUMMARY" -eq 1 ]; then
    count_summary=$(printf 'Counts: pass=%s warning=%s failure=%s error=%s' \
        "$PASS_COUNT" \
        "$WARN_COUNT" \
        "$FAIL_COUNT" \
        "$ERROR_COUNT")

    printf '%s\n' "$count_summary"
    record "$count_summary"
fi

if [ -n "$REPORT_FILE" ]; then
    report_dir=$(dirname "$REPORT_FILE")

    if [ ! -d "$report_dir" ]; then
        mkdir -p "$report_dir"
    fi

    cp "$TMP_REPORT" "$REPORT_FILE"
    printf 'Report written: %s\n' "$REPORT_FILE"
fi

if [ "$FAIL_COUNT" -gt 0 ] || [ "$ERROR_COUNT" -gt 0 ]; then
    exit 1
fi

exit 0
