///|
priv struct AnalysisTrace {
  event : RouteEvent
  from_us : UInt64
  state : String
  source : Source?
  left_censored : Bool
}

///|
priv struct MoasTrace {
  from_us : UInt64
  signature : String
  origins : Array[UInt32]
  evidence : Array[Json]
  unknown_paths : Int32
  left_censored : Bool
}

///|
fn gap_scope(gap : InvestigationGap, source : Source?) -> Bool {
  gap.collector is None ||
  source.map(fn(s) { (s.collector, s.view) }) == Some((gap.collector, gap.view))
}

///|
fn analysis_interval(
  trace : AnalysisTrace,
  until : UInt64,
  source : Source?,
  rules : InvestigationRules,
  right_censored : Bool,
) -> InvestigationInterval? {
  let start = if trace.from_us < rules.from_us {
    rules.from_us
  } else {
    trace.from_us
  }
  let end = if until > rules.to_us { rules.to_us } else { until }
  if end <= start {
    return None
  }
  Some({
    key: snapshot_route_key(trace.event),
    prefix: trace.event.prefix,
    peer: scoped_peer_key(
      trace.event.peer_asn,
      trace.event.peer_ip,
      trace.event.outbound,
      trace.event.source,
    ),
    path_id: trace.event.path_id,
    peer_asn: trace.event.peer_asn,
    peer_ip: trace.event.peer_ip,
    outbound: trace.event.outbound,
    collector: trace.event.source.bind(fn(source) { source.collector }),
    view: trace.event.source.bind(fn(source) { source.view }),
    from_us: start,
    to_us: end,
    state: trace.state,
    duration_us: if rules.continuous.unwrap_or(false) &&
      ["active", "withdrawn"].contains(trace.state) {
      Some(end - start)
    } else {
      None
    },
    left_censored: trace.left_censored || trace.from_us < rules.from_us,
    right_censored,
    start_source: trace.source,
    end_source: source,
    origin_as: if trace.state == "active" {
      origin_as(trace.event.attributes.path)
    } else {
      None
    },
    path_length: if trace.state == "active" {
      Some(path_length(trace.event.attributes.path))
    } else {
      None
    },
  })
}

///|
fn moas_now(
  analyzer : Analyzer,
  prefix : String,
  time : UInt64,
  left_censored : Bool,
) -> MoasTrace {
  let origins : Map[UInt32, Unit] = Map([])
  let evidence = []
  let keys = []
  let mut unknown = 0
  for key, route in analyzer.routes {
    if !route.active || route.event.prefix != prefix {
      continue
    }
    match origin_as(route.shared.wire.path) {
      Some(asn) => {
        origins[asn] = ()
        keys.push(key)
        evidence.push(
          (
            key,
            Json::object({
              "origin_as": Json(asn),
              "path_id": analysis_optional(route.event.path_id),
              "peer": Json(
                scoped_peer_key(
                  route.event.peer_asn,
                  route.event.peer_ip,
                  route.event.outbound,
                  route.event.source,
                ),
              ),
              "source": analysis_optional(route.event.source),
              "observed_at_us": Json(route.time),
              "time_precision": Json(
                if route.event.microseconds is Some(_) {
                  "microsecond"
                } else {
                  "second"
                },
              ),
            }),
          ),
        )
      }
      None => unknown += 1
    }
  }
  let ordered = origins.keys().to_array()
  ordered.sort()
  keys.sort()
  evidence.sort_by_key(fn(pair) { pair.0 })
  {
    from_us: time,
    signature: Json((ordered, keys, unknown)).stringify(),
    origins: ordered,
    evidence: evidence.map(fn(pair) { pair.1 }),
    unknown_paths: unknown,
    left_censored,
  }
}

