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

MODE="${1:-}"
ROOT="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURE_DIR="$SCRIPT_DIR/fixtures/lot1"
MARKER='.soria-ir-telemetry-timeline'
DECISION='TELEMETRY_READY_WITH_ACTIONS'

fail(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"; }

safe_root(){
  [[ -n "$ROOT" ]] || return 1
  case "$ROOT" in
    /tmp/soria-ir-*|"$HOME"/soria-ir-*) return 0 ;;
    *) return 1 ;;
  esac
}

require_safe_root(){ safe_root || fail "unsafe lab root: ${ROOT:-<empty>}"; }
require_marker(){ [[ -f "$ROOT/$MARKER" ]] || fail "missing marker: $ROOT/$MARKER"; }

fixture_count(){ find "$1" -type f | wc -l | tr -d ' '; }

build_manifest(){
  local tree="$1" out="$2"
  (
    cd "$tree"
    find . -type f -print | LC_ALL=C sort | while IFS= read -r file; do
      sha256sum "$file"
    done
  ) > "$out"
}

verify_source_integrity(){
  [[ -f "$ROOT/.source-manifest.sha256" ]] || return 1
  (
    cd "$ROOT/source"
    sha256sum -c "$ROOT/.source-manifest.sha256" >/dev/null 2>&1
  )
}

lookup_source_type(){
  local source_id="$1"
  awk -F, -v id="$source_id" 'NR>1 && $1==id {print $2; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/sources/source-catalog.csv"
}

source_identity_required(){
  local source_id="$1"
  awk -F, -v id="$source_id" 'NR>1 && $1==id {print $5; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/sources/source-catalog.csv"
}

clock_limit(){
  local source_type="$1"
  awk -F, -v t="$source_type" 'NR>1 && $2==t {print $3; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/quality/clock-policy.csv"
}

