///|
/// Usage attributed to one run, stage and model after reconciliation.
pub(all) struct UsageGroup {
  run_id : String
  stage : String
  model : String
  totals : Totals
  usage_complete : Bool
  missing_input_calls : Int32
  missing_output_calls : Int32
} derive(ToJson)

///|
pub extend UsageGroup with ToJson::{to_json}

///|
/// Row counts distinguish decoding, duplicate copies and accepted identities.
pub(all) struct AuditStats {
  parsed_calls : Int32
  accepted_calls : Int32
  duplicate_rows : Int32
  conflicting_rows : Int32
} derive(ToJson)

///|
pub extend AuditStats with ToJson::{to_json}

///|
/// Additive envelope; the existing Report and audit commands remain unchanged.
pub(all) struct DetailedReport {
  schema_version : Int32
  report : Report
  stats : AuditStats
  group_totals_available : Bool
  groups : Array[UsageGroup]
  diagnostic_counts : Map[String, Int32]
} derive(ToJson)

///|
pub extend DetailedReport with ToJson::{to_json}

///|
/// Group order follows first accepted occurrence; no prompt or output is retained.
pub fn detail_records(raw : Parsed, manifest_text : String?) -> DetailedReport {
  let parsed = reconcile(raw)
  let report = audit_reconciled(parsed, manifest_text)
  let counts : Map[String, Int32] = Map([])
  for finding in report.findings {
    counts.set(finding.code, counts.get(finding.code).unwrap_or(0) + 1)
  }
  let members : Map[(String, String, String), Array[Record]] = Map([])
  let order : Array[(String, String, String)] = []
  for record in parsed.records {
    let call = record.call
    let key = (call.run_id, call.stage, call.model)
    match members.get(key) {
      Some(rows) => rows.push(record)
      None => {
        members.set(key, [record])
        order.push(key)
      }
    }
  }
  let groups : Array[UsageGroup] = []
  for key in order {
    let rows = members[key]
    let group = account({ records: rows, findings: [], })
    let mut missing_input = 0
    let mut missing_output = 0
    for row in rows {
      if row.call.input_tokens is None {
        missing_input += 1
      }
      if row.call.output_tokens is None {
        missing_output += 1
      }
    }
    groups.push({
      run_id: key.0,
      stage: key.1,
      model: key.2,
      totals: group.totals,
      usage_complete: group.usage_complete,
      missing_input_calls: missing_input,
      missing_output_calls: missing_output,
    })
  }
  {
    schema_version: 1,
    report,
    stats: {
      parsed_calls: raw.records.length(),
      accepted_calls: parsed.records.length(),
      duplicate_rows: counts.get("duplicate_call").unwrap_or(0),
      conflicting_rows: counts.get("conflicting_call").unwrap_or(0),
    },
    group_totals_available: !counts.contains("aggregate_overflow"),
    groups: if counts.contains("aggregate_overflow") {
      []
    } else {
      groups
    },
    diagnostic_counts: counts,
  }
}

///|
/// Detailed normalized-JSONL audit with optional expected task manifest.
pub fn audit_detail(text : String, manifest_text : String?) -> DetailedReport {
  detail_records(parse_jsonl(text), manifest_text)
}