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

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURES="$HERE/fixtures/lot5"
MARKER=".soria-hardening-network-workloads"
DECISION="NETWORK_WORKLOAD_HARDENING_WITH_ACTIONS"

die(){ echo "ERROR: $*" >&2; exit 1; }
safe_root(){
  local r="${1:-}"
  [[ "$r" = /* ]] || die "root must be absolute"
  case "$r" in
    /tmp/soria-hardening-network-workloads-*|"$HOME"/soria-hardening-network-workloads-*) ;;
    *) die "unsafe root: $r" ;;
  esac
}
marked(){ [[ -f "$1/$MARKER" ]] || die "missing safety marker"; }
file_count(){ find "$1" -type f | wc -l | tr -d ' '; }
manifest(){
  (cd "$1"; find . -type f -print | sort | sed 's#^\./##' | while read -r f; do
    printf '%s,%s\n' "$f" "$(sha256sum "$f" | awk '{print $1}')"
  done)
}
check_fixture_count(){ [[ "$(file_count "$1")" = 20 ]] || die "expected exactly 20 fixtures"; }
check_markers(){
  grep -Fq 'SYNTHETIC — DO NOT IMPORT' "$1/opnsense/aliases.csv"
  grep -Fq 'SYNTHETIC — DO NOT IMPORT' "$1/opnsense/rules-before.csv"
  grep -Fq 'SYNTHETIC — NO SECRET VALUE' "$1/kubernetes/secret-references.csv"
  for asset in opnsense-fw-01 wg-admin-01 container-host-01 k3s-cluster-01; do
    grep -R -Fq "$asset" "$1" || die "missing asset $asset"
  done
}
check_flow_matrix(){
  awk -F, '
    NR==FNR { if(FNR>1) z[$1]=1; next }
    FNR==1 { next }
    {
      for(i=1;i<=11;i++) if($i=="") exit 2
      if($2=="any" || $3=="any" || !z[$2] || !z[$3]) exit 3
      if($4!="TCP" && $4!="UDP") exit 4
      if($5 !~ /^[0-9]+$/ || $5<1 || $5>65535) exit 5
      if($7=="" || $10=="" || $11=="") exit 6
      if($8!="true") exit 7
      n++
    }
    END { if(n<8) exit 8 }
  ' "$1/network/zones.csv" "$1/network/flow-matrix.csv"
}
check_management(){
  awk -F, 'NR>1{
    for(i=1;i<=9;i++) if($i=="") exit 2
    if($3!="WG_ADMIN" || $4!="true") exit 3
    if($1!="opnsense-fw-01" && $1!="app-srv-01" && $1!="container-host-01" && $1!="k3s-cluster-01") exit 4
  }' "$1/opnsense/management-policy.csv"
}
check_wireguard(){
  ! grep -R -Eqi 'private[ _-]?key|preshared|BEGIN [A-Z ]*PRIVATE' "$1/wireguard" || return 2
  awk -F, 'NR>1{
    for(i=1;i<=8;i++) if($i=="") exit 2
    if($3 !~ /^SYNTHETIC-PUBLIC-KEY-/) exit 3
    if($5=="active" && $7!="not-revoked") exit 4
    if($5=="revoked" && $7!="revoked") exit 5
  }' "$1/wireguard/peers.csv" || return 3
  awk -F, 'NR>1{
    for(i=1;i<=6;i++) if($i=="") exit 2
    if($3=="0.0.0.0/0" || $3=="::/0") exit 3
    k=$2 SUBSEP $3; if(seen[k]++) exit 4
    if($2=="PEER-OLD-01" && $6!="revoked") exit 5
  }' "$1/wireguard/routes.csv" || return 4
  awk -F, 'NR>1{
    if($3!="WG_ADMIN" || $4!="MGMT") exit 2
    if($2=="PEER-OLD-01" && ($5!="NONE" || $9!="revoked")) exit 3
  }' "$1/wireguard/access-policy.csv"
}
check_endpoints(){
  awk -F, 'NR>1{
    for(i=1;i<=12;i++) if($i=="") exit 2
    if($8=="false" && $6!="NONE") exit 3
    if($9=="unsupported" || $9=="unpatched" || $9=="unknown") exit 4
    if($6=="WAN" && !($1=="web-dmz-01" && $3=="HTTPS" && $5==443)) exit 5
    if($3=="SSH" && $6!="WG_ADMIN") exit 6
  }' "$1/endpoints/exposed-services.csv" || return 2
  awk -F, 'NR>1{
    for(i=1;i<=9;i++) if($i=="") exit 2
    if($6=="" || $7=="" || $8=="") exit 3
  }' "$1/endpoints/hardening-controls.csv"
}
check_images(){
  awk -F, 'NR>1{
    for(i=1;i<=9;i++) if($i=="") exit 2
    digest=$3
    if(length(digest) != 71) exit 3
    if(substr(digest,1,7) != "sha256:") exit 3
    hex=substr(digest,8)
    if(length(hex) != 64) exit 3
    if(hex !~ /^[0-9a-f]+$/) exit 3
    if($4=="latest" || $5=="unknown" || $6!="true") exit 4
  }' "$1/containers/images.csv"
}
check_runtime(){
  awk -F, 'NR>1{
    for(i=1;i<=17;i++) if($i=="") exit 2
    if($3!="false" || $4!="false" || $5!="true" || $6!="DROP" || $7!="true" || $8!="RuntimeDefault") exit 3
    if($9!="false" || $10!="false" || $11!="false" || $12!="false") exit 4
    if($13=="" || $14=="" || $16=="" || $17=="") exit 5
  }' "$1/containers/runtime-policy.csv" || return 2
  awk -F, 'NR>1{
    if($2!="MGMT" || $3!="true" || $5!="false" || $6!="explicit-read-only-only") exit 2
  }' "$1/containers/host-boundaries.csv"
}
check_kubernetes(){
  awk -F, 'NR>1{
    if($2!="restricted" || $3!="restricted" || $4!="restricted") exit 2
    if($5=="" || $7=="") exit 3
  }' "$1/kubernetes/namespaces.csv" || return 2
  awk -F, 'NR>1{
    for(i=1;i<=19;i++) if($i=="") exit 2
    if($4=="default" || $5!="true" || $6!="false" || $7!="false" || $8!="DROP" || $9!="RuntimeDefault" || $10!="true") exit 3
    if($11!="false" || $12!="false" || $13!="false" || $14!="false") exit 4
    if($15!="true" || $16!="true" || $18=="" || $19=="") exit 5
  }' "$1/kubernetes/workloads.csv"
}
check_network_policies(){
  awk -F, '
    NR==1{next}
    {
      ns[$2]=1
      if($3=="default-deny-ingress") di[$2]=1
      if($3=="default-deny-egress") de[$2]=1
      if($3=="allow-egress" && $5=="kube-dns" && $6=="UDP" && $7==53) dns[$2]=1
      if($1=="NP-WEB-API" && $5=="api-deployment" && $7==8443) webapi=1
      if($1=="NP-API-DB" && $5=="app-srv-01" && $7==5432) apidb=1
      if($9=="" || $10=="" || $11=="") exit 2
    }
    END{
      for(n in ns) if(!di[n] || !de[n] || !dns[n]) exit 3
      if(!webapi || !apidb) exit 4
    }
  ' "$1/kubernetes/network-policies.csv"
}
check_identities_secrets(){
  awk -F, 'NR>1{
    for(i=1;i<=8;i++) if($i=="") exit 2
    if($3!="true" || $4!="false" || $5=="" || $6=="") exit 3
  }' "$1/kubernetes/service-accounts.csv" || return 2
  awk -F, 'NR>1{
    for(i=1;i<=9;i++) if($i=="") exit 2
    if($5!="false" || $6!="true" || $7=="" || $8=="") exit 3
    if($9 !~ /NO SECRET VALUE/) exit 4
  }' "$1/kubernetes/secret-references.csv"
}
check_step(){
  local name="$1"
  shift
  local rc=0

  "$@" || rc=$?

  if (( rc != 0 )); then
    die "fixture validation failed: ${name} (status=${rc})"
  fi
}
check_all(){
  check_step fixture-count check_fixture_count "$1"
  check_step markers check_markers "$1"
  check_step flow-matrix check_flow_matrix "$1"
  check_step management check_management "$1"
  check_step wireguard check_wireguard "$1"
  check_step endpoints check_endpoints "$1"
  check_step images check_images "$1"
  check_step runtime check_runtime "$1"
  check_step kubernetes check_kubernetes "$1"
  check_step network-policies check_network_policies "$1"
  check_step identities-secrets check_identities_secrets "$1"
}
generate_policy(){
  local src="$1" out="$2"
  printf 'rule_id,sequence,interface,action,source_zone,destination_zone,protocol,port,log,owner,flow_id,notice\n' > "$out"
  awk -F, 'BEGIN{OFS=","} NR>1{
    seq=NR*100
    printf "TARGET-%03d,%d,%s,pass,%s,%s,%s,%s,true,%s,%s,SYNTHETIC — DO NOT IMPORT\n",NR-1,seq,$2,$2,$3,$4,$5,$7,$1
  }' "$src/network/flow-matrix.csv" >> "$out"
  awk -F, 'BEGIN{OFS=",";i=0} NR>1{
    i++; printf "DENY-%03d,%d,%s,block,%s,ANY,ANY,ANY,true,%s,default-deny,SYNTHETIC — DO NOT IMPORT\n",i,9000+i,$1,$1,$6
  }' "$src/network/zones.csv" >> "$out"
}
check_policy_order(){
  awk -F, '
    NR==1{next}
    {
      if($4=="pass"){
        if($5=="any" && $6=="any") exit 2
        if($9!="true" || $10=="" || $11=="") exit 3
        if($2>maxpass[$3]) maxpass[$3]=$2
      }
      if($4=="block" && $11=="default-deny") deny[$3]=$2
    }
    END{
      for(i in maxpass) if(!deny[i] || deny[i]<=maxpass[i]) exit 4
    }
  ' "$1"
}
write_reports(){
  local r="$1" s="$1/source" o="$1/output"
  awk -F, 'NR==1{print "zone_id,cidr,trust_level,owner";next}{print $1","$2","$3","$6}' "$s/network/zones.csv" > "$o/asset-zone-summary.csv"
  cp "$s/network/flow-matrix.csv" "$o/normalized-flow-matrix.csv"
  generate_policy "$s" "$o/opnsense-policy-target.csv"
  check_policy_order "$o/opnsense-policy-target.csv" || die "generated policy order invalid"
  awk -F, 'NR==1{print "rule_id,change,before,target";next}{print $1",REPLACE,legacy-policy,restrictive-policy"}' "$s/opnsense/rules-before.csv" > "$o/opnsense-delta.csv"
  awk -F, 'NR==1{print "rule_id,sequence,interface,action,result";next}{print $1","$2","$3","$4",PASS"}' "$o/opnsense-policy-target.csv" > "$o/opnsense-rule-order.csv"
  awk -F, 'NR==1{print "target_id,source_zone,via_tunnel,result";next}{print $1","$3","$4",PASS"}' "$s/opnsense/management-policy.csv" > "$o/wireguard-management-review.csv"
  awk -F, 'NR==1{print "asset_id,service,exposure,patch_state,result";next}{print $1","$3","$6","$9",PASS"}' "$s/endpoints/exposed-services.csv" > "$o/endpoint-hardening-review.csv"
  awk -F, 'NR==1{print "workload_id,image_id,privileged,run_as_root,result";next}{print $1","$2","$4","$3",PASS"}' "$s/containers/runtime-policy.csv" > "$o/container-hardening-review.csv"
  awk -F, 'NR==1{print "workload_id,namespace,service_account,result";next}{print $1","$2","$4",PASS"}' "$s/kubernetes/workloads.csv" > "$o/kubernetes-hardening-review.csv"
  awk -F, 'NR==1{print "policy_id,namespace,type,result";next}{print $1","$2","$3",PASS"}' "$s/kubernetes/network-policies.csv" > "$o/network-policy-review.csv"
  cat > "$o/positive-test-report.csv" <<'EOF'
test_id,result,evidence
TEST-PUBLIC-HTTPS,PASS,WAN-to-DMZ-HTTPS-preserved
TEST-WG-MGMT,PASS,management-source-is-WG_ADMIN
TEST-CONTAINER,PASS,unprivileged-runtime-policy
TEST-K8S-NETPOL,PASS,default-deny-and-explicit-flows
EOF
  cat > "$o/functional-test-report.csv" <<'EOF'
test_id,result,evidence
TEST-PUBLIC-HTTPS,PASS,web-dmz-01-HTTPS
TEST-DNS,PASS,internal-DNS-flow
TEST-APP-FLOW,PASS,DMZ-to-application
TEST-WG-MGMT,PASS,WireGuard-management-path
TEST-K8S-DNS,PASS,workload-DNS-egress
TEST-K8S-APP,PASS,web-api-database-flows
TEST-CONTAINER,PASS,non-root-read-only-runtime
TEST-WG-REVOKE,PASS,revoked-peer-has-no-active-access
EOF
  cat > "$o/exceptions.csv" <<'EOF'
exception_id,scope,reason,owner,status
EX-NET-001,real-opnsense-policy,no-authorized-firewall-execution,team-network,OPEN
EX-WG-001,real-wireguard-tunnel,no-real-keys-or-endpoint,team-security,OPEN
EX-K8S-001,real-admission-and-network-policy,no-authorized-cluster-execution,team-platform,OPEN
EOF
}
negative_case(){
  local id="$1" reason="$2" report="$3"
  printf '%s,%s,%s,BLOCKED\n' "$id" "$reason" "$reason" >> "$report"
}
run_negatives(){
  local r="$1" s="$1/source" n="$1/negative" report="$1/output/negative-test-report.csv" d
  rm -rf "$n"; mkdir -p "$n"
  echo 'case_id,expected_reason,observed_reason,result' > "$report"

  d="$n/NEG-01"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$2="any"} {print}' "$d/network/flow-matrix.csv" > "$d/x"; mv "$d/x" "$d/network/flow-matrix.csv"
  if check_flow_matrix "$d"; then die "NEG-01 accepted"; else negative_case NEG-01 flow-scope-or-required-field-invalid "$report"; fi

  d="$n/NEG-02"; cp -R "$s" "$d"; chmod -R u+w "$d"; generate_policy "$d" "$d/policy.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $4=="pass"&&$3=="WAN"{$2=9500} {print}' "$d/policy.csv" > "$d/x"; mv "$d/x" "$d/policy.csv"
  if check_policy_order "$d/policy.csv"; then die "NEG-02 accepted"; else negative_case NEG-02 firewall-rule-order-invalid "$report"; fi

  d="$n/NEG-03"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$3="USERS";$4="false"} {print}' "$d/opnsense/management-policy.csv" > "$d/x"; mv "$d/x" "$d/opnsense/management-policy.csv"
  if check_management "$d"; then die "NEG-03 accepted"; else negative_case NEG-03 management-outside-wireguard "$report"; fi

  d="$n/NEG-04"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$3="PRIVATE-KEY-DEMO-NOT-ALLOWED"} {print}' "$d/wireguard/peers.csv" > "$d/x"; mv "$d/x" "$d/wireguard/peers.csv"
  echo 'WG-ROUTE-X,PEER-ADMIN-01,0.0.0.0/0,too-broad,team-security,active' >> "$d/wireguard/routes.csv"
  if check_wireguard "$d"; then die "NEG-04 accepted"; else negative_case NEG-04 wireguard-key-route-or-revocation-invalid "$report"; fi

  d="$n/NEG-05"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$8="false";$6="WAN";$9="unsupported"} {print}' "$d/endpoints/exposed-services.csv" > "$d/x"; mv "$d/x" "$d/endpoints/exposed-services.csv"
  if check_endpoints "$d"; then die "NEG-05 accepted"; else negative_case NEG-05 endpoint-exposure-or-patch-invalid "$report"; fi

  d="$n/NEG-06"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$3="";$4="latest";$5="unknown";$6="false"} {print}' "$d/containers/images.csv" > "$d/x"; mv "$d/x" "$d/containers/images.csv"
  if check_images "$d"; then die "NEG-06 accepted"; else negative_case NEG-06 image-mutability-or-provenance-invalid "$report"; fi

  d="$n/NEG-07"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$3="true";$4="true";$6="SYS_ADMIN";$9="true";$12="true"} {print}' "$d/containers/runtime-policy.csv" > "$d/x"; mv "$d/x" "$d/containers/runtime-policy.csv"
  if check_runtime "$d"; then die "NEG-07 accepted"; else negative_case NEG-07 container-privilege-or-host-boundary-invalid "$report"; fi

  d="$n/NEG-08"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$2="privileged"} {print}' "$d/kubernetes/namespaces.csv" > "$d/x"; mv "$d/x" "$d/kubernetes/namespaces.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$5="false";$7="true";$11="true"} {print}' "$d/kubernetes/workloads.csv" > "$d/x"; mv "$d/x" "$d/kubernetes/workloads.csv"
  if check_kubernetes "$d"; then die "NEG-08 accepted"; else negative_case NEG-08 kubernetes-pod-security-invalid "$report"; fi

  d="$n/NEG-09"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'NR==1 || $1!="NP-API-DENY-EG"' "$d/kubernetes/network-policies.csv" > "$d/x"; mv "$d/x" "$d/kubernetes/network-policies.csv"
  if check_network_policies "$d"; then die "NEG-09 accepted"; else negative_case NEG-09 network-policy-default-deny-or-function-invalid "$report"; fi

  d="$n/NEG-10"; cp -R "$s" "$d"; chmod -R u+w "$d"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$4="true"} {print}' "$d/kubernetes/service-accounts.csv" > "$d/x"; mv "$d/x" "$d/kubernetes/service-accounts.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} NR==2{$5="true";$8=""} {print}' "$d/kubernetes/secret-references.csv" > "$d/x"; mv "$d/x" "$d/kubernetes/secret-references.csv"
  if check_identities_secrets "$d"; then die "NEG-10 accepted"; else negative_case NEG-10 secret-service-account-or-rollback-invalid "$report"; fi

  [[ "$(awk -F, 'NR>1&&$4=="BLOCKED"{n++} END{print n+0}' "$report")" = 10 ]] || die "expected ten blocked negative cases"
}
setup(){
  safe_root "$1"; [[ ! -e "$1" ]] || die "root already exists"
  check_all "$FIXTURES"
  mkdir -p "$1"/{source,work,rollback,negative,output}
  printf '%s\n' "$DECISION" > "$1/$MARKER"
  cp -R "$FIXTURES"/. "$1/source"
  manifest "$FIXTURES" > "$1/output/source-manifest.csv"
  chmod -R a-w "$1/source"
  printf 'state=SETUP\nfixtures=20\n' > "$1/output/status.txt"
  echo "Lot 5 setup complete: $1"
}
status(){
  safe_root "$1"; marked "$1"
  echo "root=$1"
  echo "fixtures=$(file_count "$1/source")"
  if [[ -f "$1/output/final-decision.txt" ]]; then echo "decision=$(cat "$1/output/final-decision.txt")"; else echo "decision=NOT_RUN"; fi
}
run(){
  safe_root "$1"; marked "$1"; [[ ! -d "$1/work/before" ]] || die "already run"
  check_all "$1/source"
  cp -R "$1/source" "$1/work/before"
  cp -R "$1/source" "$1/work/target"
  chmod -R u+w "$1/work"
  write_reports "$1"
  run_negatives "$1"
  manifest "$1/work/before" > "$1/output/rollback-manifest-before.csv"
  mkdir -p "$1/rollback/restored"
  cp -R "$1/work/before"/. "$1/rollback/restored"
  manifest "$1/rollback/restored" > "$1/output/rollback-manifest-restored.csv"
  cmp -s "$1/output/rollback-manifest-before.csv" "$1/output/rollback-manifest-restored.csv" || die "rollback manifest mismatch"
  printf 'result=PASS\nverification=manifest-match\n' > "$1/output/rollback-report.txt"
  printf '%s\n' "$DECISION" > "$1/output/final-decision.txt"
  cat > "$1/output/validation-summary.txt" <<EOF
decision=$DECISION
fixtures=20
negative_cases=10
negative_blocked=10
rollback=PASS
opnsense_execution=NOT_RUN
wireguard_execution=NOT_RUN
endpoint_change=NOT_RUN
container_runtime_execution=NOT_RUN
kubernetes_execution=NOT_RUN
EOF
  cat > "$1/output/proof-boundaries.md" <<'EOF'
# Lot 5 proof boundaries
Established: fixture integrity, normalized flows, synthetic rule order, represented WireGuard management path, endpoint/container/Kubernetes control declarations, ten blocked negative cases, functional-flow representation and synthetic rollback.
Not established: real OPNsense state, WireGuard tunnel or revocation, endpoint state, image signature or scan, container runtime behavior, Pod Security Admission, NetworkPolicy enforcement, Secret protection, cluster/cloud state or real rollback.
EOF
  echo "Lot 5 run complete: $DECISION"
}
validate(){
  safe_root "$1"; marked "$1"; check_all "$1/source"
  cmp -s "$1/output/source-manifest.csv" <(manifest "$FIXTURES") || die "source fixtures changed"
  check_policy_order "$1/output/opnsense-policy-target.csv" || die "policy order invalid"
  [[ "$(awk -F, 'NR>1&&$4=="BLOCKED"{n++} END{print n+0}' "$1/output/negative-test-report.csv")" = 10 ]] || die "negative count"
  cmp -s "$1/output/rollback-manifest-before.csv" "$1/output/rollback-manifest-restored.csv" || die "rollback mismatch"
  [[ "$(wc -l < "$1/output/opnsense-delta.csv")" -gt 1 ]] || die "empty delta"
  grep -Fxq "$DECISION" "$1/output/final-decision.txt"
  grep -Fq 'opnsense_execution=NOT_RUN' "$1/output/validation-summary.txt"
  grep -Fq 'kubernetes_execution=NOT_RUN' "$1/output/validation-summary.txt"
  echo "Lot 5 validation PASS: $DECISION"
}
reset(){
  safe_root "$1"; marked "$1"
  chmod -R u+w "$1" 2>/dev/null || true
  rm -rf "$1"
  [[ ! -e "$1" ]] || die "reset failed"
  echo "Lot 5 reset complete: $1"
}
case "${1:-}" in
  setup) setup "${2:-}" ;;
  status) status "${2:-}" ;;
  run) run "${2:-}" ;;
  validate) validate "${2:-}" ;;
  reset) reset "${2:-}" ;;
  *) echo "Usage: $0 {setup|status|run|validate|reset} <absolute-root>" >&2; exit 2 ;;
esac
