/**
 * GT06 GPS-tracker protocol codec.
 *
 * Frame: 0x78 0x78 | len(1) | protocol(1) | content(N) | serial(2) | crc(2) | 0x0D 0x0A
 * `len` counts protocol..crc (= 5 + N). CRC-ITU (CRC-16/X.25) is computed over
 * len..serial (inclusive) and equals the 2 crc bytes.
 */

const START = 0x7878;
const STOP = 0x0d0a;

export const GT06_PROTOCOL = {
  LOGIN: 0x01,
  LOCATION: 0x12,
  HEARTBEAT: 0x13,
  STRING: 0x15,
  ALARM: 0x16,
  ADDRESS_BY_PHONE: 0x1a,
  SERVER_COMMAND: 0x80,
} as const;

/** CRC-16/X.25 (a.k.a. CRC-ITU) used by GT06 — reflected, poly 0x8408, init/xorout 0xFFFF. */
export function crcItu(bytes: Buffer): number {
  let fcs = 0xffff;
  for (const b of bytes) {
    fcs ^= b;
    for (let i = 0; i < 8; i++) {
      fcs = fcs & 1 ? (fcs >> 1) ^ 0x8408 : fcs >> 1;
    }
  }
  return (~fcs) & 0xffff;
}

export interface Gt06Gps {
  time: string; // ISO (UTC)
  satellites: number;
  latitude: number;
  longitude: number;
  speed: number; // km/h
  course: number; // 0-360
  positioned: boolean;
}

export interface Gt06Lbs {
  mcc: number;
  mnc: number;
  lac: number;
  cellId: number;
}

export interface Gt06Status {
  terminalInfo: number;
  voltageLevel: number;
  gsmLevel: number;
  alarm: number; // former byte of alarm/language
  charging: boolean;
  accOn: boolean;
  gpsTracking: boolean;
  oilCut: boolean;
}

export type Gt06Packet =
  | { type: 'login'; protocol: number; serial: number; imei: string }
  | { type: 'location'; protocol: number; serial: number; gps: Gt06Gps; lbs: Gt06Lbs }
  | { type: 'heartbeat'; protocol: number; serial: number; status: Gt06Status }
  | { type: 'alarm'; protocol: number; serial: number; gps: Gt06Gps; lbs: Gt06Lbs; status: Gt06Status }
  | { type: 'unknown'; protocol: number; serial: number };

/** Terminal ID (8 BCD bytes) → 15-digit IMEI (drops the leading pad nibble). */
function decodeImei(buf: Buffer): string {
  return buf.toString('hex').replace(/^0/, '');
}

function decodeDateTime(buf: Buffer, off: number): string {
  const year = 2000 + buf[off];
  const month = buf[off + 1];
  const day = buf[off + 2];
  const hour = buf[off + 3];
  const min = buf[off + 4];
  const sec = buf[off + 5];
  return new Date(Date.UTC(year, month - 1, day, hour, min, sec)).toISOString();
}

/** Convert a GT06 lat/long raw uint32 to decimal degrees (magnitude only). */
function rawToDegrees(raw: number): number {
  return raw / 30000 / 60;
}

function decodeGps(buf: Buffer, off: number): Gt06Gps {
  const time = decodeDateTime(buf, off);
  const lenSat = buf[off + 6];
  const satellites = lenSat & 0x0f;
  const latRaw = buf.readUInt32BE(off + 7);
  const lonRaw = buf.readUInt32BE(off + 11);
  const speed = buf[off + 15];
  const cs = buf.readUInt16BE(off + 16);
  const hi = (cs >> 8) & 0xff;
  const positioned = Boolean(hi & 0x10); // BYTE_1 Bit4
  const isWest = Boolean(hi & 0x08); // BYTE_1 Bit3 (1 = West)
  const isNorth = Boolean(hi & 0x04); // BYTE_1 Bit2 (1 = North)
  const course = cs & 0x03ff; // BYTE_1 Bit1-0 + BYTE_2
  let latitude = rawToDegrees(latRaw);
  let longitude = rawToDegrees(lonRaw);
  if (!isNorth) latitude = -latitude;
  if (isWest) longitude = -longitude;
  return { time, satellites, latitude, longitude, speed, course, positioned };
}

