// JSON I/O: serialize a Device (or lint Issues) to JSON and back.
//
// Conversion uses `moonbitlang/core/json`. To keep the JSON schema aligned with
// the point-table text keywords ("float32", "R", "error", ...) and to stay
// warning-free under nightly `moon check --deny-warn`, we do NOT derive
// ToJson/FromJson on any custom type (doing so triggers the
// `implicit_impl_as_method` deprecation warning). Instead we build/read plain
// `Map[String, Json]` / arrays / primitives using only the standard ToJson and
// FromJson impls that ship with core.

///|
/// Build the JSON object for a single register using only standard ToJson impls.
fn reg_to_map(r : Register) -> Map[String, Json] {
  {
    "name": @json.to_json(r.name),
    "address": @json.to_json(r.address),
    "rtype": @json.to_json(type_name(r.rtype)),
    "access": @json.to_json(access_name(r.access)),
    "unit": @json.to_json(r.unit),
    "jsonb": @json.to_json(r.jsonb),
    "line": @json.to_json(r.line),
  }
}

///|
/// Serialize a whole `Device` to a `Json` value.
pub fn device_to_json(dev : Device) -> Json {
  let rows : Array[Map[String, Json]] = []
  for r in dev.registers {
    rows.push(reg_to_map(r))
  }
  @json.to_json({ "registers": @json.to_json(rows) })
}

///|
/// Serialize a whole `Device` to a pretty JSON string.
pub fn device_to_json_string(dev : Device) -> String {
  device_to_json(dev).stringify(indent=2)
}

///|
/// Read a required string field from a decoded object.
fn jstr(row : Map[String, Json], key : String) -> Result[String, String] {
  match row.get(key) {
    None => Err("missing field '" + key + "'")
    Some(v) => {
      let s : String = @json.from_json(v) catch {
        _ => return Err("field '" + key + "' must be a string")
      }
      Ok(s)
    }
  }
}

///|
/// Read a required int field from a decoded object.
fn jint(row : Map[String, Json], key : String) -> Result[Int, String] {
  match row.get(key) {
    None => Err("missing field '" + key + "'")
    Some(v) => {
      let n : Int = @json.from_json(v) catch {
        _ => return Err("field '" + key + "' must be an integer")
      }
      Ok(n)
    }
  }
}

///|
/// Convert a decoded object back to a `Register`, validating the keywords.
fn reg_from_map(row : Map[String, Json]) -> Result[Register, String] {
  let name = match jstr(row, "name") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let address = match jint(row, "address") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let unit = match jstr(row, "unit") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let jsonb = match jstr(row, "jsonb") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let line = match jint(row, "line") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let rtype_key = match jstr(row, "rtype") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let access_key = match jstr(row, "access") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let rtype = match parse_type(rtype_key) {
    Ok(v) => v
    Err(e) => return Err("invalid 'rtype': " + e)
  }
  let access = match parse_access(access_key) {
    Ok(v) => v
    Err(e) => return Err("invalid 'access': " + e)
  }
  Ok({ name, address, rtype, access, unit, jsonb, line, })
}

///|
/// Parse a JSON string back into a `Device`.
pub fn parse_device_json(text : String) -> Result[Device, String] {
  let j = @json.parse(text) catch { _ => return Err("invalid JSON document") }
  let obj : Map[String, Json] = @json.from_json(j) catch {
    _ => return Err("top level must be a JSON object")
  }
  let regsJson = match obj.get("registers") {
    None => return Err("missing field 'registers'")
    Some(v) => v
  }
  let rows : Array[Map[String, Json]] = @json.from_json(regsJson) catch {
    _ => return Err("field 'registers' must be an array of objects")
  }
  let regs : Array[Register] = []
  for row in rows {
    match reg_from_map(row) {
      Ok(r) => regs.push(r)
      Err(e) => return Err(e)
    }
  }
  Ok({ registers: regs, })
}

///|
/// Serialize a lint finding to its JSON object form.
fn issue_to_map(it : Issue) -> Map[String, Json] {
  {
    "severity": @json.to_json(severity_name(it.severity)),
    "line": @json.to_json(it.line),
    "message": @json.to_json(it.message),
  }
}

