// Stateful UDS session analysis over a recorded candump log.
// This is above ISO-TP/UDS decoding: it correlates tester requests with ECU
// responses, handles suppressed positives and pending responses, checks the
// service/session echo and the security-access seed/key order.

pub(all) struct UdsSessionIssue {
  step : Int
  code : String
  severity : String
  detail : String
}

pub(all) struct UdsSessionReport {
  verdict : String
  requests : Int
  responses : Int
  session : String
  security_level : Int
  transferred_bytes : Int
  issues : Array[UdsSessionIssue]
}

fn session_issue(
  issues : Array[UdsSessionIssue],
  step : Int,
  code : String,
  severity : String,
  detail : String,
) -> Unit {
  issues.push({ step, code, severity, detail })
}

fn remove_pending(
  slot : Int,
  pending_step : Array[Int],
  pending_can : Array[Int],
  pending_sid : Array[Int],
  pending_sub : Array[Int],
) -> Unit {
  for i in slot..<(pending_can.length() - 1) {
    pending_step[i] = pending_step[i + 1]
    pending_can[i] = pending_can[i + 1]
    pending_sid[i] = pending_sid[i + 1]
    pending_sub[i] = pending_sub[i + 1]
  }
  ignore(pending_step.pop())
  ignore(pending_can.pop())
  ignore(pending_sid.pop())
  ignore(pending_sub.pop())
}

fn expected_response_id(request_id : Int) -> Int {
  if request_id >= 0x7e0 && request_id <= 0x7e7 {
    request_id + 8
  } else {
    request_id
  }
}

fn payloads_in_order(text : String) -> Array[(Int, Int, Array[Int])] raise IsotpError {
  let frames = parse_candump(text)
  let ids : Array[Int] = Array::new()
  for frame in frames {
    let mut seen = false
    for id in ids {
      if id == frame.id {
        seen = true
      }
    }
    if !seen {
      ids.push(frame.id)
    }
  }
  let messages : Array[(Int, Int, Array[Int])] = Array::new()
  for id in ids {
    let reasm = IsotpReassembler::new()
    for i in 0.. messages.push((i, id, payload))
          None => ()
        }
      }
    }
    if reasm.is_active() { raise Incomplete(reasm.expected, reasm.payload.length()) }
  }
  messages.sort_by(fn(a, b) { a.0 - b.0 })
  messages
}