function decodeStatus(buf: Buffer, off: number): Gt06Status {
  const terminalInfo = buf[off];
  const voltageLevel = buf[off + 1];
  const gsmLevel = buf[off + 2];
  const alarm = buf[off + 3]; // alarm/language former byte
  return {
    terminalInfo,
    voltageLevel,
    gsmLevel,
    alarm,
    oilCut: Boolean(terminalInfo & 0x80),
    gpsTracking: Boolean(terminalInfo & 0x40),
    charging: Boolean(terminalInfo & 0x04),
    accOn: Boolean(terminalInfo & 0x02),
  };
}

function decodeLbs(buf: Buffer, off: number): Gt06Lbs {
  return {
    mcc: buf.readUInt16BE(off),
    mnc: buf[off + 2],
    lac: buf.readUInt16BE(off + 3),
    cellId: buf.readUIntBE(off + 5, 3),
  };
}

function decodeFrame(frame: Buffer): Gt06Packet | null {
  // frame = 78 78 len ...content... serial(2) crc(2) 0D 0A
  const len = frame[2];
  const protocol = frame[3];
  const serialOff = 3 + (len - 4); // after protocol + content, before serial(2)+crc(2)
  const serial = frame.readUInt16BE(serialOff);
  const contentOff = 4; // start of content (after protocol)

  switch (protocol) {
    case GT06_PROTOCOL.LOGIN:
      return { type: 'login', protocol, serial, imei: decodeImei(frame.subarray(contentOff, contentOff + 8)) };
    case GT06_PROTOCOL.LOCATION:
      return { type: 'location', protocol, serial, gps: decodeGps(frame, contentOff), lbs: decodeLbs(frame, contentOff + 18) };
    case GT06_PROTOCOL.HEARTBEAT:
      return { type: 'heartbeat', protocol, serial, status: decodeStatus(frame, contentOff) };
    case GT06_PROTOCOL.ALARM: {
      const gps = decodeGps(frame, contentOff);
      // content: date(6)+lenSat(1)+lat(4)+lng(4)+speed(1)+course(2)=18, lbsLen(1), lbs(8), status(1+1+1+2)
      const lbs = decodeLbs(frame, contentOff + 18 + 1);
      const status = decodeStatus(frame, contentOff + 18 + 1 + 8);
      return { type: 'alarm', protocol, serial, gps, lbs, status };
    }
    default:
      return { type: 'unknown', protocol, serial };
  }
}

/**
 * Pull all complete frames out of a stream buffer. Returns the decoded packets
 * (skipping CRC-failed ones) and the trailing bytes that form a partial frame.
 */
export function parseStream(buffer: Buffer): { packets: Gt06Packet[]; rest: Buffer } {
  const packets: Gt06Packet[] = [];
  let i = 0;
  while (i + 5 <= buffer.length) {
    if (buffer.readUInt16BE(i) !== START) {
      i += 1; // resync
      continue;
    }
    const len = buffer[i + 2];
    const frameLen = 2 + 1 + len + 2; // start + lenByte + (protocol..crc) + stop
    if (i + frameLen > buffer.length) break; // incomplete
    const frame = buffer.subarray(i, i + frameLen);
    if (frame.readUInt16BE(frameLen - 2) === STOP) {
      // CRC over len..serial (frame[2 .. frameLen-4]); crc bytes are frameLen-4..frameLen-3.
      const crcCalc = crcItu(frame.subarray(2, frameLen - 4));
      const crcRecv = frame.readUInt16BE(frameLen - 4);
      if (crcCalc === crcRecv) {
        const pkt = decodeFrame(frame);
        if (pkt) packets.push(pkt);
      }
      i += frameLen;
    } else {
      i += 1; // bad frame, resync
    }
  }
  return { packets, rest: buffer.subarray(i) };
}

/** Build a server → terminal ACK (login/heartbeat/alarm) echoing the serial. */
export function buildAck(protocol: number, serial: number): Buffer {
  const body = Buffer.alloc(3); // protocol + serial(2)
  body[0] = protocol;
  body.writeUInt16BE(serial, 1);
  const len = body.length + 2; // + crc(2)
  const forCrc = Buffer.concat([Buffer.from([len]), body]);
  const crc = crcItu(forCrc);
  return Buffer.concat([
    Buffer.from([0x78, 0x78, len]),
    body,
    Buffer.from([(crc >> 8) & 0xff, crc & 0xff, 0x0d, 0x0a]),
  ]);
}
