import crypto from 'node:crypto';

const version = '005';
export const noUpload = '0';
export const audioVideoUpload = '3';

// Service Type
const MEDIA_CHANNEL_SERVICE = 1;
const RECORDING_SERVICE = 2;
const PUBLIC_SHARING_SERVICE = 3;
const IN_CHANNEL_PERMISSION = 4;

// InChannelPermissionKey
const ALLOW_UPLOAD_IN_CHANNEL = 1;

type ExtraMap = Record<number, string>;

interface MessageOptions {
  serviceType: number;
  appID: Buffer;
  unixTs: number;
  salt: number;
  channelName: string;
  uid: number;
  expiredTs: number;
  extra?: ExtraMap;
  pack?: () => Buffer;
}

interface DynamicKey5ContentOptions {
  serviceType: number;
  signature: string;
  appID: Buffer;
  unixTs: number;
  salt: number;
  expiredTs: number;
  extra?: ExtraMap;
  pack?: () => Buffer;
}

interface ByteBufInterface {
  buffer: Buffer;
  position: number;
  pack: () => Buffer;
  putUint16: (v: number) => ByteBufInterface;
  putUint32: (v: number) => ByteBufInterface;
  putBytes: (bytes: Buffer) => ByteBufInterface;
  putString: (str: string) => ByteBufInterface;
  putTreeMap: (map?: ExtraMap) => ByteBufInterface;
}

const ByteBuf = (): ByteBufInterface => {
  const that: ByteBufInterface = {
    buffer: Buffer.alloc(1024),
    position: 0,

    pack() {
      const out = Buffer.alloc(that.position);
      that.buffer.copy(out, 0, 0, out.length);
      return out;
    },

    putUint16(v: number) {
      that.buffer.writeUInt16LE(v, that.position);
      that.position += 2;
      return that;
    },

    putUint32(v: number) {
      that.buffer.writeUInt32LE(v, that.position);
      that.position += 4;
      return that;
    },

    putBytes(bytes: Buffer) {
      that.putUint16(bytes.length);
      bytes.copy(that.buffer, that.position);
      that.position += bytes.length;
      return that;
    },

    putString(str: string) {
      return that.putBytes(Buffer.from(str));
    },

    putTreeMap(map?: ExtraMap) {
      if (!map) {
        that.putUint16(0);
        return that;
      }

      that.putUint16(Object.keys(map).length);
      for (const key in map) {
        that.putUint16(parseInt(key, 10));
        that.putString(map[key]);
      }

      return that;
    },
  };

  that.buffer.fill(0);
  return that;
};
const hexDecode = (str: string): Buffer => {
  return Buffer.from(str, 'hex');
};
const encodeHMac = (key: Buffer, message: Buffer): string => {
  return crypto.createHmac('sha1', key).update(message).digest('hex').toUpperCase();
};
const Message = (options: MessageOptions): MessageOptions => {
  options.pack = () => {
    const out = ByteBuf();
    return out
      .putUint16(options.serviceType)
      .putBytes(options.appID)
      .putUint32(options.unixTs)
      .putUint32(options.salt)
      .putString(options.channelName)
      .putUint32(options.uid)
      .putUint32(options.expiredTs)
      .putTreeMap(options.extra)
      .pack();
  };

  return options;
};
const generateSignature5 = (
  appCertificate: string,
  serviceType: number,
  appID: string,
  unixTs: number,
  randomInt: number,
  channelName: string,
  uid: number,
  expiredTs: number,
  extra?: ExtraMap
): string => {
  const rawAppID = hexDecode(appID);
  const rawAppCertificate = hexDecode(appCertificate);

  const m = Message({
    serviceType,
    appID: rawAppID,
    unixTs,
    salt: randomInt,
    channelName,
    uid,
    expiredTs,
    extra,
  });

  const toSign = m.pack!();
  return encodeHMac(rawAppCertificate, toSign);
};

const DynamicKey5Content = (options: DynamicKey5ContentOptions): DynamicKey5ContentOptions => {
  options.pack = () => {
    const out = ByteBuf();
    return out
      .putUint16(options.serviceType)
      .putString(options.signature)
      .putBytes(options.appID)
      .putUint32(options.unixTs)
      .putUint32(options.salt)
      .putUint32(options.expiredTs)
      .putTreeMap(options.extra)
      .pack();
  };

  return options;
};

export const generateDynamicKey = (
  appID: string,
  appCertificate: string,
  channelName: string,
  unixTs: number,
  randomInt: number,
  uid: number,
  expiredTs: number,
  extra?: ExtraMap,
  serviceType: number = MEDIA_CHANNEL_SERVICE
): string => {
  const signature = generateSignature5(appCertificate, serviceType, appID, unixTs, randomInt, channelName, uid, expiredTs, extra);
  const content = DynamicKey5Content({
    serviceType,
    signature,
    appID: hexDecode(appID),
    unixTs,
    salt: randomInt,
    expiredTs,
    extra,
  }).pack!();
  return version + content.toString('base64');
};
export const generatePublicSharingKey = (
  appID: string,
  appCertificate: string,
  channelName: string,
  unixTs: number,
  randomInt: number,
  uid: number,
  expiredTs: number
): string => {
  const channelNameStr = channelName.toString();
  return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, PUBLIC_SHARING_SERVICE);
};

export const generateRecordingKey = (
  appID: string,
  appCertificate: string,
  channelName: string,
  unixTs: number,
  randomInt: number,
  uid: number,
  expiredTs: number
): string => {
  const channelNameStr = channelName.toString();
  return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, RECORDING_SERVICE);
};

export const generateMediaChannelKey = (
  appID: string,
  appCertificate: string,
  channelName: string,
  unixTs: number,
  randomInt: number,
  uid: number,
  expiredTs: number
): string => {
  const channelNameStr = channelName.toString();
  return generateDynamicKey(appID, appCertificate, channelNameStr, unixTs, randomInt, uid, expiredTs, undefined, MEDIA_CHANNEL_SERVICE);
};

export const generateInChannelPermissionKey = (
  appID: string,
  appCertificate: string,
  channelName: string,
  unixTs: number,
  randomInt: number,
  uid: number,
  expiredTs: number,
  permission: string
): string => {
  const extra: ExtraMap = {};
  extra[ALLOW_UPLOAD_IN_CHANNEL] = permission;
  return generateDynamicKey(appID, appCertificate, channelName, unixTs, randomInt, uid, expiredTs, extra, IN_CHANNEL_PERMISSION);
};

export { version };