fn analyze_uds_messages(messages : Array[(Int, Int, Array[Int])]) -> UdsSessionReport {
  let issues : Array[UdsSessionIssue] = Array::new()
  let pending_step : Array[Int] = Array::new()
  let pending_can : Array[Int] = Array::new()
  let pending_sid : Array[Int] = Array::new()
  let pending_sub : Array[Int] = Array::new()
  let mut requests = 0
  let mut responses = 0
  let mut current_session = "default"
  let mut security_level = 0
  let mut security_seed_level = -1
  let mut download_active = false
  let mut transferred_bytes = 0
  let mut expected_bytes = 0
  let mut max_block = 0
  let mut last_block = 0
  let mut last_data : Array[Int] = []
  let mut message_no = 0
  for message in messages {
    message_no = message_no + 1
    let can_id = message.1
    let payload = message.2
    if payload.length() == 0 {
      continue
    }
    let sid = payload[0]
    if sid >= 0x10 && sid <= 0x3e {
      requests = requests + 1
      let sub = if payload.length() > 1 { payload[1] & 0x7f } else { 0 }
      if sid == 0x3e && payload.length() > 1 && (payload[1] & 0x80) != 0 {
        // Suppress-positive TesterPresent: no response is expected.
        continue
      }
      if sid == 0x27 {
        if (sub & 1) == 0 && security_seed_level != sub - 1 {
          session_issue(
            issues,
            message_no,
            "SECURITY_SEQUENCE",
            "fail",
            "key send 0x" + hex_int(sub, 2) + " without matching seed request",
          )
        }
      }
      if sid == 0x36 && !download_active {
        session_issue(issues, message_no, "TRANSFER_BEFORE_DOWNLOAD", "fail", "TransferData has no accepted RequestDownload")
      }
      pending_step.push(message_no)
      pending_can.push(can_id)
      pending_sid.push(sid)
      pending_sub.push(sub)
    } else if sid == 0x7f || (sid >= 0x40 && sid <= 0x7e) {
      responses = responses + 1
      let mut slot = -1
      let mut id_slot = -1
      for i in 0.. 1 && payload[1] == pending_sid[i]
        } else {
          sid - 0x40 == pending_sid[i]
        }
        let id_matches = if pending_can[i] >= 0x7e0 && pending_can[i] <= 0x7e7 {
          pending_can[i] + 8 == can_id
        } else {
          pending_can[i] == can_id
        }
        if id_matches {
          id_slot = i
          if sid_matches {
            slot = i
            break
          }
        }
      }
      if slot < 0 && id_slot >= 0 {
        session_issue(
          issues,
          message_no,
          "SERVICE_MISMATCH",
          "fail",
          "response 0x" + hex_int(sid, 2) + " does not match request 0x" +
          hex_int(pending_sid[id_slot], 2),
        )
        remove_pending(id_slot, pending_step, pending_can, pending_sid, pending_sub)
      } else if slot < 0 {
        session_issue(
          issues,
          message_no,
          "ORPHAN_RESPONSE",
          "fail",
          "response 0x" + hex_int(sid, 2) + " has no matching request",
        )
      } else {
        let req_sid = pending_sid[slot]
        let req_sub = pending_sub[slot]
        let request_data = messages[pending_step[slot] - 1].2
        if sid == 0x7f {
          let nrc = if payload.length() > 2 { payload[2] } else { 0 }
          if nrc == 0x78 {
            session_issue(
              issues,
              message_no,
              "PENDING_RESPONSE",
              "review",
              "request 0x" + hex_int(req_sid, 2) + " is pending (NRC 0x78)",
            )
          } else {
            if req_sid == 0x27 {
              security_seed_level = -1
            }
            session_issue(
              issues,
              message_no,
              "NEGATIVE_RESPONSE",
              "review",
              "request 0x" + hex_int(req_sid, 2) + " rejected: NRC 0x" +
              hex_int(nrc, 2) + " " + nrc_description(nrc),
            )
            // Remove the request: a negative response is final.
            remove_pending(slot, pending_step, pending_can, pending_sid, pending_sub)
          }
        } else {
          if req_sid == 0x22 {
            if request_data.length() == 3 && (payload.length() < 3 || payload[1] != request_data[1] || payload[2] != request_data[2]) {
              session_issue(issues, message_no, "DID_ECHO_MISMATCH", "fail", "ReadDataByIdentifier response belongs to a different DID")
            }
          }
          if req_sid == 0x34 {
            if request_data.length() < 3 || payload.length() < 3 {
              session_issue(issues, message_no, "DOWNLOAD_FORMAT", "fail", "download request or response is truncated")
            } else {
              let address_len = request_data[2] & 15
              let size_len = request_data[2] >> 4
              let block_len = payload[1] >> 4
              if request_data[1] != 0 {
                session_issue(issues, message_no, "UNSUPPORTED_DOWNLOAD_FORMAT", "fail", "compressed or encrypted downloads are outside this auditor")
              } else if address_len == 0 || size_len == 0 || size_len > 4 || request_data.length() != 3 + address_len + size_len || block_len == 0 || block_len > 4 || payload.length() != 2 + block_len {
                session_issue(issues, message_no, "DOWNLOAD_FORMAT", "fail", "invalid download address/size or block-length fields")
              } else {
                let mut total = 0L
                let mut limit = 0L
                for k in (3 + address_len).. 2147483647L || limit < 3L || limit > 2147483647L {
                  session_issue(issues, message_no, "DOWNLOAD_SIZE", "fail", "download size or block limit is outside the offline auditor range")
                } else {
                  download_active = true
                  transferred_bytes = 0
                  expected_bytes = total.to_int()
                  max_block = limit.to_int()
                  last_block = 0
                  last_data = []
                }
              }
            }
          } else if req_sid == 0x36 && download_active {
            if request_data.length() < 3 || payload.length() != 2 || payload[1] != request_data[1] {
              session_issue(issues, message_no, "BLOCK_ECHO_MISMATCH", "fail", "TransferData response must echo its block counter")
            } else if request_data.length() > max_block {
              session_issue(issues, message_no, "BLOCK_TOO_LARGE", "fail", "TransferData exceeds the ECU negotiated block length")
            } else {
              let block = request_data[1]
              if block == last_block && last_data == request_data {
                // An acknowledged exact retransmission is not counted twice.
                ()
              } else if block != ((last_block + 1) & 255) {
                session_issue(issues, message_no, "BLOCK_SEQUENCE", "fail", "TransferData counter skipped, changed on retry, or arrived out of order")
              } else {
                last_block = block
                last_data = request_data
                transferred_bytes = transferred_bytes + request_data.length() - 2
                if transferred_bytes > expected_bytes {
                  session_issue(issues, message_no, "TRANSFER_SIZE_EXCEEDED", "fail", "acknowledged transfer exceeds RequestDownload memorySize")
                }
              }
            }
          } else if req_sid == 0x37 {
            if !download_active {
              session_issue(issues, message_no, "EXIT_BEFORE_DOWNLOAD", "fail", "transfer exit without an accepted download")
            } else if transferred_bytes != expected_bytes {
              session_issue(issues, message_no, "TRANSFER_SIZE_MISMATCH", "fail", "acknowledged bytes do not match RequestDownload memorySize")
            }
            download_active = false
          }
          if req_sid == 0x27 {
            if payload.length() < 2 || payload[1] != req_sub {
              session_issue(issues, message_no, "SECURITY_ECHO_MISMATCH", "fail", "security response does not echo the requested subfunction")
              security_seed_level = -1
            } else if (req_sub & 1) == 1 {
              if payload.length() < 3 {
                session_issue(issues, message_no, "MISSING_SEED", "fail", "successful seed response has no seed bytes")
              } else {
                security_seed_level = req_sub
              }
            } else if security_seed_level == req_sub - 1 {
              security_level = req_sub - 1
              security_seed_level = -1
            }
          }
          if req_sid == 0x10 {
            if payload.length() < 2 || payload[1] != req_sub {
              session_issue(
                issues,
                message_no,
                "SESSION_ECHO_MISMATCH",
                "fail",
                "requested session 0x" + hex_int(req_sub, 2) +
                " but ECU returned a missing or different session echo",
              )
            } else {
              current_session = session_name(req_sub)
              security_level = 0
              security_seed_level = -1
            }
          }
          // Positive response is final.
          remove_pending(slot, pending_step, pending_can, pending_sid, pending_sub)
        }
      }
    } else {
      session_issue(issues,message_no,"UNSUPPORTED_SERVICE","review","payload is outside the implemented UDS service range")
    }
  }
  for i in 0.. Array[UdsEcuReport] raise IsotpError {
  let messages = payloads_in_order(text)
  let ids : Array[Int] = []
  for message in messages {
    let id = if message.1 >= 0x7e8 && message.1 <= 0x7ef { message.1 - 8 } else { message.1 }
    if !ids.contains(id) { ids.push(id) }
  }
  let out : Array[UdsEcuReport] = []
  for id in ids {
    let group : Array[(Int, Int, Array[Int])] = []
    for message in messages {
      if message.1 == id || message.1 == expected_response_id(id) {
        group.push(message)
      }
    }
    out.push({ channel, request_id: id, report: analyze_uds_messages(group) })
  }
  out
}

