///| Independent replay auditor. The expected layout is derived from the spec;

///| no planner routines or plan-provided expected state participate.

///|
fn da_origin_key(origin : DeliveryOrigin) -> String {
  origin.root.to_json().stringify() + ":" + origin.path.to_json().stringify()
}

///|
fn da_parents(path : String) -> Array[String] {
  let parts = path.split("/").map(p => p.to_owned()).collect()
  let parents : Array[String] = []
  let mut prefix = ""
  for i in 0..<(parts.length() - 1) {
    prefix = if i == 0 { parts[i] } else { prefix + "/" + parts[i] }
    parents.push(prefix)
  }
  parents
}

///|
fn da_structure(path : String) -> Bool {
  if path == "" || path.has_prefix("/") || path.has_prefix("\\") {
    return false
  }
  let leading = path.iter().take(2).collect()
  if leading.length() == 2 &&
    leading[1] == ':' &&
    (
      (leading[0] >= 'A' && leading[0] <= 'Z') ||
      (leading[0] >= 'a' && leading[0] <= 'z')
    ) {
    return false
  }
  for c in path.iter() {
    if c == '\\' || c.to_int() == 0 {
      return false
    }
  }
  for p in path.split("/") {
    if p == "" || p == "." || p == ".." {
      return false
    }
  }
  true
}

///|
fn da_portable(path : String) -> Bool {
  let diagnostics : Array[Diagnostic] = []
  ignore(check_path(path, 1, diagnostics))
  diagnostics.is_empty()
}

///|
fn da_entries(
  state : Map[String, DeliveryStateEntry],
) -> Array[DeliveryStateEntry] {
  let result : Array[DeliveryStateEntry] = []
  for key in sorted_keys(state) {
    result.push(state[key])
  }
  result.sort_by((a, b) => ordinal_compare(a.path, b.path))
  result
}

///|
fn da_reject(
  code : String,
  path : String,
  message : String,
  step : Int,
  state : Map[String, DeliveryStateEntry],
  trace : Array[DeliveryTrace],
) -> DeliveryAudit {
  {
    status: if code == "SNAPSHOT_INCOMPLETE" {
      "incomplete"
    } else {
      "invalid"
    },
    valid: false,
    first_failed_step: step,
    diagnostics: [{ code, path, message, step, }],
    final_entries: da_entries(state),
    trace,
  }
}

///|
fn da_empty_origin() -> DeliveryOrigin {
  { root: "", path: "", }
}

