import { reactive } from "vue";
import type {
  ClientMessage,
  EncryptedEnvelope,
  RecordSummary,
  ServerMessage,
  SessionCrypto,
  WireMessage
} from "./types";

const protocolVersion = 1;
export const wsUrl = import.meta.env.VITE_WS_URL ?? "ws://localhost:8080/ws";

export const connectionState = reactive<{
  records: RecordSummary[];
  status: string;
}>({
  records: [],
  status: "Nicht verbunden"
});

export const liveUpdateState = reactive<{
  revision: number;
  lastTitle: string;
  lastUpdatedAt: string | null;
}>({
  revision: 0,
  lastTitle: "",
  lastUpdatedAt: null
});

let connectionStarted = false;
let reconnectTimer: number | undefined;

export function ensureConnection() {
  if (connectionStarted) return;
  connectionStarted = true;
  connect();
}

export function stopConnection() {
  connectionStarted = false;
  if (reconnectTimer !== undefined) {
    window.clearTimeout(reconnectTimer);
    reconnectTimer = undefined;
  }
  connectionState.records = [];
  connectionState.status = "Nicht verbunden";
}

function connect() {
  const socket = new WebSocket(wsUrl);
  let session: SessionCrypto | null = null;

  socket.addEventListener("open", async () => {
    session = await createSessionCrypto();
    connectionState.status = "Handshake...";
    socket.send(JSON.stringify({
      type: "hello",
      payload: {
        protocol_version: protocolVersion,
        key_id: session.keyId,
        session_key: session.exportedKey
      }
    } satisfies WireMessage));
  });

  socket.addEventListener("message", async (event) => {
    const wireMessage = JSON.parse(event.data) as WireMessage;
    if (wireMessage.type === "hello_ack") {
      connectionState.status = "Verbunden";
      if (session) {
        const envelope = await encryptMessage(session, {
          type: "subscribe",
          payload: { topic: "records" }
        });
        socket.send(JSON.stringify({ type: "encrypted", payload: envelope } satisfies WireMessage));
      }
      return;
    }

    if (wireMessage.type === "encrypted" && session) {
      const message = await decryptMessage<ServerMessage>(session, wireMessage.payload);
      applyMessage(message);
      return;
    }

    if (wireMessage.type === "error") {
      connectionState.status = wireMessage.payload.message;
    }
  });

  socket.addEventListener("close", () => {
    connectionStarted = false;
    connectionState.status = "Verbindung getrennt, neuer Versuch...";
    reconnectTimer = window.setTimeout(ensureConnection, 1500);
  });

  socket.addEventListener("error", () => {
    connectionState.status = "Socket-Fehler";
  });
}

async function createSessionCrypto(): Promise<SessionCrypto> {
  const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
    "encrypt",
    "decrypt"
  ]);
  const rawKey = await crypto.subtle.exportKey("raw", key);

  return {
    key,
    keyId: crypto.randomUUID(),
    exportedKey: bytesToBase64(new Uint8Array(rawKey))
  };
}

async function encryptMessage(session: SessionCrypto, message: ClientMessage) {
  const nonce = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(JSON.stringify(message));
  const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, session.key, encoded);

  return {
    enc: `aes-256-gcm-v${protocolVersion}`,
    key_id: session.keyId,
    nonce: bytesToBase64(nonce),
    ciphertext: bytesToBase64(new Uint8Array(ciphertext))
  } satisfies EncryptedEnvelope;
}

async function decryptMessage<T>(session: SessionCrypto, envelope: EncryptedEnvelope): Promise<T> {
  const plaintext = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv: base64ToBytes(envelope.nonce) },
    session.key,
    base64ToBytes(envelope.ciphertext)
  );
  return JSON.parse(new TextDecoder().decode(plaintext)) as T;
}

function bytesToBase64(bytes: Uint8Array) {
  let binary = "";
  bytes.forEach((byte) => {
    binary += String.fromCharCode(byte);
  });
  return btoa(binary);
}

function base64ToBytes(value: string) {
  const binary = atob(value);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }
  return bytes;
}

function applyMessage(message: ServerMessage) {
  if (message.type === "snapshot") {
    connectionState.records = message.payload.records;
    connectionState.status = "Verbunden";
  }

  if (message.type === "record_changed") {
    const index = connectionState.records.findIndex((record) => record.id === message.payload.record.id);
    if (index >= 0) {
      connectionState.records[index] = message.payload.record;
    } else {
      connectionState.records.unshift(message.payload.record);
    }
    connectionState.status = "Aktualisiert";
    liveUpdateState.revision += 1;
    liveUpdateState.lastTitle = message.payload.record.title;
    liveUpdateState.lastUpdatedAt = message.payload.record.updated_at;
  }

  if (message.type === "error") {
    connectionState.status = message.payload.message;
  }
}