// Reassembly and session state must never mix identical IDs from different buses.
pub fn analyze_uds_network(text : String) -> Array[UdsEcuReport] raise IsotpError {
  let channels : Array[String] = []
  let logs : Array[StringBuilder] = []
  let mut invalid = 0
  for raw in text.split("\n") {
    let line = raw.trim_space().to_string()
    if line.length() == 0 || line.has_prefix("#") { continue }
    match parse_candump_line(line) {
      None => invalid = invalid + 1
      Some(frame) => {
        let mut index = -1
        for i in 0.. 0 {
    out.push({ channel: "", request_id: -1, report: {
      verdict: "fail", requests: 0, responses: 0, session: "unknown", security_level: 0, transferred_bytes: 0,
      issues: [{ step: 0, code: "MALFORMED_CAPTURE", severity: "fail", detail: invalid.to_string() + " lines cannot be parsed" }],
    } })
  }
  out
}

pub fn analyze_uds_session(text : String) -> UdsSessionReport raise IsotpError {
  let ecus = analyze_uds_network(text)
  if ecus.length() == 1 { return ecus[0].report }
  let issues : Array[UdsSessionIssue] = []
  let mut requests = 0
  let mut responses = 0
  let mut verdict = "review"
  for ecu in ecus {
    requests = requests + ecu.report.requests
    responses = responses + ecu.report.responses
    for item in ecu.report.issues { issues.push(item) }
    if ecu.report.verdict == "fail" { verdict = "fail" }
  }
  if ecus.length() == 0 {
    session_issue(issues, 0, "EMPTY_CAPTURE", "review", "no diagnostic messages decoded")
  } else if verdict != "fail" {
    verdict = "pass"
    for ecu in ecus { if ecu.report.verdict == "review" { verdict = "review" } }
  }
  { verdict, requests, responses, session: "multiple/none; inspect per-ECU reports", security_level: 0, transferred_bytes: 0, issues }
}

// Transport-independent UDS payload audit; usable with CAN, DoIP, queues or
// software clients after the transport supplies direction and payload bytes.
pub(all) struct DirectedUdsPayload {
  is_request : Bool
  payload : Array[Int]
}

pub fn analyze_uds_exchange(messages : Array[DirectedUdsPayload]) -> UdsSessionReport {
  let items : Array[(Int, Int, Array[Int])] = []
  for i in 0.. UdsSessionReport raise IsotpError {
  let messages : Array[DirectedUdsPayload] = []
  for raw in text.split("\n") {
    let line = raw.trim_space().to_string()
    if line.length() == 0 || line.has_prefix("#") { continue }
    if !line.has_prefix(">>") && !line.has_prefix("<<") { raise InvalidLength(-1) }
    let payload = match hex_decode(line.substring(start=2)) {
      Some(bytes) => bytes
      None => raise InvalidLength(-1)
    }
    if payload.length() == 0 { raise EmptyPayload }
    messages.push({ is_request: line.has_prefix(">>"), payload })
  }
  analyze_uds_exchange(messages)
}

pub fn render_uds_session(report : UdsSessionReport) -> String {
  let out = StringBuilder()
  out.write_string("verdict: " + report.verdict + "\n")
  out.write_string("requests: " + report.requests.to_string() + "\n")
  out.write_string("responses: " + report.responses.to_string() + "\n")
  out.write_string("session: " + report.session + "\n")
  out.write_string("security level: " + report.security_level.to_string() + "\n")
  out.write_string("acknowledged transfer bytes: " + report.transferred_bytes.to_string() + "\n")
  for item in report.issues {
    out.write_string(
      "issue step=" + item.step.to_string() + " " + item.severity + " " +
      item.code + ": " + item.detail + "\n",
    )
  }
  out.to_string()
}