///|
/// AR Connect request assembly.
///
/// Two layers:
///   1. IOD block layer (`encode_connect_iod_blocks`) — byte-exact per the C#
///      reference: ARBlockReq, IOCR(input), IOCR(output), ExpectedSubmodule,
///      AlarmCR, each wrapped in the standard IO block header.
///   2. CL-RPC envelope (`encode_connect_request`) — DCE/RPC header (opnum 0,
///      PNIO device interface) + NDR args wrapper around the IOD blocks.
///
/// NOTE: the IOD block layer uses big-endian PROFINET block fields, while the
/// DCE/RPC header and NDR argument wrapper follow DREP=0x10 little-endian, as
/// in the C# reference `DceClient`/`z`/`IoRequest` path.

///|
/// Concatenate the AR Connect IOD blocks (each with its block header) in the
/// canonical Connect.req order.
pub fn encode_connect_iod_blocks(
  ar : ARBlockReq,
  input_iocr : IOCRBlockReq,
  output_iocr : IOCRBlockReq,
  expected : ExpectedSubmoduleBlockReq,
  alarm : AlarmCRBlockReq,
) -> Bytes {
  let output : Array[Byte] = []
  append_bytes(
    output,
    encode_io_block(block_type_ar_req, encode_ar_block_req(ar)),
  )
  append_bytes(
    output,
    encode_io_block(block_type_iocr_req, encode_iocr_block_req(input_iocr)),
  )
  append_bytes(
    output,
    encode_io_block(block_type_iocr_req, encode_iocr_block_req(output_iocr)),
  )
  append_bytes(
    output,
    encode_io_block(
      block_type_expected_submodule_req,
      encode_expected_submodule_block_req(expected),
    ),
  )
  append_bytes(
    output,
    encode_io_block(block_type_alarm_cr_req, encode_alarm_cr_block_req(alarm)),
  )
  Bytes::from_array(output)
}

///|
/// Assemble a full CL-RPC Connect request UDP payload: DCE/RPC header
/// (opnum 0, PNIO device interface) + NDR args wrapper + IOD blocks.
pub fn encode_connect_request(
  blocks : Bytes,
  activity_uuid : @rpc.Uuid,
  object_uuid? : @rpc.Uuid = @rpc.Uuid::empty(),
  sequence_number? : Int = 0,
) -> Bytes raise @frame.FrameError {
  let ndr = @rpc.IoRequest::{
    ..@rpc.IoRequest::default(),
    args_length: blocks.length(),
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_request(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_request,
    object_uuid,
    interface_uuid: @rpc.Uuid::from_hex(@rpc.uuid_pnio_device),
    activity_uuid,
    interface_version: 1,
    opnum: 0,
    sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

///|
/// ExpectedSubmodule layout for the VTS-EX-CA0400 drive, derived from its GSDML
/// (`GSDML-V2.3-V_T-VTS-EX-CA0400`):
///   - API 0, Slot 1 = Drive (module 0x00000002): Standard telegram 1
///     (submodule 0x00000002) at subslot 0x0001, INPUT+OUTPUT 10 bytes each
///     (PZD-5/5).
/// Per PN-AL, ExpectedSubmoduleBlockReq contains only submodules referenced by
/// the IOCRs, so the DAP/system submodules are intentionally omitted here.
/// IOCR/frame-offset wiring and exact properties are tuned live in b3.
pub fn ca400_expected_submodule() -> ExpectedSubmoduleBlockReq {
  let drive_slot = ExpectedSlot::{
    api: 0,
    slot_number: 1,
    module_ident: 0x00000002,
    module_properties: 0,
    submodules: [
      ExpectedSubmodule::{
        subslot_number: 0x0001,
        submodule_ident: 0x00000002,
        submodule_properties: 0x0003, // INPUT_AND_OUTPUT
        data_descriptions: [
          DataDescription::{
            data_description: data_description_input,
            data_length: 10,
            iocs_length: 1,
            iops_length: 1,
          },
          DataDescription::{
            data_description: data_description_output,
            data_length: 10,
            iocs_length: 1,
            iops_length: 1,
          },
        ],
      },
    ],
  }
  ExpectedSubmoduleBlockReq::{ apis: [drive_slot] }
}

///|
/// CA400 Standard Telegram 1 startup parameter record from the GSDML
/// `IDS_TEL1/ParameterRecordDataItem Index=1234 Length=15` Const data.
pub fn ca400_tel1_parameter_record_data() -> Bytes {
  Bytes::from_array([
    b'\x03', b'\x03', b'\x01', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
    b'\x60', b'\x08', b'\x60', b'\x05', b'\x60', b'\x0A',
  ])
}

///|
fn ca400_iocr_api() -> IocrApi {
  IocrApi::{
    api: 0,
    io_data_objects: [
      IoDataObject::{ slot_number: 1, subslot_number: 1, frame_offset: 0 },
    ],
    iocs: [
      IoDataObject::{ slot_number: 1, subslot_number: 1, frame_offset: 11 },
    ],
  }
}

///|
/// Initial CA400 input IOCR mapping for Standard Telegram 1. With the RT
/// Ethernet transport (`LT=0x8892`, `IOCRProperties=2`), DataLength uses the
/// RT_CLASS minimum of 40 bytes; telegram data/status occupy the leading bytes.
pub fn ca400_input_iocr(
  frame_id? : Int = 0x8000,
  send_clock_factor? : Int = 32,
  reduction_ratio? : Int = 2,
  watchdog_factor? : Int = 3,
  data_hold_factor? : Int = 3,
) -> IOCRBlockReq {
  IOCRBlockReq::{
    ..IOCRBlockReq::default_input(frame_id),
    data_length: 40,
    send_clock_factor,
    reduction_ratio,
    watchdog_factor,
    data_hold_factor,
    apis: [ca400_iocr_api()],
  }
}

///|
/// Initial CA400 output IOCR mapping for Standard Telegram 1. See
/// `ca400_input_iocr` for the 40-byte DataLength rationale.
pub fn ca400_output_iocr(
  frame_id? : Int = 0x8001,
  send_clock_factor? : Int = 32,
  reduction_ratio? : Int = 2,
  watchdog_factor? : Int = 3,
  data_hold_factor? : Int = 3,
) -> IOCRBlockReq {
  IOCRBlockReq::{
    ..IOCRBlockReq::default_output(frame_id),
    data_length: 40,
    send_clock_factor,
    reduction_ratio,
    watchdog_factor,
    data_hold_factor,
    apis: [ca400_iocr_api()],
  }
}