#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

PROGRAM=$(basename "$0")
SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/"$PROGRAM"
ROLE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
ARCHIVE="$ROLE_DIR/sanquan-mysql-q200-network-gate-20260729.tar.gz"
INPUT_TEMPLATE="$ROLE_DIR/inputs/Q200_MYSQL_EXTERNAL_INPUTS.example.env"
EXPECTED_ARCHIVE_SHA256=8cd2a6b327a49fd99d167e7f90dc1a0ca87a10ca52add334ab8cd339fb261fc3
EXPECTED_MATRIX_SHA256=85d5b4f47dc5a3d654f8e757e7329d59fcdf4331e19ebfec56bc7c3fa800e305
EXPECTED_Q122_SHA256=d20143cc9df939c33de3aa1b8e08ebd1b4d59b6a4322b32d68d83dc622b69808
MODE=${1:-}

usage() {
  printf '%s\n' \
    "Usage:" \
    "  $PROGRAM --static-validate" \
    "  $PROGRAM audit-package" \
    "  $PROGRAM validate-inputs /absolute/local/input.env"
}

die() {
  printf 'Q200_MYSQL_LOCAL_AUDIT_ERROR=%s\n' "$*" >&2
  return 1
}

finish() {
  local rc=$?
  printf 'Q200_MYSQL_LOCAL_AUDIT_MODE=%s\n' "${MODE:-INVALID}"
  printf 'Q200_MYSQL_LOCAL_AUDIT_END=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
  printf 'Q200_MYSQL_LOCAL_AUDIT_RC=%s\n' "$rc"
  printf 'REMOTE_MUTATION=ZERO\n'
  printf 'Q221_REPLICATION=NOT_PERFORMED_Q221_GATE\n'
  printf 'Q261_BUSINESS_SQL=NOT_PERFORMED_Q261_GATE\n'
  if [[ "$rc" -eq 0 ]]; then
    printf 'Q200_MYSQL_LOCAL_AUDIT_JUDGMENT=PASS_LOCAL_ONLY\n'
  else
    printf 'Q200_MYSQL_LOCAL_AUDIT_JUDGMENT=FAIL_CLOSED\n'
  fi
  printf 'END Q200_MYSQL_PACKAGE_AND_INPUT_AUDIT RC=%s\n' "$rc"
  return "$rc"
}
trap finish EXIT

printf 'BEGIN Q200_MYSQL_PACKAGE_AND_INPUT_AUDIT\n'
printf 'Q200_MYSQL_LOCAL_AUDIT_START=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"

static_validate() {
  local marker key
  bash -n "$SCRIPT_PATH"
  [[ -f "$ARCHIVE" && ! -L "$ARCHIVE" ]] || die 'locked Q-200 archive is absent'
  [[ -f "$INPUT_TEMPLATE" && ! -L "$INPUT_TEMPLATE" ]] || die 'input template is absent'
  for marker in \
    "$EXPECTED_ARCHIVE_SHA256" \
    "$EXPECTED_MATRIX_SHA256" \
    "$EXPECTED_Q122_SHA256" \
    Q200_DEP_Q192 \
    Q200_DEP_Q194 \
    Q200_MANAGEMENT_CIDR \
    Q200_NTP_SOURCES \
    Q200_DNS_SERVERS \
    Q200_REDIS01_ORCHESTRATOR_DB_ACCESS_DECISION \
    Q200_REMOTE_LOCK_OWNER \
    Q200_MYSQL_PACKAGE_EXECUTION_BLOCKED_NEEDS_CONTRACT_REVISION
  do
    grep -Fq -- "$marker" "$SCRIPT_PATH" "$INPUT_TEMPLATE" ||
      die "required static marker missing: $marker"
  done
  for key in "${REQUIRED_KEYS[@]}"; do
    [[ "$(grep -Ec "^${key}=" "$INPUT_TEMPLATE")" -eq 1 ]] ||
      die "input template key missing or duplicated: $key"
  done
  printf 'Q200_MYSQL_LOCAL_AUDITOR_STATIC_PASS\n'
}

