///|
fn json_record(fields : Array[(String, Json)]) -> Json {
  Json::object(Map(fields))
}

///|
fn content_json(page : ContentPage) -> Json {
  match page {
    Empty => Json::null()
    Source(source, number) =>
      json_record([
        ("source", Json::string(source)),
        ("page", Json::Json(number)),
      ])
  }
}

///|
/// Canonical, escaped request serialization. Reparse to reproduce the same job.
pub fn job_spec_json(spec : JobSpec) -> Result[String, WorkflowError] {
  match build_job(spec) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let sections = spec.sections.map(fn(section) {
    json_record([
      ("source", Json::string(section.source)),
      ("source_pages", Json::Json(section.source_pages)),
      ("selection", Json::string(section.selection)),
      ("start_on_recto", Json::boolean(section.start_on_recto)),
    ])
  })
  let print = Map([
    ("copies", Json::Json(spec.print.copies)),
    ("reverse_sheets", Json::boolean(spec.print.reverse_sheets)),
    (
      "collation",
      Json::string(
        match spec.print.collation {
          Collated => "collated"
          Uncollated => "uncollated"
        },
      ),
    ),
  ])
  match spec.print.mode {
    Automatic => print.set("mode", Json::string("automatic"))
    Manual(stack) => {
      print.set("mode", Json::string("manual"))
      print.set(
        "back_stack",
        Json::string(
          match stack {
            SameOrder => "same"
            Reversed => "reversed"
          },
        ),
      )
    }
  }
  let fields = Map([
    ("schema", Json::Json(1)),
    ("sections", Json::array(sections)),
    (
      "binding",
      Json::string(if spec.binding == Left { "left" } else { "right" }),
    ),
    (
      "duplex",
      Json::string(
        if spec.duplex == LongEdge {
          "long-edge"
        } else {
          "short-edge"
        },
      ),
    ),
    (
      "paper",
      json_record([
        ("width", Json::Json(spec.paper.width)),
        ("height", Json::Json(spec.paper.height)),
        ("margin", Json::Json(spec.paper.margin)),
        ("gutter", Json::Json(spec.paper.gutter)),
        ("creep", Json::Json(spec.paper.creep)),
      ]),
    ),
    ("print", Json::object(print)),
  ])
  match spec.capacities {
    Some(sizes) =>
      fields.set("gatherings", Json::array(sizes.map(fn(n) { Json::Json(n) })))
    None => {
      fields.set("maximum", Json::Json(spec.maximum))
      fields.set(
        "policy",
        Json::string(
          match spec.policy {
            Fill => "fill"
            Balanced => "balanced"
          },
        ),
      )
    }
  }
  Ok(Json::object(fields).stringify(indent=2))
}

///|
fn resolved_slot(booklet : Booklet, slot : Slot) -> Json {
  match slot {
    Blank =>
      json_record([
        ("logical", Json::null()),
        ("content", Json::null()),
        ("padding", Json::boolean(true)),
      ])
    Page(n) =>
      json_record([
        ("logical", Json::Json(n)),
        ("content", content_json(booklet.document.pages[n - 1])),
        ("padding", Json::boolean(false)),
      ])
  }
}

///|
/// Resolved manifest records source provenance and actual print-pass order.
pub fn job_manifest(job : PrintJob) -> Result[String, WorkflowError] {
  match verify_schedule(job.booklet, job.schedule) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let report = match preflight(job.booklet, job.paper) {
    Err(e) => return Err(e)
    Ok(report) => report
  }
  let faces : Array[Json] = []
  for index, sheet in job.booklet.sheets {
    for side, face in [sheet.front, sheet.back] {
      faces.push(
        json_record([
          ("sheet", Json::Json(index + 1)),
          ("gathering", Json::Json(sheet.signature + 1)),
          ("depth", Json::Json(sheet.depth)),
          ("back", Json::boolean(side == 1)),
          ("rotation", Json::Json(face.rotation)),
          ("left", resolved_slot(job.booklet, face.left)),
          ("right", resolved_slot(job.booklet, face.right)),
        ]),
      )
    }
  }
  let passes = job.schedule.passes.map(fn(pass) {
    json_record([
      ("reload", Json::boolean(pass.reload)),
      (
        "impressions",
        Json::array(
          pass.impressions.map(fn(i) {
            json_record([
              ("copy", Json::Json(i.copy)),
              ("sheet", Json::Json(i.sheet + 1)),
              ("back", Json::boolean(i.back)),
            ])
          }),
        ),
      ),
    ])
  })
  let result = json_record([
    ("schema", Json::Json(1)),
    ("logical_pages", Json::Json(report.logical_pages)),
    ("content_pages", Json::Json(report.content_pages)),
    ("explicit_blanks", Json::Json(report.explicit_blanks)),
    ("padding_blanks", Json::Json(report.padding_blanks)),
    ("physical_sheets", Json::Json(report.sheets * job.schedule.options.copies)),
    ("faces", Json::array(faces)),
    ("passes", Json::array(passes)),
    (
      "issues",
      Json::array(
        report.issues.map(fn(i) {
          json_record([
            ("blocking", Json::boolean(i.blocking)),
            ("field", Json::string(i.field)),
            ("message", Json::string(i.message)),
          ])
        }),
      ),
    ),
  ])
  Ok(result.stringify(indent=2))
}

///|
fn csv_quote(value : String) -> String {
  "\"" + value.replace(old="\"", new="\"\"") + "\""
}

///|
fn slot_csv(booklet : Booklet, slot : Slot) -> String {
  match slot {
    Blank => ",,,true"
    Page(n) =>
      match booklet.document.pages[n - 1] {
        Empty => "\{n},,,false"
        Source(source, page) => "\{n},\{csv_quote(source)},\{page},false"
      }
  }
}

///|
/// One record per impression, with RFC-style quoting for arbitrary source labels.
pub fn job_csv(job : PrintJob) -> Result[String, WorkflowError] {
  match verify_schedule(job.booklet, job.schedule) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let rows = [
    "pass,copy,sheet,gathering,depth,face,left_logical,left_source,left_page,left_padding,right_logical,right_source,right_page,right_padding,rotation",
  ]
  for pass_index, pass in job.schedule.passes {
    for impression in pass.impressions {
      let sheet = job.booklet.sheets[impression.sheet]
      let face = if impression.back { sheet.back } else { sheet.front }
      let side = if impression.back { "back" } else { "front" }
      rows.push(
        "\{pass_index + 1},\{impression.copy},\{impression.sheet + 1},\{sheet.signature + 1},\{sheet.depth},\{side},\{slot_csv(job.booklet, face.left)},\{slot_csv(job.booklet, face.right)},\{face.rotation}",
      )
    }
  }
  Ok(rows.join("\n") + "\n")
}