import crypto from 'node:crypto';
import crc32 from 'crc-32';
import { UINT32 } from 'cuint';

const version = '006';
const randomInt = Math.floor(Math.random() * 0xffffffff);
const VERSION_LENGTH = 3;
const APP_ID_LENGTH = 32;

export const priviledges = {
  kJoinChannel: 1,
  kPublishAudioStream: 2,
  kPublishVideoStream: 3,
  kPublishDataStream: 4,
  kRtmLogin: 1000,
};

type Messages = Record<number, number>;

interface MessageOptions {
  salt: number;
  ts: number;
  messages: Messages;
  pack?: () => Buffer;
}

interface AccessTokenContentOptions {
  signature: Buffer | string;
  crc_channel: number;
  crc_uid: number;
  crc_channel_name?: number;
  m: Buffer | string;
  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?: Record<string, string>) => ByteBufInterface;
  putTreeMapUInt32: (map?: Messages) => ByteBufInterface;
}

interface ReadByteBufInterface {
  buffer: Buffer;
  position: number;
  getUint16: () => number;
  getUint32: () => number;
  getString: () => Buffer;
  getTreeMapUInt32: () => Messages;
}

const encodeHMac = (key: string, message: Buffer): Buffer => {
  return crypto.createHmac('sha256', key).update(message).digest();
};
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?: Record<string, string>) {
      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;
    },

    putTreeMapUInt32(map?: Messages) {
      if (!map) {
        that.putUint16(0);
        return that;
      }

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

      return that;
    },
  };

  that.buffer.fill(0);
  return that;
};
const ReadByteBuf = (bytes: Buffer): ReadByteBufInterface => {
  const that: ReadByteBufInterface = {
    buffer: bytes,
    position: 0,

    getUint16() {
      const ret = that.buffer.readUInt16LE(that.position);
      that.position += 2;
      return ret;
    },

    getUint32() {
      const ret = that.buffer.readUInt32LE(that.position);
      that.position += 4;
      return ret;
    },

    getString() {
      const len = that.getUint16();
      const out = Buffer.alloc(len);
      that.buffer.copy(out, 0, that.position, that.position + len);
      that.position += len;
      return out;
    },

    getTreeMapUInt32() {
      const map: Messages = {};
      const len = that.getUint16();
      for (let i = 0; i < len; i++) {
        const key = that.getUint16();
        const value = that.getUint32();
        map[key] = value;
      }
      return map;
    },
  };

  return that;
};
const AccessTokenContent = (options: AccessTokenContentOptions): AccessTokenContentOptions => {
  options.pack = () => {
    const out = ByteBuf();
    return out
      .putBytes(options.signature as Buffer)
      .putUint32(options.crc_channel)
      .putUint32(options.crc_uid)
      .putBytes(options.m as Buffer)
      .pack();
  };

  return options;
};

const unPackContent = (bytes: Buffer): AccessTokenContentOptions => {
  const readbuf = ReadByteBuf(bytes);
  return AccessTokenContent({
    signature: readbuf.getString(),
    crc_channel_name: readbuf.getUint32(),
    crc_uid: readbuf.getUint32(),
    m: readbuf.getString(),
    crc_channel: 0,
  });
};
const Message = (options: MessageOptions): MessageOptions => {
  options.pack = () => {
    const out = ByteBuf();
    return out.putUint32(options.salt).putUint32(options.ts).putTreeMapUInt32(options.messages).pack();
  };

  return options;
};

const unPackMessages = (bytes: Buffer): MessageOptions => {
  const readbuf = ReadByteBuf(bytes);
  return Message({
    salt: readbuf.getUint32(),
    ts: readbuf.getUint32(),
    messages: readbuf.getTreeMapUInt32(),
  });
};
export class AccessToken {
  public appID: string;
  public appCertificate: string;
  public channelName: string;
  public uid: string;
  public messages: Messages;
  public salt: number;
  public ts: number;

  public constructor(appID: string, appCertificate: string, channelName: string, uid: number | string) {
    this.appID = appID;
    this.appCertificate = appCertificate;
    this.channelName = channelName;
    this.messages = {};
    this.salt = randomInt;
    this.ts = Math.floor(new Date().getTime() / 1000) + 24 * 3600;
    if (uid === 0) {
      this.uid = '';
    } else {
      this.uid = `${uid}`;
    }
  }

  public build(): string {
    const m = Message({
      salt: this.salt,
      ts: this.ts,
      messages: this.messages,
    }).pack!();

    const toSign = Buffer.concat([Buffer.from(this.appID, 'utf8'), Buffer.from(this.channelName, 'utf8'), Buffer.from(this.uid, 'utf8'), m]);

    const signature = encodeHMac(this.appCertificate, toSign);
    const crc_channel = UINT32(crc32.str(this.channelName)).and(UINT32(0xffffffff)).toNumber();
    const crc_uid = UINT32(crc32.str(this.uid)).and(UINT32(0xffffffff)).toNumber();
    const content = AccessTokenContent({
      signature,
      crc_channel,
      crc_uid,
      m,
    }).pack!();
    return version + this.appID + content.toString('base64');
  }

  public addPriviledge(priviledge: number, expireTimestamp: number): void {
    this.messages[priviledge] = expireTimestamp;
  }

  public fromString(originToken: string): boolean {
    try {
      const originVersion = originToken.substr(0, VERSION_LENGTH);
      if (originVersion !== version) {
        return false;
      }
      this.appID = originToken.substr(VERSION_LENGTH, APP_ID_LENGTH);
      const originContent = originToken.substr(VERSION_LENGTH + APP_ID_LENGTH);
      const originContentDecodedBuf = Buffer.from(originContent, 'base64');

      const content = unPackContent(originContentDecodedBuf);
      const msgs = unPackMessages(content.m as Buffer);
      this.salt = msgs.salt;
      this.ts = msgs.ts;
      this.messages = msgs.messages;
    } catch (err) {
      console.log(err);
      return false;
    }

    return true;
  }
}

export { version };