known_asset(){ grep -q "^${1}," "$ROOT/source/assets/assets.csv"; }
known_identity(){ grep -q "^${1}," "$ROOT/source/identities/identities.csv"; }
known_source(){ grep -q "^${1}," "$ROOT/source/sources/source-catalog.csv"; }
valid_timestamp(){ [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; }

check_event_line(){
  local line="$1"
  local event_id timestamp source_id asset_id identity_id event_kind outcome severity detail skew synthetic source_type identity_required max_skew
  IFS=, read -r event_id timestamp source_id asset_id identity_id event_kind outcome severity detail skew synthetic <<< "$line"

  [[ -n "$event_id" && -n "$timestamp" && -n "$source_id" && -n "$asset_id" && -n "$event_kind" && -n "$outcome" && -n "$severity" && -n "$detail" && -n "$skew" && -n "$synthetic" ]] || return 1
  valid_timestamp "$timestamp" || return 1
  known_source "$source_id" || return 1
  known_asset "$asset_id" || return 1
  source_type="$(lookup_source_type "$source_id")" || return 1
  identity_required="$(source_identity_required "$source_id")" || return 1
  if [[ "$identity_required" == yes ]]; then
    [[ -n "$identity_id" && "$identity_id" != '-' ]] || return 1
    known_identity "$identity_id" || return 1
  elif [[ -n "$identity_id" && "$identity_id" != '-' ]]; then
    known_identity "$identity_id" || return 1
  fi
  [[ "$skew" =~ ^[0-9]+$ ]] || return 1
  max_skew="$(clock_limit "$source_type")" || return 1
  (( skew <= max_skew )) || return 1
  [[ "$synthetic" == SYNTHETIC ]] || return 1
  return 0
}

all_event_files(){
  printf '%s\n' \
    "$ROOT/source/events/linux-auth.csv" \
    "$ROOT/source/events/windows-security.csv" \
    "$ROOT/source/events/network-sensor.csv" \
    "$ROOT/source/events/application.csv"
}

setup_lab(){
  require_safe_root
  [[ ! -e "$ROOT" ]] || fail "root already exists: $ROOT"
  [[ -d "$FIXTURE_DIR" ]] || fail "missing fixture directory: $FIXTURE_DIR"
  [[ "$(fixture_count "$FIXTURE_DIR")" -eq 12 ]] || fail 'reference fixture bundle must contain exactly 12 files'

  mkdir -p "$ROOT/source" "$ROOT/output" "$ROOT/work"
  printf 'SORIA_IR_TELEMETRY_TIMELINE\n' > "$ROOT/$MARKER"
  cp -R "$FIXTURE_DIR"/. "$ROOT/source/"
  [[ "$(fixture_count "$ROOT/source")" -eq 12 ]] || fail 'copied fixture count mismatch'
  build_manifest "$ROOT/source" "$ROOT/.source-manifest.sha256"
  verify_source_integrity || fail 'source integrity failed immediately after setup'
  printf 'Setup OK: 12 synthetic telemetry fixtures copied and integrity manifest created.\n'
}

status_lab(){
  require_safe_root
  require_marker
  local count integrity outputs decision
  count="$(fixture_count "$ROOT/source")"
  if verify_source_integrity; then integrity=OK; else integrity=FAILED; fi
  if [[ -s "$ROOT/output/normalized-events.csv" ]]; then outputs=PRESENT; else outputs=ABSENT; fi
  if [[ -s "$ROOT/output/decision.txt" ]]; then decision="$(cat "$ROOT/output/decision.txt")"; else decision=NOT_RUN; fi
  printf 'marker=OK\nfixtures=%s\nintegrity=%s\noutputs=%s\ndecision=%s\n' "$count" "$integrity" "$outputs" "$decision"
}

write_coverage(){
  local out="$ROOT/output/source-coverage.csv"
  printf 'requirement_id,purpose,required_source_type,result,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/sources/coverage-requirements.csv" | while IFS=, read -r req purpose source_type expected synthetic; do
    local result
    if awk -F, -v t="$source_type" 'NR>1 && $2==t {found=1} END{exit(found?0:1)}' "$ROOT/source/sources/source-catalog.csv"; then result=COVERED; else result=GAP; fi
    printf '%s,%s,%s,%s,%s\n' "$req" "$purpose" "$source_type" "$result" "$synthetic" >> "$out"
  done
}

write_quality_and_normalized(){
  local q="$ROOT/output/quality-report.csv" n="$ROOT/output/normalized-events.csv" combined="$ROOT/work/all-events.csv"
  printf 'event_id,result,reason,synthetic\n' > "$q"
  printf 'event_id,timestamp,source_id,source_type,asset_id,identity_id,event_kind,outcome,severity,detail,synthetic\n' > "$n"
  : > "$combined"

  all_event_files | while IFS= read -r file; do
    tail -n +2 "$file"
  done > "$combined"

  local duplicates
  duplicates="$(awk -F, '{c[$1]++} END{for(id in c) if(c[id]>1) print id}' "$combined")"
  [[ -z "$duplicates" ]] || fail "duplicate event IDs in positive corpus: $duplicates"

  local failures=0
  while IFS= read -r line; do
    [[ -n "$line" ]] || continue
    local event_id timestamp source_id asset_id identity_id event_kind outcome severity detail skew synthetic source_type
    IFS=, read -r event_id timestamp source_id asset_id identity_id event_kind outcome severity detail skew synthetic <<< "$line"
    if check_event_line "$line"; then
      source_type="$(lookup_source_type "$source_id")"
      printf '%s,PASS,mandatory-quality-checks-passed,%s\n' "$event_id" "$synthetic" >> "$q"
      printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \
        "$event_id" "$timestamp" "$source_id" "$source_type" "$asset_id" "$identity_id" "$event_kind" "$outcome" "$severity" "$detail" "$synthetic" >> "$n"
    else
      printf '%s,FAIL,mandatory-quality-check-failed,%s\n' "$event_id" "${synthetic:-UNKNOWN}" >> "$q"
      failures=$((failures+1))
    fi
  done < "$combined"
  (( failures == 0 )) || fail "positive corpus contains $failures invalid event(s)"
}

write_timeline(){
  local n="$ROOT/output/normalized-events.csv" t="$ROOT/output/timeline.csv"
  head -n 1 "$n" > "$t"
  tail -n +2 "$n" | LC_ALL=C sort -t, -k2,2 -k1,1 >> "$t"
}

criterion_value(){
  local class="$1" column="$2"
  awk -F, -v c="$class" -v col="$column" 'NR>1 && $1==c {print $col; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/classification/criteria.csv"
}

write_classification(){
  local out="$ROOT/output/classification.csv"
  local inc_conf inc_src alert_conf alert_src signal_conf signal_src
  inc_conf="$(criterion_value INCIDENT 2)"; inc_src="$(criterion_value INCIDENT 3)"
  alert_conf="$(criterion_value ALERT 2)"; alert_src="$(criterion_value ALERT 3)"
  signal_conf="$(criterion_value SIGNAL 2)"; signal_src="$(criterion_value SIGNAL 3)"
  printf 'candidate_id,event_refs,source_count,confidence,impact,compromise,classification,reason,synthetic\n' > "$out"

  tail -n +2 "$ROOT/source/classification/candidates.csv" | while IFS=, read -r id refs sources confidence impact compromise reason synthetic; do
    local class
    if [[ "$compromise" == yes ]] && (( confidence >= inc_conf && sources >= inc_src )); then
      class=INCIDENT
    elif (( confidence >= alert_conf && sources >= alert_src )); then
      class=ALERT
    elif (( confidence >= signal_conf && sources >= signal_src )); then
      class=SIGNAL
    else
      class=EVENT
    fi
    printf '%s,%s,%s,%s,%s,%s,%s,%s,%s\n' "$id" "$refs" "$sources" "$confidence" "$impact" "$compromise" "$class" "$reason" "$synthetic" >> "$out"
  done
}

record_negative(){
  local family="$1" status="$2"
  printf '%s,%s,SYNTHETIC\n' "$family" "$status" >> "$ROOT/output/negative-results.csv"
}

run_negative_tests(){
  local out="$ROOT/output/negative-results.csv" neg="$ROOT/work/negative"
  rm -rf "$neg"; mkdir -p "$neg"
  printf 'family,status,synthetic\n' > "$out"

  local base='NEG-BASE,2026-08-07T10:00:00Z,SRC-WIN,AST-IR-001,ID-ADMIN,auth_failure,FAILURE,4,negative-test,10,SYNTHETIC'
  local line

  line='NEG-001,,SRC-WIN,AST-IR-001,ID-ADMIN,auth_failure,FAILURE,4,missing-timestamp,10,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative missing timestamp unexpectedly passed'; else record_negative missing-timestamp BLOCKED; fi

  line='NEG-002,2026/08/07-10:00:00,SRC-WIN,AST-IR-001,ID-ADMIN,auth_failure,FAILURE,4,malformed-timestamp,10,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative malformed timestamp unexpectedly passed'; else record_negative malformed-timestamp BLOCKED; fi

  line='NEG-003,2026-08-07T10:00:00Z,SRC-WIN,AST-IR-001,ID-ADMIN,auth_failure,FAILURE,4,clock-skew,9999,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative clock skew unexpectedly passed'; else record_negative clock-skew-outside-tolerance BLOCKED; fi

  line='NEG-004,2026-08-07T10:00:00Z,SRC-NOPE,AST-IR-001,ID-ADMIN,auth_failure,FAILURE,4,unknown-source,10,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative unknown source unexpectedly passed'; else record_negative unknown-source BLOCKED; fi

  line='NEG-005,2026-08-07T10:00:00Z,SRC-WIN,AST-NOPE,ID-ADMIN,auth_failure,FAILURE,4,unknown-asset,10,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative unknown asset unexpectedly passed'; else record_negative unknown-asset BLOCKED; fi

  line='NEG-006,2026-08-07T10:00:00Z,SRC-WIN,AST-IR-001,,auth_failure,FAILURE,4,missing-identity,10,SYNTHETIC'
  if check_event_line "$line"; then fail 'negative missing identity unexpectedly passed'; else record_negative missing-required-identity BLOCKED; fi

  printf '%s\n%s\n' "$base" "$base" > "$neg/duplicate.csv"
  if awk -F, '{c[$1]++} END{bad=0; for(id in c) if(c[id]>1) bad=1; exit(bad?0:1)}' "$neg/duplicate.csv"; then
    record_negative duplicate-event-id BLOCKED
  else
    fail 'negative duplicate event ID was not detected'
  fi

  cp -R "$ROOT/source" "$neg/tampered-source"
  printf '\nSYNTHETIC-TAMPER\n' >> "$neg/tampered-source/events/application.csv"
  if (
    cd "$neg/tampered-source"
    sha256sum -c "$ROOT/.source-manifest.sha256" >/dev/null 2>&1
  ); then
    fail 'negative provenance tamper unexpectedly passed'
  else
    record_negative provenance-sha256-mismatch BLOCKED
  fi
}

write_proof_boundaries(){
  cat > "$ROOT/output/proof-boundaries.md" <<'BOUNDARY'
# SYNTHETIC Lot 1 proof boundaries

This lab proves deterministic handling of a synthetic local telemetry corpus only.

It does not prove live Windows or Linux audit configuration, real NTP synchronization, live log completeness or retention, Wazuh/SIEM ingestion, Suricata packet visibility, real identity or asset authenticity, real detection effectiveness, existence of a real incident, forensic admissibility, legal chain of custody, containment, eradication or recovery.

Derived normalization and timeline files never replace the synthetic originals. The source SHA-256 manifest remains the reference for integrity inside this exercise.
BOUNDARY
}

run_lab(){
  require_safe_root
  require_marker
  need awk; need sort; need sha256sum; need grep; need sed; need find
  [[ "$(fixture_count "$ROOT/source")" -eq 12 ]] || fail 'source fixture count must remain 12'
  verify_source_integrity || fail 'source integrity mismatch before run'

  rm -rf "$ROOT/output" "$ROOT/work"
  mkdir -p "$ROOT/output" "$ROOT/work"
  cp "$ROOT/.source-manifest.sha256" "$ROOT/output/source-integrity.sha256"
  write_coverage
  write_quality_and_normalized
  write_timeline
  write_classification
  run_negative_tests
  write_proof_boundaries
  printf '%s\n' "$DECISION" > "$ROOT/output/decision.txt"
  printf 'Run OK: telemetry quality, normalization, timeline and qualification generated.\n'
}

validate_lab(){
  require_safe_root
  require_marker
  [[ "$(fixture_count "$ROOT/source")" -eq 12 ]] || fail 'expected exactly 12 source fixtures'
  verify_source_integrity || fail 'source integrity mismatch'

  local required=(
    source-coverage.csv quality-report.csv normalized-events.csv timeline.csv classification.csv
    negative-results.csv source-integrity.sha256 proof-boundaries.md decision.txt
  )
  local file
  for file in "${required[@]}"; do [[ -s "$ROOT/output/$file" ]] || fail "missing output: $file"; done

  cmp -s "$ROOT/.source-manifest.sha256" "$ROOT/output/source-integrity.sha256" || fail 'output integrity manifest differs from setup manifest'
  ! grep -Fq ',FAIL,' "$ROOT/output/quality-report.csv" || fail 'positive quality report contains FAIL'

  local normalized_rows timeline_rows
  normalized_rows=$(( $(wc -l < "$ROOT/output/normalized-events.csv") - 1 ))
  timeline_rows=$(( $(wc -l < "$ROOT/output/timeline.csv") - 1 ))
  (( normalized_rows > 0 && normalized_rows == timeline_rows )) || fail 'timeline/normalized row count mismatch'

  {
    head -n 1 "$ROOT/output/normalized-events.csv"
    tail -n +2 "$ROOT/output/normalized-events.csv" | LC_ALL=C sort -t, -k2,2 -k1,1
  } > "$ROOT/work/expected-timeline.csv"
  cmp -s "$ROOT/work/expected-timeline.csv" "$ROOT/output/timeline.csv" || fail 'timeline is not deterministic chronological order'

  local source_ids="$ROOT/work/source-event-ids.txt"
  all_event_files | while IFS= read -r f; do tail -n +2 "$f" | cut -d, -f1; done | LC_ALL=C sort > "$source_ids"
  tail -n +2 "$ROOT/output/normalized-events.csv" | cut -d, -f1 | LC_ALL=C sort > "$ROOT/work/normalized-event-ids.txt"
  cmp -s "$source_ids" "$ROOT/work/normalized-event-ids.txt" || fail 'normalized event IDs do not trace exactly to source event IDs'

  for file in EVENT SIGNAL ALERT INCIDENT; do
    grep -Fq ",$file," "$ROOT/output/classification.csv" || fail "missing classification: $file"
  done

  [[ $(( $(wc -l < "$ROOT/output/negative-results.csv") - 1 )) -eq 8 ]] || fail 'expected exactly eight negative families'
  [[ "$(grep -c ',BLOCKED,SYNTHETIC$' "$ROOT/output/negative-results.csv")" -eq 8 ]] || fail 'all negative families must be BLOCKED'

  grep -Fq 'TEL-005,endpoint-process-execution,endpoint-edr,GAP,SYNTHETIC' "$ROOT/output/source-coverage.csv" || fail 'intentional telemetry coverage gap missing'
  [[ "$(cat "$ROOT/output/decision.txt")" == "$DECISION" ]] || fail 'unexpected reference decision'
  grep -Fq 'does not prove' "$ROOT/output/proof-boundaries.md" || fail 'proof boundary statement missing'
  grep -Fq 'real incident' "$ROOT/output/proof-boundaries.md" || fail 'real-incident proof boundary missing'

  printf 'Validate OK: 12 fixtures, %s events, four classes and eight blocked negatives. Decision=%s\n' "$normalized_rows" "$DECISION"
}

reset_lab(){
  require_safe_root
  require_marker
  rm -rf -- "$ROOT"
  [[ ! -e "$ROOT" ]] || fail 'reset failed'
  printf 'Reset OK: lab root removed.\n'
}

case "$MODE" in
  setup) setup_lab ;;
  status) status_lab ;;
  run) run_lab ;;
  validate) validate_lab ;;
  reset) reset_lab ;;
  *) fail 'usage: module1-telemetry-timeline-qualification.sh {setup|status|run|validate|reset} ROOT' ;;
esac