///|
fn moas_close(
  prefix : String,
  trace : MoasTrace,
  until : UInt64,
  rules : InvestigationRules,
  right_censored : Bool,
) -> Json? {
  let end = if until > rules.to_us { rules.to_us } else { until }
  if trace.origins.length() < 2 || end <= trace.from_us {
    return None
  }
  Some(
    Json::object({
      "prefix": Json(prefix),
      "id": Json("moas|" + prefix + "|" + trace.from_us.to_string()),
      "from_us": Json(trace.from_us),
      "to_us": Json(end),
      "origins": Json(trace.origins),
      "evidence": Json(trace.evidence),
      "unknown_paths": Json(trace.unknown_paths),
      "duration_us": if rules.continuous.unwrap_or(false) {
        Json(end - trace.from_us)
      } else {
        Json::null()
      },
      "left_censored": Json(trace.left_censored),
      "right_censored": Json(right_censored),
      "interpretation": Json("simultaneous_observed_origins_not_hijack_verdict"),
    }),
  )
}

///|
fn analysis_findings(
  event : InvestigationEvent,
  rules : InvestigationRules,
) -> Array[Json] raise ParseError {
  let result = []
  let after = match event.after {
    Some(s) if s.active => s
    _ => return result
  }
  let target = parse_prefix_text(event.prefix)
  let actual = origin_as(after.attributes.path)
  for focus in rules.focus.unwrap_or([]) {
    let parent = parse_prefix_text(focus.prefix)
    if !parent.contains(target) {
      continue
    }
    let expected = focus.expected_origins.unwrap_or([])
    let more = target.length > parent.length
    let allowed = focus.allowed_more_specifics
      .unwrap_or([])
      .any(text => parse_prefix_text(text) == target)
    let condition = if actual is None {
      "origin_unknown"
    } else if expected.is_empty() {
      "expectation_unconfigured"
    } else if more && allowed {
      "allowed_more_specific"
    } else if actual.map(fn(asn) { expected.contains(asn) }).unwrap_or(false) {
      if more {
        "same_origin_more_specific"
      } else {
        "expected_origin"
      }
    } else {
      "unexpected_origin"
    }
    result.push(
      Json::object({
        "id": Json(event.id + "|" + parent.text() + "|" + condition),
        "event_id": Json(event.id),
        "rule_version": Json(rules.version),
        "condition": Json(condition),
        "parent_prefix": Json(parent.text()),
        "prefix": Json(event.prefix),
        "expected_origins": Json(expected),
        "actual_origin": analysis_optional(actual),
        "more_specific": Json(more),
        "peer": Json(event.peer),
        "source": analysis_optional(event.source),
        "quality": Json(if actual is None { "unknown" } else { "observed" }),
        "interpretation": Json("investigation_clue"),
      }),
    )
  }
  result
}

///|
/// 从活动区间检查更具体路由,包含窗口内从未更新的基线路由。
fn investigation_specifics(
  intervals : Array[InvestigationInterval],
  rules : InvestigationRules,
) -> Array[Json] raise ParseError {
  let rows = []
  let mut bytes = 0
  for interval in intervals {
    if interval.state != "active" {
      continue
    }
    let target = parse_prefix_text(interval.prefix)
    for focus in rules.focus.unwrap_or([]) {
      let parent = parse_prefix_text(focus.prefix)
      if parent.length >= target.length || !parent.contains(target) {
        continue
      }
      let expected = focus.expected_origins.unwrap_or([])
      let allowed = focus.allowed_more_specifics
        .unwrap_or([])
        .any(text => parse_prefix_text(text) == target)
      let condition = if interval.origin_as is None {
        "origin_unknown"
      } else if expected.is_empty() {
        "expectation_unconfigured"
      } else if allowed {
        "allowed_more_specific"
      } else if interval.origin_as
        .map(fn(asn) { expected.contains(asn) })
        .unwrap_or(false) {
        "same_origin_more_specific"
      } else {
        "unexpected_origin"
      }
      rows.push(
        Json::object({
          "id": Json(
            interval.key +
            "|" +
            interval.from_us.to_string() +
            "|" +
            parent.text(),
          ),
          "prefix": Json(interval.prefix),
          "parent_prefix": Json(parent.text()),
          "peer": Json(interval.peer),
          "peer_asn": Json(interval.peer_asn),
          "peer_ip": Json(interval.peer_ip),
          "outbound": Json(interval.outbound),
          "collector": analysis_optional(interval.collector),
          "view": analysis_optional(interval.view),
          "path_id": analysis_optional(interval.path_id),
          "from_us": Json(interval.from_us),
          "to_us": Json(interval.to_us),
          "duration_us": analysis_optional(interval.duration_us),
          "condition": Json(condition),
          "expected_origins": Json(expected),
          "actual_origin": analysis_optional(interval.origin_as),
          "quality": Json(
            if interval.duration_us is Some(_) {
              "continuous_observation"
            } else {
              "continuity_unverified"
            },
          ),
          "rule_version": Json(rules.version),
          "start_source": analysis_optional(interval.start_source),
          "end_source": analysis_optional(interval.end_source),
          "interpretation": Json("active_more_specific_observation"),
        }),
      )
      let size = rows[rows.length() - 1].stringify().length() * 4
      if rows.length() > rules.capacity.unwrap_or(20000) * 8 ||
        size > rules.max_bytes.unwrap_or(67108864) - bytes {
        raise Invalid(0, "更具体区间超过派生结果预算")
      }
      bytes += size
    }
  }
  rows
}

