export const ARUSNAVI_INTERNAL_SPECIFICATION_REF =
  "arusnavi.internal.protocol-sheet.gid-12.v1";

const HEADER2_LENGTH = 10;
const HEADER_START = 0xff;
const HEADER2_GPRS_VERSION = 0x23;
const PACKAGE_START = 0x5b;
const PACKAGE_END = 0x5d;
const SERVER_COMMAND_START = 0x7b;
const SERVER_COMMAND_END = 0x7d;
const MIN_PACKAGE_NUMBER = 0x01;
const MAX_PACKAGE_NUMBER = 0xfb;
const PACKET_FIXED_LENGTH = 8;
const MAX_PACKET_DATA_LENGTH = 32 * 1024;
const MAX_PACKAGE_LENGTH = 64 * 1024;

export const ARUSNAVI_B2_MODEL_PROFILE = deepFreeze({
  schemaVersion: "nodedc.device-model-profile.v1",
  profileRef: "arusnavi.b2.internal.v1",
  vendor: "ARUSNAVI",
  model: "B2",
  deviceType: "tracker",
  protocol: "INTERNAL",
  monitoringServerSlots: 4,
  serverIdentity: {
    kind: "imei",
    source: "modem",
    trust: "claimed-not-ownership-proof",
  },
  bootstrap: {
    operatorSurface: "ARUSNAVI_WEB_OR_LOCAL_CONFIGURATOR",
    platformCredentialRequired: false,
    preserveExistingRoutes: true,
  },
  framing: {
    status: "verified-read-only",
    specificationRef: ARUSNAVI_INTERNAL_SPECIFICATION_REF,
    headerVersion: "HEADER2_GPRS_0x23",
    headerBytes: HEADER2_LENGTH,
    maxBufferedBytes: MAX_PACKAGE_LENGTH,
  },
  acknowledgement: {
    header: "server-time-only",
    package: "package-number-only",
  },
  commandTransport: {
    status: "disabled",
    exportedCommandBuilders: 0,
  },
  routeCompatibility: {
    gelios: "parallel-preserved",
    automaticCommandFailover: false,
  },
});

export function tryParseB2Header2(input) {
  assertBuffer(input, "b2_header_buffer_required");
  if (input.length < HEADER2_LENGTH) {
    return Object.freeze({
      status: "incomplete",
      minimumBytes: HEADER2_LENGTH,
    });
  }
  if (input[0] !== HEADER_START) {
    throw new TypeError("b2_header_start_invalid");
  }
  if (input[1] !== HEADER2_GPRS_VERSION) {
    throw new TypeError("b2_header_version_unsupported");
  }

  const identifier = input.readBigUInt64LE(2).toString(10);
  if (!/^\d{15}$/.test(identifier)) {
    throw new TypeError("b2_header_imei_invalid");
  }

  return Object.freeze({
    status: "complete",
    bytesConsumed: HEADER2_LENGTH,
    identifier: Object.freeze({
      kind: "imei",
      value: identifier,
      trust: "claimed-not-ownership-proof",
    }),
    evidence: Object.freeze({
      transport: "tcp",
      bytesObserved: HEADER2_LENGTH,
      framingStatus: "verified",
      specificationRef: ARUSNAVI_INTERNAL_SPECIFICATION_REF,
    }),
  });
}

export function buildB2HeaderAcknowledgement(unixSeconds) {
  const timestamp = normalizeUInt32(
    unixSeconds,
    "b2_header_ack_timestamp_invalid",
  );
  const commandData = Buffer.allocUnsafe(4);
  commandData.writeUInt32LE(timestamp);
  return Buffer.from([
    SERVER_COMMAND_START,
    commandData.length,
    0x00,
    checksum(commandData),
    ...commandData,
    SERVER_COMMAND_END,
  ]);
}

