///|
pub(all) struct NativeStubFallbackLockdownRule {
  file : String
  required_includes : Array[String]
  required_tokens : Array[String]
  forbidden_tokens : Array[String]
  expected_ffi_export_count : Int
  expected_set_error_call_count : Int
  expected_helper_call_count : Int
} derive(Eq, Debug)

///|
pub(all) struct NativeStubFallbackLockdownFact {
  file : String
  includes : Array[String]
  present_tokens : Array[String]
  present_forbidden_tokens : Array[String]
  ffi_export_count : Int
  set_error_call_count : Int
  helper_call_count : Int
} derive(Eq, Debug)

///|
pub(all) struct NativeStubFallbackLockdownPolicyReport {
  rule_count : Int
  actual_file_count : Int
  missing_file_count : Int
  extra_file_count : Int
  required_include_count : Int
  include_exact_set_mismatch_count : Int
  required_token_count : Int
  missing_required_token_count : Int
  forbidden_token_count : Int
  present_forbidden_token_count : Int
  ffi_export_count_mismatch_count : Int
  set_error_call_count_mismatch_count : Int
  helper_call_count_mismatch_count : Int
  duplicate_file_fact_count : Int
  replaces_file_reading : Bool
  replaces_token_extraction : Bool
  live_io_evidence : Bool
} derive(Eq, Debug)

///|
pub fn NativeStubFallbackLockdownPolicyReport::passes(
  self : NativeStubFallbackLockdownPolicyReport,
) -> Bool {
  self.rule_count > 0 &&
  self.actual_file_count == self.rule_count &&
  self.missing_file_count == 0 &&
  self.extra_file_count == 0 &&
  self.required_include_count > 0 &&
  self.include_exact_set_mismatch_count == 0 &&
  self.required_token_count > 0 &&
  self.missing_required_token_count == 0 &&
  self.forbidden_token_count > 0 &&
  self.present_forbidden_token_count == 0 &&
  self.ffi_export_count_mismatch_count == 0 &&
  self.set_error_call_count_mismatch_count == 0 &&
  self.helper_call_count_mismatch_count == 0 &&
  self.duplicate_file_fact_count == 0 &&
  !self.replaces_file_reading &&
  !self.replaces_token_extraction &&
  !self.live_io_evidence
}

///|
pub fn native_stub_fallback_lockdown_rules() -> Array[
  NativeStubFallbackLockdownRule,
] {
  [
    fallback_lockdown_rule(
      "stub_npcap_unsupported.c",
      ["stub_common.h"],
      ["#if !defined(_WIN32)", "MOONBIT_FFI_EXPORT"],
      [
        "stub_npcap_windows.h", "stub_npcap_windows_[A-Za-z0-9_]+.c", "#include ",
        "PCAP_OPENFLAG_PROMISCUOUS", "lockwire_native_npcap_ensure", "lockwire_native_npcap_findalldevs",
        "lockwire_native_npcap_freealldevs", "lockwire_native_npcap_live_ensure",
        "lockwire_native_npcap_open_live_handle", "lockwire_native_npcap_close_handle",
        "lockwire_native_npcap_sendpacket_handle", "lockwire_native_npcap_next_ex_handle",
        "lockwire_native_npcap_geterr_handle", "lockwire_native_npcap_compile_filter_handle",
        "lockwire_native_npcap_setfilter_handle", "lockwire_native_npcap_freecode_handle",
        "lockwire_native_npcap_live_handle_get", "lockwire_native_npcap_live_handle_alloc",
        "lockwire_native_npcap_live_handle_release", "lockwire_native_npcap_copy_moonbit_bytes_to_c_string",
      ],
      9,
      9,
      0,
    ),
    fallback_lockdown_rule(
      "stub_raw_socket_unsupported.c",
      ["stub_common.h"],
      [
        "#define LOCKWIRE_RAW_SOCKET_UNSUPPORTED_ERROR (-90)", "static void raw_socket_unsupported_error(void)",
      ],
      [
        "stub_raw_socket_linux.h", "stub_raw_socket_linux_[A-Za-z0-9_]+.c", "#include ", "#include ", "#include ",
        "#include ", "#include ", "AF_PACKET", "SOCK_RAW", "ETH_P_",
        "sockaddr_ll", "if_nametoindex(", "socket(", "bind(", "setsockopt(", "send(",
        "recv(", "poll(", "close(",
      ],
      6,
      1,
      6,
    ),
    fallback_lockdown_rule(
      "stub_udp_unsupported.c",
      ["stub_common.h"],
      [
        "#define LOCKWIRE_UDP_UNSUPPORTED_ERROR (-20)", "static void udp_unsupported_error(void)",
        "#if !defined(_WIN32)", "!defined(__linux__)", "!defined(__APPLE__)", "!defined(__FreeBSD__)",
      ],
      [
        "stub_udp_windows.h", "stub_udp_posix.h", "stub_udp_[A-Za-z0-9_]+.c", "#include ",
        "#include ", "#include ", "#include ",
        "AF_INET", "SOCK_DGRAM", "sockaddr_in", "WSAStartup", "LoadLibraryA(", "GetProcAddress(",
        "socket(", "bind(", "setsockopt(", "sendto(", "recvfrom(", "getsockname(",
        "close(", "closesocket(",
      ],
      5,
      1,
      5,
    ),
  ]
}