///|
priv struct IncidentTrace {
  first_us : UInt64
  mut last_us : UInt64
  prefix : String
  peer : String
  path_id : UInt32?
  reminders : Array[String]
  event_ids : Array[String]
  recoveries : Array[String]
}

///|
fn incident_json(trace : IncidentTrace, rules : InvestigationRules) -> Json {
  let end = if rules.to_us - trace.first_us <
    rules.incident_us.unwrap_or(60000000UL) {
    rules.to_us
  } else {
    trace.first_us + rules.incident_us.unwrap_or(60000000UL)
  }
  Json::object({
    "id": Json("incident|" + trace.event_ids[0]),
    "first_us": Json(trace.first_us),
    "last_us": Json(trace.last_us),
    "to_us": Json(end),
    "prefix": Json(trace.prefix),
    "peer": Json(trace.peer),
    "path_id": analysis_optional(trace.path_id),
    "reminder_ids": Json(trace.reminders),
    "event_ids": Json(trace.event_ids),
    "recovery_event_ids": Json(trace.recoveries),
    "rule_version": Json(rules.version),
    "end_condition": Json(
      if end == rules.to_us {
        "window_censored"
      } else {
        "fixed_window_elapsed"
      },
    ),
    "unknown_intervals": Json(rules.gaps.unwrap_or([])),
    "interpretation": Json("grouped_observations"),
  })
}