///|
/// Render lint findings as a JSON array string.
pub fn render_issues_json(issues : Array[Issue]) -> String {
  let rows : Array[Map[String, Json]] = []
  for it in issues {
    rows.push(issue_to_map(it))
  }
  @json.to_json(rows).stringify(indent=2)
}

///|
/// Round-trip a `Device` through JSON.
pub fn json_roundtrip(dev : Device) -> Result[Device, String] {
  parse_device_json(device_to_json_string(dev))
}

///|
test "device json keywords survive a JSON round trip" {
  let dev : Device = {
    registers: [
      {
        name: "coil_temp",
        address: 40001,
        rtype: TFloat32,
        access: Read,
        unit: "degC",
        jsonb: "jsonb->coil_temp",
        line: 2,
      },
      {
        name: "conveyor",
        address: 40003,
        rtype: TBit,
        access: ReadWrite,
        unit: "",
        jsonb: "jsonb->conveyor_run",
        line: 3,
      },
    ],
  }
  let back = json_roundtrip(dev).unwrap()
  assert_eq(back.registers.length(), 2)
  assert_eq(type_name(back.registers[0].rtype), "float32")
  assert_eq(access_name(back.registers[0].access), "R")
  assert_eq(type_name(back.registers[1].rtype), "bool")
  assert_eq(access_name(back.registers[1].access), "RW")
  assert_eq(back.registers[0].unit, "degC")
  assert_eq(back.registers[0].jsonb, "jsonb->coil_temp")
}

///|
test "device json string mentions the text keywords" {
  let dev : Device = {
    registers: [
      {
        name: "a",
        address: 40001,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 1,
      },
    ],
  }
  let s = device_to_json_string(dev)
  assert_eq(@json.valid(s), true)
  assert_eq(s.contains("int16"), true)
  assert_eq(s.contains("\"R\""), true)
  assert_eq(s.contains("40001"), true)
}

///|
test "parse_device_json rejects malformed input" {
  assert_eq(parse_device_json("not json") is Ok(_), false)
  assert_eq(parse_device_json("[]") is Ok(_), false)
  assert_eq(parse_device_json("{\"name\": 1}") is Ok(_), false)
}

///|
test "parse_device_json rejects invalid keywords" {
  let bad = "{\"registers\": [{\"name\":\"x\",\"address\":40001,\"rtype\":\"garbage\",\"access\":\"R\",\"unit\":\"\",\"jsonb\":\"\",\"line\":1}]}"
  assert_eq(parse_device_json(bad) is Ok(_), false)
}

///|
test "render_issues_json is valid json with severity keywords" {
  let issues = [mk_issue(Error, 3, "dup name"), mk_issue(Warning, 9, "gap")]
  let s = render_issues_json(issues)
  assert_eq(@json.valid(s), true)
  assert_eq(s.contains("error"), true)
  assert_eq(s.contains("warning"), true)
}

///|
test "parse_device_json accepts an empty register list" {
  assert_eq(parse_device_json("{\"registers\": []}") is Ok(_), true)
}

///|
test "parse_device_json rejects an unknown access keyword" {
  let bad = "{\"registers\": [{\"name\":\"x\",\"address\":40001,\"rtype\":\"int16\",\"access\":\"RR\",\"unit\":\"\",\"jsonb\":\"\",\"line\":1}]}"
  assert_eq(parse_device_json(bad) is Ok(_), false)
}

///|
test "parse_device_json rejects a missing required field" {
  let bad = "{\"registers\": [{\"address\":40001,\"rtype\":\"int16\"}]}"
  assert_eq(parse_device_json(bad) is Ok(_), false)
}

///|
test "parse_device_json tolerates unknown extra fields" {
  let json = "{\"registers\":[{\"name\":\"x\",\"address\":40001,\"rtype\":\"int16\",\"access\":\"R\",\"unit\":\"\",\"jsonb\":\"\",\"line\":1,\"extra\":99}]}"
  let res = parse_device_json(json)
  assert_eq(res is Ok(_), true)
  assert_eq(res.unwrap().registers.length(), 1)
}

///|
test "device json round trip preserves an empty device" {
  let dev : Device = { registers: [], }
  let back = json_roundtrip(dev).unwrap()
  assert_eq(back.registers.length(), 0)
}