///|
pub fn native_stub_expected_fallback_lockdown_facts() -> Array[
  NativeStubFallbackLockdownFact,
] {
  let facts : Array[NativeStubFallbackLockdownFact] = []
  for rule in native_stub_fallback_lockdown_rules() {
    facts.push({
      file: rule.file,
      includes: rule.required_includes,
      present_tokens: rule.required_tokens,
      present_forbidden_tokens: [],
      ffi_export_count: rule.expected_ffi_export_count,
      set_error_call_count: rule.expected_set_error_call_count,
      helper_call_count: rule.expected_helper_call_count,
    })
  }
  facts
}

///|
pub fn native_stub_fallback_lockdown_policy_report(
  actual : Array[NativeStubFallbackLockdownFact],
) -> NativeStubFallbackLockdownPolicyReport {
  native_stub_fallback_lockdown_policy_report_from_rules(
    native_stub_fallback_lockdown_rules(),
    actual,
  )
}

///|
pub fn native_stub_fallback_lockdown_policy_report_from_rules(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> NativeStubFallbackLockdownPolicyReport {
  {
    rule_count: rules.length(),
    actual_file_count: count_actual_fallback_lockdown_files(actual),
    missing_file_count: count_missing_fallback_lockdown_files(rules, actual),
    extra_file_count: count_extra_fallback_lockdown_files(rules, actual),
    required_include_count: count_required_fallback_lockdown_includes(rules),
    include_exact_set_mismatch_count: count_fallback_lockdown_include_mismatches(
      rules, actual,
    ),
    required_token_count: count_required_fallback_lockdown_tokens(rules),
    missing_required_token_count: count_missing_fallback_lockdown_tokens(
      rules, actual,
    ),
    forbidden_token_count: count_forbidden_fallback_lockdown_tokens(rules),
    present_forbidden_token_count: count_present_forbidden_fallback_lockdown_tokens(
      rules, actual,
    ),
    ffi_export_count_mismatch_count: count_fallback_lockdown_export_mismatches(
      rules, actual,
    ),
    set_error_call_count_mismatch_count: count_fallback_lockdown_set_error_mismatches(
      rules, actual,
    ),
    helper_call_count_mismatch_count: count_fallback_lockdown_helper_mismatches(
      rules, actual,
    ),
    duplicate_file_fact_count: count_duplicate_fallback_lockdown_file_facts(
      actual,
    ),
    replaces_file_reading: false,
    replaces_token_extraction: false,
    live_io_evidence: false,
  }
}

///|
fn fallback_lockdown_rule(
  file : String,
  required_includes : Array[String],
  required_tokens : Array[String],
  forbidden_tokens : Array[String],
  expected_ffi_export_count : Int,
  expected_set_error_call_count : Int,
  expected_helper_call_count : Int,
) -> NativeStubFallbackLockdownRule {
  {
    file,
    required_includes,
    required_tokens,
    forbidden_tokens,
    expected_ffi_export_count,
    expected_set_error_call_count,
    expected_helper_call_count,
  }
}

///|
fn count_actual_fallback_lockdown_files(
  facts : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let files : Array[String] = []
  for fact in facts {
    if !fallback_lockdown_contains_string(files, fact.file) {
      files.push(fact.file)
    }
  }
  files.length()
}

///|
fn count_missing_fallback_lockdown_files(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    if !fallback_lockdown_contains_file(actual, rule.file) {
      count += 1
    }
  }
  count
}