///|
/// 输入先在有界缓冲中稳定排序,再复用 Analyzer;没有由前端解释路径的第二套规则。
pub fn Investigation::report(self : Investigation) -> Json raise ParseError {
  let rules = self.rules
  let analyzer = Analyzer::new(
    from_time=(rules.from_us / 1000000UL).to_uint(),
    from_micros=(rules.from_us % 1000000UL).to_int(),
    to_time=((rules.to_us - 1UL) / 1000000UL).to_uint(),
    to_micros=((rules.to_us - 1UL) % 1000000UL).to_int(),
    capacity=rules.capacity.unwrap_or(20000),
  )
  let inputs = self.inputs.copy()
  for gap in rules.gaps.unwrap_or([]) {
    inputs.push({
      event: None,
      state: None,
      source: None,
      time: gap.from_us,
      baseline: false,
      gap: Some(gap),
      late: false,
    })
  }
  inputs.sort_by_key(fn(input) {
    (
      if input.baseline {
        0
      } else {
        1
      },
      input.time,
      if input.gap is Some(_) {
        0
      } else {
        1
      },
      source_scope(input.source),
      input.source.map(fn(s) { s.file }).unwrap_or(""),
      input.source.map(fn(s) { s.record }).unwrap_or(0L),
      input.source.map(fn(s) { s.event_index }).unwrap_or(0),
    )
  })
  let events = []
  let findings = []
  let intervals = []
  let moas = []
  let incidents = []
  let open_incidents : Map[String, IncidentTrace] = Map([])
  let traces : Map[String, AnalysisTrace] = Map([])
  let moas_traces : Map[String, MoasTrace] = Map([])
  let baselines : Map[String, Unit] = Map([])
  let scopes : Map[String, Unit] = Map([])
  for scope in self.baseline_scopes.keys() {
    baselines[scope] = ()
    scopes[scope] = ()
  }
  let initial = []
  let controls = []
  let all = chart_counts()
  let selected = chart_counts()
  let mut started = false
  let mut reordered = 0
  let mut latest = 0UL
  for input in self.inputs {
    if !input.baseline && input.time < latest {
      reordered += 1
    }
    if !input.baseline && input.time > latest {
      latest = input.time
    }
  }
  for input in inputs {
    let time = input.time
    if time >= rules.to_us {
      continue
    }
    if !started && !input.baseline && time >= rules.from_us {
      started = true
      for _, trace in traces {
        initial.push(
          Json::object({
            "key": Json(snapshot_route_key(trace.event)),
            "state": Json(trace.state),
            "event": trace.event.to_json(),
            "path_view": analysis_optional(
              investigation_path_view(
                Some(
                  route_snapshot(
                    trace.event,
                    trace.state == "active",
                    trace.event.attributes,
                  ),
                ),
              ),
            ),
          }),
        )
      }
      let prefixes : Map[String, Unit] = Map([])
      for _, route in analyzer.routes {
        prefixes[route.event.prefix] = ()
      }
      for prefix in prefixes.keys() {
        moas_traces[prefix] = moas_now(analyzer, prefix, rules.from_us, true)
      }
    }
    let affected = []
    if input.gap is Some(gap) {
      controls.push(
        Json::object({
          "kind": Json("gap"),
          "time_us": Json(time),
          "to_us": Json(gap.to_us),
          "collector": analysis_optional(gap.collector),
          "view": analysis_optional(gap.view),
        }),
      )
      let keys = traces.keys().to_array()
      for key in keys {
        let trace = traces.get(key).unwrap()
        if !gap_scope(gap, trace.event.source) {
          continue
        }
        affected.push(trace.event.prefix)
        if analysis_interval(trace, time, None, rules, false) is Some(interval) {
          intervals.push(interval)
        }
        let gap_trace = {
          ..trace,
          from_us: gap.from_us,
          state: "gap",
          source: None,
          left_censored: gap.from_us < rules.from_us,
        }
        if analysis_interval(gap_trace, gap.to_us, None, rules, false)
          is Some(interval) {
          intervals.push(interval)
        }
        traces[key] = {
          ..trace,
          from_us: gap.to_us,
          state: "unknown",
          source: None,
          left_censored: false,
        }
        if analyzer.routes.get(key) is Some(route) {
          analyzer.release_attributes(route.shared)
        }
        analyzer.routes.remove(key)
      }
    } else if input.state is Some(state) {
      if input.late {
        continue
      }
      scopes[source_scope(state.source)] = ()
      analyzer.observe_state(state)
      if state.old_state == 6 && state.new_state != 6 {
        controls.push(
          Json::object({
            "kind": Json("session_reset"),
            "time_us": Json(time),
            "peer_asn": Json(state.peer_asn),
            "peer_ip": Json(state.peer_ip),
            "source": analysis_optional(state.source),
          }),
        )
        for key in traces.keys().to_array() {
          let trace = traces.get(key).unwrap()
          if trace.event.peer_asn != state.peer_asn ||
            trace.event.peer_ip != state.peer_ip ||
            source_scope(trace.event.source) != source_scope(state.source) {
            continue
          }
          affected.push(trace.event.prefix)
          if analysis_interval(trace, time, state.source, rules, false)
            is Some(interval) {
            intervals.push(interval)
          }
          traces[key] = {
            ..trace,
            from_us: time,
            state: "session_reset",
            source: state.source,
            left_censored: false,
          }
        }
      }
    } else if input.event is Some(event) {
      let key = snapshot_route_key(event)
      scopes[source_scope(event.source)] = ()
      if input.baseline {
        baselines[source_scope(event.source)] = ()
      }
      let old = analyzer.routes.get(key)
      let in_gap = rules.gaps
        .unwrap_or([])
        .any(fn(gap) {
          gap_scope(gap, event.source) &&
          gap.from_us <= time &&
          time < gap.to_us
        })
      let accepted = !in_gap && !input.late
      let changes = if !accepted {
        []
      } else if input.baseline {
        analyzer.seed(event)
        []
      } else {
        analyzer.observe(event)
      }
      let after = if !accepted {
        None
      } else {
        analyzer.routes
        .get(key)
        .map(fn(route) {
          route_snapshot(route.event, route.active, route.shared.wire)
        })
      }
      if accepted {
        if traces.get(key) is Some(trace) {
          if analysis_interval(trace, time, event.source, rules, false)
            is Some(interval) {
            intervals.push(interval)
          }
        } else if time > rules.from_us {
          let unknown : AnalysisTrace = {
            event,
            from_us: rules.from_us,
            state: "unknown",
            source: None,
            left_censored: true,
          }
          if analysis_interval(unknown, time, event.source, rules, false)
            is Some(interval) {
            intervals.push(interval)
          }
        }
        traces[key] = {
          event: {
            ..event,
            attributes: after
            .map(fn(s) { s.attributes })
            .unwrap_or(event.attributes),
          },
          from_us: time,
          state: if event.action == "withdraw" {
            "withdrawn"
          } else {
            "active"
          },
          source: event.source,
          left_censored: time < rules.from_us || input.baseline,
        }
        affected.push(event.prefix)
      }
      if !input.baseline && time >= rules.from_us {
        let row : InvestigationEvent = {
          id: investigation_source_id(event.source),
          time_us: time,
          prefix: event.prefix,
          peer: scoped_peer_key(
            event.peer_asn,
            event.peer_ip,
            event.outbound,
            event.source,
          ),
          peer_asn: event.peer_asn,
          peer_ip: event.peer_ip,
          outbound: event.outbound,
          path_id: event.path_id,
          source: event.source,
          kind: if input.late {
            "out_of_order"
          } else if in_gap {
            "gap_observation"
          } else {
            event.action
          },
          accepted,
          changes_state: accepted &&
          (match (old, after) {
            (Some(previous), Some(next)) =>
              previous.active != next.active ||
              (
                next.active &&
                !route_attributes_equal(previous.shared.wire, next.attributes)
              )
            (None, Some(next)) => next.active
            _ => false
          }),
          before: old.map(fn(route) {
            route_snapshot(route.event, route.active, route.shared.wire)
          }),
          after,
          changes,
          selected: false,
          before_path: investigation_path_view(
            old.map(fn(route) {
              route_snapshot(route.event, route.active, route.shared.wire)
            }),
          ),
          after_path: investigation_path_view(after),
        }
        let row = { ..row, selected: analysis_matches(rules.query, row), }
        events.push(row)
        all.observe(row, rules)
        if row.selected {
          selected.observe(row, rules)
        }
        let clues = analysis_findings(row, rules)
        for clue in clues {
          findings.push(clue)
        }
        let reminder_ids = changes.map(fn(change) { row.id + "|" + change.kind })
        for clue in clues {
          match clue {
            { "condition": "unexpected_origin", "id": String(id), .. } =>
              reminder_ids.push(id)
            _ => ()
          }
        }
        if open_incidents.get(key) is Some(trace) &&
          time - trace.first_us >= rules.incident_us.unwrap_or(60000000UL) {
          incidents.push(incident_json(trace, rules))
          open_incidents.remove(key)
        }
        if !reminder_ids.is_empty() {
          let trace = open_incidents.get_or_init(key, fn() {
            {
              first_us: time,
              last_us: time,
              prefix: row.prefix,
              peer: row.peer,
              path_id: row.path_id,
              reminders: [],
              event_ids: [],
              recoveries: [],
            }
          })
          trace.last_us = time
          trace.event_ids.push(row.id)
          for id in reminder_ids {
            trace.reminders.push(id)
          }
        }
        if open_incidents.get(key) is Some(trace) &&
          trace.first_us < time &&
          row.kind == "announce" &&
          clues.any(fn(clue) {
            match clue {
              {
                "condition": "expected_origin"
                | "same_origin_more_specific"
                | "allowed_more_specific",
                ..
              } => true
              _ => false
            }
          }) {
          trace.recoveries.push(row.id)
        }
      }
    }
    if started {
      let unique : Map[String, Unit] = Map([])
      for prefix in affected {
        unique[prefix] = ()
      }
      for prefix in unique.keys() {
        let next = moas_now(
          analyzer,
          prefix,
          if time < rules.from_us {
            rules.from_us
          } else {
            time
          },
          false,
        )
        if moas_traces.get(prefix) is Some(trace) &&
          trace.signature != next.signature {
          if moas_close(prefix, trace, time, rules, false) is Some(row) {
            moas.push(row)
          }
        }
        if moas_traces
          .get(prefix)
          .map(fn(trace) { trace.signature != next.signature })
          .unwrap_or(true) {
          moas_traces[prefix] = next
        }
      }
    }
    if intervals.length() + findings.length() + moas.length() >
      rules.capacity.unwrap_or(20000) * 8 {
      raise Invalid(0, "调查派生结果超过容量预算")
    }
  }
  if !started {
    for _, trace in traces {
      initial.push(
        Json::object({
          "key": Json(snapshot_route_key(trace.event)),
          "state": Json(trace.state),
          "event": trace.event.to_json(),
          "path_view": analysis_optional(
            investigation_path_view(
              Some(
                route_snapshot(
                  trace.event,
                  trace.state == "active",
                  trace.event.attributes,
                ),
              ),
            ),
          ),
        }),
      )
    }
    let prefixes : Map[String, Unit] = Map([])
    for _, route in analyzer.routes {
      prefixes[route.event.prefix] = ()
    }
    for prefix in prefixes.keys() {
      moas_traces[prefix] = moas_now(analyzer, prefix, rules.from_us, true)
    }
  }
  for _, trace in traces {
    if analysis_interval(trace, rules.to_us, None, rules, true)
      is Some(interval) {
      intervals.push(interval)
    }
  }
  for prefix, trace in moas_traces {
    if moas_close(prefix, trace, rules.to_us, rules, true) is Some(row) {
      moas.push(row)
    }
  }
  for _, trace in open_incidents {
    incidents.push(incident_json(trace, rules))
  }
  intervals.sort_by_key(fn(row) { (row.from_us, row.key) })
  initial.sort_by_key(fn(row) { row.stringify() })
  moas.sort_by_key(fn(row) { row.stringify() })
  findings.sort_by_key(fn(row) { row.stringify() })
  incidents.sort_by_key(fn(row) { row.stringify() })
  let missing = scopes
    .keys()
    .to_array()
    .filter(fn(scope) { !baselines.contains(scope) })
  missing.sort()
  let quality = Json::object({
    "baseline": Json(
      if missing.is_empty() && !scopes.is_empty() {
        "explicit_rib"
      } else {
        "missing"
      },
    ),
    "missing_baseline_scopes": Json(missing),
    "continuous": Json(rules.continuous.unwrap_or(false)),
    "gaps": Json(rules.gaps.unwrap_or([])),
    "reordered_inputs": Json(reordered),
    "ordering": Json("baseline_time_scope_file_record_event_index"),
    "input_budget_bytes": Json(self.bytes),
    "limits": Json("observations_only_not_risk_score_or_business_outage"),
  })
  let report = Json::object({
    "schema_version": Json("1.0"),
    "type": Json("investigation_core"),
    "rules": rules.to_json(),
    "quality": quality,
    "events": Json(events),
    "selected": Json(events.filter(fn(row) { row.selected })),
    "initial": Json(initial),
    "controls": Json(controls),
    "intervals": Json(intervals),
    "moas": Json(moas),
    "findings": Json(findings),
    "more_specifics": Json(investigation_specifics(intervals, rules)),
    "incidents": Json(incidents),
    "summary": Json::object({
      "all": all.report("all", rules),
      "selected": selected.report("selected", rules),
    }),
    "comparison": investigation_comparison(events, intervals, rules),
  })
  if report.stringify().length() > rules.max_bytes.unwrap_or(67108864) {
    raise Invalid(0, "调查输出超过内存预算")
  }
  report
}