import type { Encrypted, IEncryption, CreateRotator } from "./types";
import { isValidHexKey } from "./util";

const ALGORITHM = "AES-GCM";
const IV_LENGTH = 12;

function chunks(str: string, size: number) {
  const chunks: string[] = [];
  for (let i = 0; i < str.length; i += size) {
    chunks.push(str.slice(i, i + size));
  }
  return chunks;
}

function hexToBytes(hex: string) {
  return chunks(hex, 2).map((byte) => parseInt(byte, 16));
}

function bytesToHex(bytes: number[]) {
  return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

export class Cipher implements IEncryption {
  private secretKeyHex: string;
  private secretKey: CryptoKey | null;

  static createRotator: CreateRotator = (oldSecretKey, newSecretKey) => {
    const oldCipher = new Cipher(oldSecretKey);
    const newCipher = new Cipher(newSecretKey);

    const rotator = async (prevEncrypted: Encrypted): Promise<Encrypted> => {
      const deciphered = await oldCipher.decrypt(prevEncrypted);
      return await newCipher.encrypt(deciphered);
    };

    return rotator;
  };

  constructor(secretKeyHex: string) {
    if (!isValidHexKey(secretKeyHex)) {
      throw new Error("Secret key must be a valid hex string");
    }
    this.secretKeyHex = secretKeyHex;
    this.secretKey = null;
  }

  private async getSecretKey(): Promise<CryptoKey> {
    if (!this.secretKey) {
      this.secretKey = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(this.secretKeyHex),
        { name: ALGORITHM },
        false,
        ["encrypt", "decrypt"]
      );
    }

    return this.secretKey;
  }

  async encrypt(text: string): Promise<Encrypted> {
    const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
    const encoded = new TextEncoder().encode(text);

    const encrypted = await crypto.subtle.encrypt(
      {
        name: ALGORITHM,
        iv,
      },
      await this.getSecretKey(),
      encoded
    );

    return {
      iv: bytesToHex(Array.from(iv)),
      content: bytesToHex(Array.from(new Uint8Array(encrypted))),
      authTag: "",
    };
  }

  async decrypt(encrypted: Encrypted): Promise<string> {
    const iv = new Uint8Array(hexToBytes(encrypted.iv));
    const content = new Uint8Array(hexToBytes(encrypted.content));

    const decrypted = await crypto.subtle.decrypt(
      {
        name: ALGORITHM,
        iv,
      },
      await this.getSecretKey(),
      content
    );

    return new TextDecoder().decode(decrypted);
  }
}