export function tryParseB2Package(input) {
  assertBuffer(input, "b2_package_buffer_required");
  if (input.length === 0) {
    return Object.freeze({ status: "incomplete", minimumBytes: 1 });
  }
  if (input[0] !== PACKAGE_START) {
    throw new TypeError("b2_package_start_invalid");
  }
  if (input.length < 3) {
    return Object.freeze({ status: "incomplete", minimumBytes: 3 });
  }

  const packageNumber = input[1];
  if (
    packageNumber < MIN_PACKAGE_NUMBER
    || packageNumber > MAX_PACKAGE_NUMBER
  ) {
    throw new TypeError("b2_package_number_invalid");
  }

  let offset = 2;
  let packetCount = 0;
  while (true) {
    if (offset >= MAX_PACKAGE_LENGTH) {
      throw new TypeError("b2_package_length_exceeded");
    }
    if (offset >= input.length) {
      return Object.freeze({
        status: "incomplete",
        minimumBytes: offset + 1,
      });
    }
    if (input[offset] === PACKAGE_END) {
      if (packetCount === 0) {
        throw new TypeError("b2_package_empty");
      }
      return Object.freeze({
        status: "complete",
        bytesConsumed: offset + 1,
        packageNumber,
        packetCount,
      });
    }
    if (input.length - offset < 3) {
      return Object.freeze({
        status: "incomplete",
        minimumBytes: offset + 3,
      });
    }

    const dataLength = input.readUInt16LE(offset + 1);
    if (dataLength > MAX_PACKET_DATA_LENGTH) {
      throw new TypeError("b2_packet_data_length_exceeded");
    }
    const packetLength = PACKET_FIXED_LENGTH + dataLength;
    const packetEnd = offset + packetLength;
    if (packetEnd + 1 > MAX_PACKAGE_LENGTH) {
      throw new TypeError("b2_package_length_exceeded");
    }
    if (input.length < packetEnd) {
      return Object.freeze({
        status: "incomplete",
        minimumBytes: packetEnd,
      });
    }

    const expectedChecksum = checksum(
      input.subarray(offset + 3, packetEnd - 1),
    );
    if (input[packetEnd - 1] !== expectedChecksum) {
      throw new TypeError("b2_packet_checksum_invalid");
    }
    packetCount += 1;
    offset = packetEnd;
  }
}

export function buildB2PackageAcknowledgement(packageNumber) {
  const normalized = Number(packageNumber);
  if (
    !Number.isSafeInteger(normalized)
    || normalized < MIN_PACKAGE_NUMBER
    || normalized > MAX_PACKAGE_NUMBER
  ) {
    throw new TypeError("b2_package_ack_number_invalid");
  }
  return Buffer.from([
    SERVER_COMMAND_START,
    0x00,
    normalized,
    SERVER_COMMAND_END,
  ]);
}

export function assertB2ProfileInvariant(profile = ARUSNAVI_B2_MODEL_PROFILE) {
  if (profile.monitoringServerSlots !== 4) {
    throw new TypeError("b2_server_slot_count_invalid");
  }
  if (profile.protocol !== "INTERNAL") {
    throw new TypeError("b2_protocol_invalid");
  }
  if (profile.serverIdentity.kind !== "imei") {
    throw new TypeError("b2_identity_kind_invalid");
  }
  if (profile.framing.status !== "verified-read-only") {
    throw new TypeError("b2_framing_must_be_verified");
  }
  if (
    profile.framing.specificationRef
    !== ARUSNAVI_INTERNAL_SPECIFICATION_REF
  ) {
    throw new TypeError("b2_framing_specification_invalid");
  }
  if (profile.commandTransport.status !== "disabled") {
    throw new TypeError("b2_command_transport_must_be_disabled");
  }
  if (profile.routeCompatibility.gelios !== "parallel-preserved") {
    throw new TypeError("b2_gelios_route_must_be_preserved");
  }
  return true;
}

function checksum(input) {
  let value = 0;
  for (const byte of input) value = (value + byte) & 0xff;
  return value;
}

function normalizeUInt32(value, errorCode) {
  const normalized = Number(value);
  if (
    !Number.isSafeInteger(normalized)
    || normalized < 0
    || normalized > 0xffffffff
  ) {
    throw new TypeError(errorCode);
  }
  return normalized;
}

function assertBuffer(input, errorCode) {
  if (!Buffer.isBuffer(input)) {
    throw new TypeError(errorCode);
  }
}

function deepFreeze(value) {
  if (!value || typeof value !== "object" || Object.isFrozen(value)) {
    return value;
  }
  Object.values(value).forEach(deepFreeze);
  return Object.freeze(value);
}