archive_integrity() {
  local actual member expected actual_member actual_members expected_members_text
  local -a expected_members=(
    README.md
    SHA256SUMS
    firewall-required.tsv
    q200-mysql-network-gate.sh
    targets.tsv
  )
  actual=$(sha256sum "$ARCHIVE" | awk '{print $1}')
  [[ "$actual" == "$EXPECTED_ARCHIVE_SHA256" ]] ||
    die "outer archive SHA256 mismatch: $actual"
  actual_members=$(tar -tzf "$ARCHIVE" | sort)
  expected_members_text=$(printf '%s\n' "${expected_members[@]}" | sort)
  [[ "$actual_members" == "$expected_members_text" ]] ||
    die 'archive member set mismatch'
  while read -r expected member; do
    [[ -n "$expected" && -n "$member" ]] || continue
    actual_member=$(tar -xOzf "$ARCHIVE" "$member" | sha256sum | awk '{print $1}')
    [[ "$actual_member" == "$expected" ]] ||
      die "inner member SHA256 mismatch: $member"
  done < <(tar -xOzf "$ARCHIVE" SHA256SUMS)
  bash -n <(tar -xOzf "$ARCHIVE" q200-mysql-network-gate.sh)
  printf 'Q200_MYSQL_PACKAGE_OUTER_INNER_INTEGRITY_PASS\n'
}

audit_package() {
  local package_script
  archive_integrity
  package_script=$(tar -xOzf "$ARCHIVE" q200-mysql-network-gate.sh)

  grep -Fq 'Q200_MANAGEMENT_CIDR' <<<"$package_script" ||
    die 'management CIDR gate unexpectedly absent'
  grep -Fq 'NTPSynchronized' <<<"$package_script" ||
    die 'NTP synchronization observation unexpectedly absent'
  grep -Fq 'getent ahostsv4' <<<"$package_script" ||
    die 'resolver observation unexpectedly absent'
  grep -Fq 'firewall-cmd' <<<"$package_script" ||
    die 'firewalld observation unexpectedly absent'

  ! grep -Fq 'Q200_DEP_Q194' <<<"$package_script" ||
    die 'locked package unexpectedly changed: Q-194 marker present'
  ! grep -Fq 'Q200_DEP_Q192' <<<"$package_script" ||
    die 'locked package unexpectedly changed: Q-192 marker present'
  ! grep -Fq 'Q200_NTP_SOURCES' <<<"$package_script" ||
    die 'locked package unexpectedly changed: NTP-source marker present'
  ! grep -Fq 'Q200_DNS_SERVERS' <<<"$package_script" ||
    die 'locked package unexpectedly changed: DNS-server marker present'
  ! grep -Fq 'Q200_REMOTE_LOCK_OWNER' <<<"$package_script" ||
    die 'locked package unexpectedly changed: remote-lock marker present'
  ! grep -Fq 'Q200_REDIS01_ORCHESTRATOR_DB_ACCESS_DECISION' <<<"$package_script" ||
    die 'locked package unexpectedly changed: redis01 decision marker present'

  printf 'Q200_MYSQL_PACKAGE_INTEGRITY=PASS\n'
  printf 'Q200_MYSQL_PACKAGE_DEPENDENCY_ENFORCEMENT=GAP_Q192_Q193_Q194\n'
  printf 'Q200_MYSQL_PACKAGE_NTP_SOURCE_LOCK=GAP\n'
  printf 'Q200_MYSQL_PACKAGE_DNS_INPUT_LOCK=GAP\n'
  printf 'Q200_MYSQL_PACKAGE_ROUTE_ZONE_ASSERTION=GAP\n'
  printf 'Q200_MYSQL_PACKAGE_REMOTE_LOCK=GAP\n'
  printf 'Q200_MYSQL_REDIS01_ORCHESTRATOR_DB_ACCESS=UNRESOLVED\n'
  printf 'Q200_MYSQL_PACKAGE_EXECUTION_BLOCKED_NEEDS_CONTRACT_REVISION\n'
  return 1
}

get_value() {
  local file=$1 key=$2
  awk -F= -v wanted="$key" '$1==wanted {print substr($0,index($0,"=")+1)}' "$file"
}

require_value() {
  local file=$1 key=$2 value
  [[ "$(grep -Ec "^${key}=" "$file")" -eq 1 ]] ||
    die "input key missing or duplicated: $key"
  value=$(get_value "$file" "$key")
  [[ -n "$value" ]] || die "input value missing: $key"
  printf '%s' "$value"
}

valid_sha256() {
  [[ "$1" =~ ^[0-9a-f]{64}$ ]]
}

valid_ipv4() {
  awk -F. '
    NF!=4 {exit 1}
    {for(i=1;i<=4;i++) if($i!~/^[0-9]+$/ || $i<0 || $i>255) exit 1}
  ' <<<"$1"
}