///|
fn count_extra_fallback_lockdown_files(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let seen : Array[String] = []
  let mut count = 0
  for fact in actual {
    if !fallback_lockdown_contains_string(seen, fact.file) {
      seen.push(fact.file)
      if !fallback_lockdown_contains_rule_file(rules, fact.file) {
        count += 1
      }
    }
  }
  count
}

///|
fn count_required_fallback_lockdown_includes(
  rules : Array[NativeStubFallbackLockdownRule],
) -> Int {
  let mut count = 0
  for rule in rules {
    count += rule.required_includes.length()
  }
  count
}

///|
fn count_fallback_lockdown_include_mismatches(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for fact in actual {
      if fact.file == rule.file &&
        !fallback_lockdown_same_set(rule.required_includes, fact.includes) {
        count += 1
      }
    }
  }
  count
}

///|
fn count_required_fallback_lockdown_tokens(
  rules : Array[NativeStubFallbackLockdownRule],
) -> Int {
  let mut count = 0
  for rule in rules {
    count += rule.required_tokens.length()
  }
  count
}

///|
fn count_missing_fallback_lockdown_tokens(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for token in rule.required_tokens {
      if !fallback_lockdown_contains_present_token(actual, rule.file, token) {
        count += 1
      }
    }
  }
  count
}

///|
fn count_forbidden_fallback_lockdown_tokens(
  rules : Array[NativeStubFallbackLockdownRule],
) -> Int {
  let mut count = 0
  for rule in rules {
    count += rule.forbidden_tokens.length()
  }
  count
}

///|
fn count_present_forbidden_fallback_lockdown_tokens(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for fact in actual {
      if fact.file == rule.file {
        for token in fact.present_forbidden_tokens {
          if fallback_lockdown_contains_string(rule.forbidden_tokens, token) {
            count += 1
          }
        }
      }
    }
  }
  count
}

///|
fn count_fallback_lockdown_export_mismatches(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for fact in actual {
      if fact.file == rule.file &&
        fact.ffi_export_count != rule.expected_ffi_export_count {
        count += 1
      }
    }
  }
  count
}

///|
fn count_fallback_lockdown_set_error_mismatches(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for fact in actual {
      if fact.file == rule.file &&
        fact.set_error_call_count != rule.expected_set_error_call_count {
        count += 1
      }
    }
  }
  count
}

///|
fn count_fallback_lockdown_helper_mismatches(
  rules : Array[NativeStubFallbackLockdownRule],
  actual : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let mut count = 0
  for rule in rules {
    for fact in actual {
      if fact.file == rule.file &&
        fact.helper_call_count != rule.expected_helper_call_count {
        count += 1
      }
    }
  }
  count
}

///|
fn count_duplicate_fallback_lockdown_file_facts(
  facts : Array[NativeStubFallbackLockdownFact],
) -> Int {
  let seen : Array[String] = []
  let mut count = 0
  for fact in facts {
    if fallback_lockdown_contains_string(seen, fact.file) {
      count += 1
    } else {
      seen.push(fact.file)
    }
  }
  count
}

///|
fn fallback_lockdown_contains_rule_file(
  rules : Array[NativeStubFallbackLockdownRule],
  file : String,
) -> Bool {
  for rule in rules {
    if rule.file == file {
      return true
    }
  }
  false
}

///|
fn fallback_lockdown_contains_file(
  facts : Array[NativeStubFallbackLockdownFact],
  file : String,
) -> Bool {
  for fact in facts {
    if fact.file == file {
      return true
    }
  }
  false
}

///|
fn fallback_lockdown_contains_present_token(
  facts : Array[NativeStubFallbackLockdownFact],
  file : String,
  token : String,
) -> Bool {
  for fact in facts {
    if fact.file == file &&
      fallback_lockdown_contains_string(fact.present_tokens, token) {
      return true
    }
  }
  false
}

///|
fn fallback_lockdown_same_set(
  expected : Array[String],
  actual : Array[String],
) -> Bool {
  if expected.length() != actual.length() {
    return false
  }
  for item in expected {
    if !fallback_lockdown_contains_string(actual, item) {
      return false
    }
  }
  true
}

///|
fn fallback_lockdown_contains_string(
  items : Array[String],
  item : String,
) -> Bool {
  for got in items {
    if got == item {
      return true
    }
  }
  false
}