///|
pub fn audit_delivery_plan(
  spec : DeliverySpec,
  plan : DeliveryPlan,
) -> DeliveryAudit {
  let state : Map[String, DeliveryStateEntry] = Map([])
  let trace : Array[DeliveryTrace] = []
  let roots : Map[String, Map[String, DeliveryStateEntry]] = Map([])
  let children_by_root : Map[String, Map[String, Array[DeliveryStateEntry]]] = Map([],
  )
  let all = spec.sources.copy()
  all.push(spec.target)
  for tree in all {
    if tree.id == "" || roots.contains(tree.id) {
      return da_reject(
        "ROOT_ID",
        tree.id,
        "Root IDs must be nonempty and unique.",
        -1,
        state,
        trace,
      )
    }
    if !tree.snapshot.complete ||
      !tree.snapshot.scan_issues.is_empty() ||
      !tree.snapshot.scope.is_empty() {
      return da_reject(
        "SNAPSHOT_INCOMPLETE",
        tree.id,
        "Every root requires a complete, unscoped snapshot without scan issues.",
        -1,
        state,
        trace,
      )
    }
    if tree.id == spec.target.id &&
      tree.snapshot.profile != "portable-windows-v1" {
      return da_reject(
        "TARGET_PROFILE_UNSUPPORTED",
        tree.id,
        "Delivery targets require portable-windows-v1.",
        -1,
        state,
        trace,
      )
    }
    let entries : Map[String, DeliveryStateEntry] = Map([])
    let explicit : Map[String, Bool] = Map([])
    for entry in tree.snapshot.entries {
      if !da_structure(entry.path) || explicit.contains(entry.path) {
        return da_reject(
          "SOURCE_STRUCTURE",
          entry.path,
          "Snapshot entries must have unique, structurally valid relative paths.",
          -1,
          state,
          trace,
        )
      }
      explicit[entry.path] = true
      match entries.get(entry.path) {
        Some(old) if old.kind != entry.kind =>
          return da_reject(
            "SOURCE_STRUCTURE",
            entry.path,
            "A source file cannot also be a directory.",
            -1,
            state,
            trace,
          )
        _ => ()
      }
      entries[entry.path] = {
        path: entry.path,
        kind: entry.kind,
        origin: if entry.kind == File {
          { root: tree.id, path: entry.path, }
        } else {
          da_empty_origin()
        },
      }
      for parent in da_parents(entry.path) {
        match entries.get(parent) {
          Some(old) if old.kind == File =>
            return da_reject(
              "SOURCE_STRUCTURE", parent, "A source file cannot be an ancestor.",
              -1, state, trace,
            )
          _ =>
            entries[parent] = {
              path: parent,
              kind: Directory,
              origin: da_empty_origin(),
            }
        }
      }
    }
    roots[tree.id] = entries
    let children : Map[String, Array[DeliveryStateEntry]] = Map([])
    for entry in entries.values() {
      let parent = match entry.path.rev_split_once("/") {
        Some((prefix, _)) => prefix.to_owned()
        None => ""
      }
      match children.get(parent) {
        Some(items) => items.push(entry)
        None => children[parent] = [entry]
      }
    }
    children_by_root[tree.id] = children
  }
  if !audit(spec.target.snapshot.entries).diagnostics.is_empty() {
    return da_reject(
      "TARGET_UNREPRESENTABLE", "", "Initial target tree is not portable under ASCII case folding.",
      -1, state, trace,
    )
  }
  let initial = roots[spec.target.id]
  let expected : Map[String, DeliveryStateEntry] = Map([])
  for entry in initial.values() {
    let slot = ascii_lower(entry.path)
    state[slot] = entry
    expected[slot] = entry
  }
  let intents : Map[String, DeliveryIntent] = Map([])
  let move_goals : Map[String, String] = Map([])
  let directory_goals : Map[String, DeliveryStateEntry] = Map([])
  // Expand each mapping from initial source identities, never from replay state.
  for mapping in spec.mappings {
    if mapping.mode != "copy" && mapping.mode != "move" {
      return da_reject(
        "MAPPING_MODE",
        mapping.to,
        "Mapping mode must be copy or move.",
        -1,
        state,
        trace,
      )
    }
    guard roots.get(mapping.source) is Some(source) else {
      return da_reject(
        "MAPPING_SOURCE",
        mapping.from,
        "Mapping references an unknown source root.",
        -1,
        state,
        trace,
      )
    }
    if (mapping.to != "" && !da_portable(mapping.to)) ||
      (mapping.from != "" && !da_structure(mapping.from)) {
      return da_reject(
        "MAPPING_PATH",
        mapping.to,
        "Mapping paths are not structurally valid or the destination is not portable.",
        -1,
        state,
        trace,
      )
    }
    let source_kind = if mapping.from == "" {
      Some(Directory)
    } else {
      source.get(mapping.from).map(e => e.kind)
    }
    guard source_kind is Some(kind) else {
      return da_reject(
        "MAPPING_SOURCE",
        mapping.from,
        "Mapping source does not exist with exact spelling in the initial snapshot.",
        -1,
        state,
        trace,
      )
    }
    if mapping.mode == "move" &&
      (mapping.source != spec.target.id || kind != File) {
      return da_reject(
        "MAPPING_MOVE",
        mapping.from,
        "Move requires an initial target file; external roots and directories are read only.",
        -1,
        state,
        trace,
      )
    }
    let expanded : Array[(DeliveryStateEntry, String)] = []
    if kind == File {
      expanded.push((source[mapping.from], mapping.to))
    } else {
      if mapping.to != "" {
        match directory_goals.get(ascii_lower(mapping.to)) {
          Some(old) if old.path != mapping.to =>
            return da_reject(
              "TARGET_UNREPRESENTABLE",
              mapping.to,
              "Mapped directories have inconsistent ASCII casing.",
              -1,
              state,
              trace,
            )
          _ => ()
        }
        directory_goals[ascii_lower(mapping.to)] = {
          path: mapping.to,
          kind: Directory,
          origin: da_empty_origin(),
        }
      }
      let prefix = if mapping.from == "" { "" } else { mapping.from + "/" }
      let queue : Array[String] = [mapping.from]
      let children = children_by_root[mapping.source]
      let mut cursor = 0
      while cursor < queue.length() {
        let directory = queue[cursor]
        cursor = cursor + 1
        match children.get(directory) {
          Some(items) =>
            for entry in items {
              let relative = entry.path[prefix.length():].to_owned()
              expanded.push(
                (
                  entry,
                  if mapping.to == "" {
                    relative
                  } else {
                    mapping.to + "/" + relative
                  },
                ),
              )
              if entry.kind == Directory {
                queue.push(entry.path)
              }
            }
          None => ()
        }
      }
    }
    for pair in expanded {
      let (entry, destination) = pair
      if !da_portable(destination) {
        return da_reject(
          "TARGET_UNREPRESENTABLE", destination, "Expanded destination is not portable.",
          -1, state, trace,
        )
      }
      let slot = ascii_lower(destination)
      if entry.kind == Directory {
        match directory_goals.get(slot) {
          Some(old) if old.path != destination =>
            return da_reject(
              "TARGET_UNREPRESENTABLE", destination, "Destination directories have inconsistent ASCII casing.",
              -1, state, trace,
            )
          _ => ()
        }
        directory_goals[slot] = {
          path: destination,
          kind: Directory,
          origin: da_empty_origin(),
        }
      } else {
        if intents.contains(slot) {
          return da_reject(
            "MAPPING_COLLISION", destination, "Multiple file intents occupy the same destination slot.",
            -1, state, trace,
          )
        }
        intents[slot] = {
          mode: mapping.mode,
          source: mapping.source,
          from: entry.path,
          to: destination,
          origin: entry.origin,
        }
        if mapping.mode == "move" {
          let identity = da_origin_key(entry.origin)
          if move_goals.contains(identity) {
            return da_reject(
              "MAPPING_MOVE",
              entry.path,
              "Each initial target file can have at most one Move intent.",
              -1,
              state,
              trace,
            )
          }
          move_goals[identity] = slot
        }
      }
    }
  }
  // Remove every Move origin before adding goals, so files may become directories.
  for intent in intents.values() {
    if intent.mode == "move" {
      expected.remove(ascii_lower(intent.from))
    }
  }
  for intent in intents.values() {
    let slot = ascii_lower(intent.to)
    if expected.contains(slot) {
      return da_reject(
        "MAPPING_OCCUPIED",
        intent.to,
        "Destination is occupied by an initial entry without a vacating Move.",
        -1,
        state,
        trace,
      )
    }
    expected[slot] = { path: intent.to, kind: File, origin: intent.origin, }
    for parent in da_parents(intent.to) {
      match directory_goals.get(ascii_lower(parent)) {
        Some(old) if old.path != parent =>
          return da_reject(
            "TARGET_UNREPRESENTABLE", parent, "expected directory casing is inconsistent.",
            -1, state, trace,
          )
        _ =>
          directory_goals[ascii_lower(parent)] = {
            path: parent,
            kind: Directory,
            origin: da_empty_origin(),
          }
      }
    }
  }
  let extra_directories = directory_goals.values().collect()
  for entry in extra_directories {
    for parent in da_parents(entry.path) {
      match directory_goals.get(ascii_lower(parent)) {
        Some(old) if old.path != parent =>
          return da_reject(
            "TARGET_UNREPRESENTABLE", parent, "expected directory casing is inconsistent.",
            -1, state, trace,
          )
        _ =>
          directory_goals[ascii_lower(parent)] = {
            path: parent,
            kind: Directory,
            origin: da_empty_origin(),
          }
      }
    }
  }
  for entry in directory_goals.values() {
    let slot = ascii_lower(entry.path)
    match expected.get(slot) {
      Some(old) if old.kind != Directory || old.path != entry.path =>
        return da_reject(
          "TARGET_UNREPRESENTABLE",
          entry.path,
          "A expected directory collides with a file or a differently cased directory.",
          -1,
          state,
          trace,
        )
      _ => expected[slot] = entry
    }
  }
  let final_paths : Array[PathEntry] = da_entries(expected).map(e => {
    path: e.path,
    kind: e.kind,
  })
  if !audit(final_paths).diagnostics.is_empty() {
    return da_reject(
      "TARGET_UNREPRESENTABLE", "", "expected target tree contains incompatible path prefixes.",
      -1, state, trace,
    )
  }
  if plan.status != "ready" || !plan.diagnostics.is_empty() {
    return da_reject(
      "PLAN_NOT_READY", "", "Only a ready plan without blocking diagnostics can be audited as acceptable.",
      -1, state, trace,
    )
  }
  if plan.basis != render_delivery_spec_json(spec) {
    return da_reject(
      "BASIS_MISMATCH", "", "Plan basis must exactly match the canonical input spec.",
      -1, state, trace,
    )
  }
  let temps : Map[String, Bool] = Map([])
  for path in plan.temporary_paths {
    let slot = ascii_lower(path)
    if !da_portable(path) ||
      path.contains("/") ||
      temps.contains(slot) ||
      state.contains(slot) ||
      expected.contains(slot) {
      return da_reject(
        "TEMPORARY_CONFLICT", path, "Temporary paths must be unique portable root-level slots absent from initial and expected trees.",
        -1, state, trace,
      )
    }
    temps[slot] = true
  }
  for step, op in plan.operations {
    let slot = ascii_lower(op.to)
    if !da_portable(op.to) {
      return da_reject(
        "OP_PATH",
        op.to,
        "Operation destination is not portable.",
        step,
        state,
        trace,
      )
    }
    if state.contains(slot) {
      return da_reject(
        "DESTINATION_OCCUPIED",
        op.to,
        "Operation destination is already occupied.",
        step,
        state,
        trace,
      )
    }
    for parent in da_parents(op.to) {
      match state.get(ascii_lower(parent)) {
        Some(e) if e.kind == Directory && e.path == parent => ()
        _ =>
          return da_reject(
            "PARENT_MISSING", parent, "Operation requires an existing directory with correct casing.",
            step, state, trace,
          )
      }
    }
    if op.kind == "mkdir" {
      if op.source != "" || op.from != "" || op.origin != da_empty_origin() {
        return da_reject(
          "MKDIR_FIELDS",
          op.to,
          "Mkdir must have empty source, from and origin fields.",
          step,
          state,
          trace,
        )
      }
      match expected.get(slot) {
        Some(e) if e.kind == Directory && e.path == op.to => state[slot] = e
        _ =>
          return da_reject(
            "UNRELATED_MKDIR",
            op.to,
            "Mkdir is not required by the expected layout.",
            step,
            state,
            trace,
          )
      }
    } else {
      if op.kind != "copy" && op.kind != "move" {
        return da_reject(
          "OP_KIND",
          op.to,
          "Operation kind must be mkdir, copy or move.",
          step,
          state,
          trace,
        )
      }
      let actual = if op.source == spec.target.id {
        state.get(ascii_lower(op.from))
      } else {
        match roots.get(op.source) {
          Some(tree) => tree.get(op.from)
          None => None
        }
      }
      guard actual is Some(entry) &&
        entry.kind == File &&
        entry.path == op.from &&
        entry.origin == op.origin else {
        return da_reject(
          "SOURCE_IDENTITY",
          op.from,
          "Source must currently hold the exact claimed initial file identity.",
          step,
          state,
          trace,
        )
      }
      if op.kind == "copy" {
        match intents.get(slot) {
          Some(intent) if intent.mode == "copy" &&
            intent.origin == op.origin &&
            intent.to == op.to => ()
          _ =>
            return da_reject(
              "UNRELATED_COPY",
              op.to,
              "Copy must deliver a declared Copy identity to its exact destination.",
              step,
              state,
              trace,
            )
        }
      } else {
        if op.source != spec.target.id {
          return da_reject(
            "READONLY_SOURCE",
            op.from,
            "External source roots cannot be changed by Move.",
            step,
            state,
            trace,
          )
        }
        guard move_goals.get(da_origin_key(op.origin)) is Some(goal) else {
          return da_reject(
            "UNRELATED_MOVE",
            op.from,
            "Move identity has no declared target Move intent.",
            step,
            state,
            trace,
          )
        }
        if !temps.contains(slot) &&
          (slot != goal || expected[goal].path != op.to) {
          return da_reject(
            "UNRELATED_MOVE",
            op.to,
            "Move destination must be its declared expected goal or a declared temporary slot.",
            step,
            state,
            trace,
          )
        }
        state.remove(ascii_lower(op.from))
      }
      state[slot] = { path: op.to, kind: File, origin: op.origin, }
    }
    trace.push({
      step,
      kind: op.kind,
      from: op.from,
      to: op.to,
      origin: op.origin,
    })
  }
  for slot in temps.keys() {
    if state.contains(slot) {
      return da_reject(
        "TEMPORARY_LEFTOVER",
        state[slot].path,
        "Temporary file remains after replay.",
        -1,
        state,
        trace,
      )
    }
  }
  if state.length() != expected.length() {
    return da_reject(
      "FINAL_LAYOUT", "", "expected entry count differs from the independently derived target layout.",
      -1, state, trace,
    )
  }
  for slot, expected in expected {
    if state.get(slot) != Some(expected) {
      return da_reject(
        "FINAL_IDENTITY",
        expected.path,
        "expected path, kind or initial file identity differs from the independently derived target layout.",
        -1,
        state,
        trace,
      )
    }
  }
  {
    status: "valid",
    valid: true,
    first_failed_step: -1,
    diagnostics: [],
    final_entries: da_entries(state),
    trace,
  }
}