valid_precise_cidr() {
  local address=${1%/*} prefix=${1#*/}
  [[ "$1" == */* && "$prefix" =~ ^(2[4-9]|3[0-2])$ ]] || return 1
  [[ "$address" != 0.0.0.0 ]] || return 1
  valid_ipv4 "$address"
}

validate_csv_hosts_or_ips() {
  local value=$1 item
  IFS=, read -r -a items <<<"$value"
  ((${#items[@]} > 0)) || return 1
  for item in "${items[@]}"; do
    [[ "$item" =~ ^[A-Za-z0-9._:-]+$ ]] || return 1
  done
}

validate_csv_ipv4() {
  local value=$1 item
  IFS=, read -r -a items <<<"$value"
  ((${#items[@]} > 0)) || return 1
  for item in "${items[@]}"; do valid_ipv4 "$item" || return 1; done
}

validate_inputs() {
  local file=$1 key value management ntp dns sha decision
  [[ "$(uname -s)" == Darwin ]] || die 'input validation is local-mac-only'
  [[ "$file" == /Users/* && -f "$file" && ! -L "$file" ]] ||
    die 'input file must be a regular local file below /Users'
  grep -Ev '^[[:space:]]*(#.*|$|[A-Z0-9_]+=[^[:cntrl:]]*)$' "$file" |
    grep -q . && die 'input file contains an unsafe line'

  for key in "${REQUIRED_KEYS[@]}"; do
    [[ "$(grep -Ec "^${key}=" "$file")" -eq 1 ]] ||
      die "input key missing or duplicated: $key"
    [[ -n "$(get_value "$file" "$key")" ]] ||
      die "input value missing: $key"
  done

  for key in Q200_DEP_Q122 Q200_DEP_Q192 Q200_DEP_Q193 Q200_DEP_Q194; do
    [[ "$(get_value "$file" "$key")" == PASS ]] || die "$key is not PASS"
  done
  [[ "$(get_value "$file" Q200_Q122_EVIDENCE_SHA256)" == "$EXPECTED_Q122_SHA256" ]] ||
    die 'Q-122 evidence SHA256 mismatch'
  for key in Q200_Q192_RECEIPT_SHA256 Q200_Q193_RECEIPT_SHA256 Q200_Q194_RECEIPT_SHA256 \
    Q200_FORWARD_MAPPING_SHA256; do
    sha=$(get_value "$file" "$key")
    valid_sha256 "$sha" || die "$key is not a lowercase SHA256"
  done

  management=$(get_value "$file" Q200_MANAGEMENT_CIDR)
  valid_precise_cidr "$management" || die 'management CIDR is invalid or too broad'
  ntp=$(get_value "$file" Q200_NTP_SOURCES)
  validate_csv_hosts_or_ips "$ntp" || die 'NTP source list is invalid'
  [[ "$(get_value "$file" Q200_NTP_TRANSPORT)" == udp/123 ]] ||
    die 'NTP transport must be udp/123'
  [[ "$(get_value "$file" Q200_MAX_CLOCK_OFFSET_MS)" =~ ^[1-9][0-9]*$ ]] ||
    die 'maximum clock offset must be a positive integer in milliseconds'
  dns=$(get_value "$file" Q200_DNS_SERVERS)
  validate_csv_ipv4 "$dns" || die 'DNS server list is invalid'
  [[ "$(get_value "$file" Q200_DNS_SEARCH_DOMAINS)" =~ ^([A-Za-z0-9.-]+|NONE)(,([A-Za-z0-9.-]+|NONE))*$ ]] ||
    die 'DNS search-domain list is invalid'
  [[ "$(get_value "$file" Q200_NAME_SOURCE_POLICY)" =~ ^(DNS|HOSTS|DNS_AND_HOSTS)$ ]] ||
    die 'name source policy is invalid'
  [[ "$(get_value "$file" Q200_REVERSE_DNS_POLICY)" =~ ^(REQUIRED|WAIVED_BY_NETWORK_OWNER)$ ]] ||
    die 'reverse DNS policy is invalid'
  valid_ipv4 "$(get_value "$file" Q200_ROUTE_208_TO_203_NEXT_HOP)" ||
    die '208-to-203 next hop is invalid'
  valid_ipv4 "$(get_value "$file" Q200_ROUTE_203_TO_208_NEXT_HOP)" ||
    die '203-to-208 next hop is invalid'
  [[ "$(get_value "$file" Q200_CROSS_SUBNET_ACL_APPROVED)" == PASS ]] ||
    die 'cross-subnet ACL is not approved'
  for key in Q200_MYSQL01_FIREWALL_ZONE Q200_MYSQL02_FIREWALL_ZONE; do
    [[ "$(get_value "$file" "$key")" =~ ^[A-Za-z0-9_.-]+$ ]] ||
      die "$key is invalid"
  done
  [[ "$(get_value "$file" Q200_FIREWALL_RUNTIME_PERMANENT_PARITY)" == PASS ]] ||
    die 'firewalld runtime/permanent parity is not confirmed'
  [[ "$(get_value "$file" Q200_PORT_MATRIX_SHA256)" == "$EXPECTED_MATRIX_SHA256" ]] ||
    die 'port matrix SHA256 mismatch'
  decision=$(get_value "$file" Q200_REDIS01_ORCHESTRATOR_DB_ACCESS_DECISION)
  [[ "$decision" =~ ^(REQUIRED|NOT_REQUIRED_APPROVED)$ ]] ||
    die 'redis01 Orchestrator DB-access decision is unresolved'
  [[ "$(get_value "$file" Q200_CHANGE_WINDOW_APPROVED)" == PASS ]] ||
    die 'change window is not approved'
  [[ "$(get_value "$file" Q200_OUT_OF_BAND_RECOVERY_CONFIRMED)" == PASS ]] ||
    die 'out-of-band recovery is not confirmed'
  [[ "$(get_value "$file" Q200_REMOTE_LOCK_GRANTED)" == 1 ]] ||
    die 'Q-200 remote lock is not granted'
  [[ "$(get_value "$file" Q200_REMOTE_LOCK_OWNER)" == MYSQL_Q200 ]] ||
    die 'Q-200 remote lock owner mismatch'

  printf 'Q200_MYSQL_EXTERNAL_INPUT_CONTRACT_PASS\n'
  printf 'Q200_MYSQL_PACKAGE_EXECUTION_BLOCKED_NEEDS_CONTRACT_REVISION\n'
}

REQUIRED_KEYS=(
  Q200_DEP_Q122
  Q200_Q122_EVIDENCE_SHA256
  Q200_DEP_Q192
  Q200_Q192_RECEIPT_SHA256
  Q200_DEP_Q193
  Q200_Q193_RECEIPT_SHA256
  Q200_DEP_Q194
  Q200_Q194_RECEIPT_SHA256
  Q200_MANAGEMENT_CIDR
  Q200_MANAGEMENT_CIDR_APPROVAL_ID
  Q200_NTP_SOURCES
  Q200_NTP_TRANSPORT
  Q200_MAX_CLOCK_OFFSET_MS
  Q200_DNS_SERVERS
  Q200_DNS_SEARCH_DOMAINS
  Q200_NAME_SOURCE_POLICY
  Q200_FORWARD_MAPPING_SHA256
  Q200_REVERSE_DNS_POLICY
  Q200_ROUTE_208_TO_203_NEXT_HOP
  Q200_ROUTE_203_TO_208_NEXT_HOP
  Q200_CROSS_SUBNET_ACL_APPROVED
  Q200_MYSQL01_FIREWALL_ZONE
  Q200_MYSQL02_FIREWALL_ZONE
  Q200_FIREWALL_RUNTIME_PERMANENT_PARITY
  Q200_PORT_MATRIX_SHA256
  Q200_PORT_MATRIX_APPROVAL_ID
  Q200_REDIS01_ORCHESTRATOR_DB_ACCESS_DECISION
  Q200_CHANGE_WINDOW_APPROVED
  Q200_NETWORK_ROLLBACK_OWNER
  Q200_OUT_OF_BAND_RECOVERY_CONFIRMED
  Q200_REMOTE_LOCK_GRANTED
  Q200_REMOTE_LOCK_OWNER
)

case "$MODE" in
  --static-validate)
    [[ $# -eq 1 ]] || { usage; exit 2; }
    static_validate
    ;;
  audit-package)
    [[ $# -eq 1 ]] || { usage; exit 2; }
    audit_package
    ;;
  validate-inputs)
    [[ $# -eq 2 ]] || { usage; exit 2; }
    validate_inputs "$2"
    ;;
  *)
    usage
    exit 2
    ;;
esac
