import nacl from "tweetnacl";

const utf8 = (s) => new Uint8Array(Buffer.from(s, "utf8"));
const b64 = (u8) => Buffer.from(u8).toString("base64");
const b64url = (u8) => Buffer.from(u8).toString("base64url");
const b64decode = (s) => new Uint8Array(Buffer.from(s, "base64"));
const cat = (...arrs) => {
  const out = new Uint8Array(arrs.reduce((n, a) => n + a.length, 0));
  let o = 0;
  for (const a of arrs) {
    out.set(a, o);
    o += a.length;
  }
  return out;
};

// --- Thrift TBinaryProtocol ---
const T = { BOOL: 2, I32: 8, I64: 10, STRING: 11, STRUCT: 12, LIST: 15 };
const i32be = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
const i64be = (n) => {
  let v = BigInt(n);
  const b = new Array(8);
  for (let i = 7; i >= 0; i--) {
    b[i] = Number(v & 0xffn);
    v >>= 8n;
  }
  return b;
};
const fieldHeader = (type, id) => [type, (id >> 8) & 0xff, id & 0xff];
const STOP = [0];
const tString = (bytes) => [...i32be(bytes.length), ...bytes];
const fStr = (id, s) => [...fieldHeader(T.STRING, id), ...tString([...utf8(s)])];
const fBin = (id, u8) => [...fieldHeader(T.STRING, id), ...tString([...u8])];
const fBool = (id, v) => [...fieldHeader(T.BOOL, id), v ? 1 : 0];
const fI32 = (id, n) => [...fieldHeader(T.I32, id), ...i32be(n)];
const fI64 = (id, n) => [...fieldHeader(T.I64, id), ...i64be(n)];
const fStruct = (id, bytes) => [...fieldHeader(T.STRUCT, id), ...bytes];
const listOfStructs = (id, structs) => [...fieldHeader(T.LIST, id), T.STRUCT, ...i32be(structs.length), ...structs.flat()];

// --- message content variants (MessageEntryContents oneof) ---
// each returns the raw bytes of a MessageEntryHolder{1: MessageEntryContents{<id>: <struct>}}
function holder(variantId, structBytes) {
  const entryContents = [...fStruct(variantId, structBytes), ...STOP];
  return new Uint8Array([...fStruct(1, entryContents), ...STOP]);
}
export const content = {
  text: (text, extra = []) => holder(1, [...fStr(1, text), ...extra, ...STOP]),
  reactionAdd: (sequenceId, emoji, attachmentId) =>
    holder(2, [...fStr(1, String(sequenceId)), ...fStr(2, emoji), ...(attachmentId ? fStr(3, String(attachmentId)) : []), ...STOP]),
  reactionRemove: (sequenceId, emoji, attachmentId) =>
    holder(3, [...fStr(1, String(sequenceId)), ...fStr(2, emoji), ...(attachmentId ? fStr(3, String(attachmentId)) : []), ...STOP]),
  edit: (sequenceId, updatedText) => holder(4, [...fStr(1, String(sequenceId)), ...fStr(2, updatedText), ...STOP]),
  markRead: (sequenceId, seenAtMillis) => holder(5, [...fStr(1, String(sequenceId)), ...fI64(2, seenAtMillis), ...STOP]),
  markUnread: (sequenceId) => holder(6, [...fStr(1, String(sequenceId)), ...STOP]),
  pin: (conversationId) => holder(7, [...fStr(1, conversationId), ...STOP]),
  unpin: (conversationId) => holder(8, [...fStr(1, conversationId), ...STOP]),
  nickname: (userId, nickname) => holder(14, [...fI64(1, userId), ...fStr(2, nickname), ...STOP]),
  screenCapture: (type) => holder(9, [...fI32(1, type), ...STOP]),
  avCallStarted: (broadcastId, isAudioOnly) => holder(16, [...fBool(1, isAudioOnly), ...fStr(3, broadcastId), ...STOP]),
  avCallEnded: (broadcastId, durationSeconds, isAudioOnly, sentAtMillis = Date.now()) =>
    holder(10, [...fI64(1, sentAtMillis), ...fI64(2, durationSeconds), ...fBool(3, isAudioOnly), ...fStr(5, broadcastId), ...STOP]),
  avCallMissed: (isAudioOnly, sentAtMillis = Date.now()) => holder(11, [...fI64(1, sentAtMillis), ...fBool(2, isAudioOnly), ...STOP]),
};

// DmScreenCaptureType
export const SCREEN_CAPTURE = { Unknown: 0, Screenshot: 1, Recording: 2 };

export function messageCreateEvent(frame, ckeyVersion, { shouldNotify = true, ttlMsec } = {}) {
  return new Uint8Array([
    ...fBin(100, frame),
    ...fStr(101, ckeyVersion),
    ...fBool(102, shouldNotify),
    ...(ttlMsec != null ? fI64(103, ttlMsec) : []),
    ...STOP,
  ]);
}

// --- keys ---
const b64urlDecode = (s) => new Uint8Array(Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64"));

export async function generateKeyMaterial() {
  const identity = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
  const signing = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
  const identitySpki = new Uint8Array(await crypto.subtle.exportKey("spki", identity.publicKey));
  const signingSpki = new Uint8Array(await crypto.subtle.exportKey("spki", signing.publicKey));
  const signature = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, signing.privateKey, identitySpki));
  const identityPrivateJwk = await crypto.subtle.exportKey("jwk", identity.privateKey);
  const signingPrivateJwk = await crypto.subtle.exportKey("jwk", signing.privateKey);
  // Juicebox-backed secret = the two P-256 private scalars (identity ‖ signing)
  const juiceboxSecret = cat(b64urlDecode(identityPrivateJwk.d), b64urlDecode(signingPrivateJwk.d));
  return {
    identity,
    signing,
    publicKeyB64: b64(identitySpki),
    signingPublicKeyB64: b64(signingSpki),
    identitySignatureB64: b64(signature),
    identityPrivateJwk,
    signingPrivateJwk,
    juiceboxSecret,
  };
}

// rebuild a P-256 private CryptoKey from its raw 32-byte scalar + the published public key (for SPKI x/y)
async function importPrivFromScalar(scalar, spkiBytes, name, usages) {
  const pub = await crypto.subtle.importKey("spki", spkiBytes, { name, namedCurve: "P-256" }, true, name === "ECDH" ? [] : ["verify"]);
  const { x, y } = await crypto.subtle.exportKey("jwk", pub);
  const d = Buffer.from(scalar).toString("base64url");
  return crypto.subtle.importKey("jwk", { kty: "EC", crv: "P-256", d, x, y, ext: false }, { name, namedCurve: "P-256" }, false, usages);
}

// reconstruct the identity + signing keys from a recovered Juicebox secret (idScalar ‖ signScalar)
export async function recoverIdentityKeys({ publicKeyB64, signingPublicKeyB64, secret }) {
  const idScalar = secret.slice(0, 32);
  const identityKey = await importPrivFromScalar(idScalar, b64decode(publicKeyB64), "ECDH", ["deriveBits"]);
  let signingKey = null;
  if (secret.length >= 64 && signingPublicKeyB64) {
    signingKey = await importPrivFromScalar(secret.slice(32, 64), b64decode(signingPublicKeyB64), "ECDSA", ["sign"]);
  }
  return { identityKey, signingKey };
}

export async function importIdentity({ identityPrivateJwk, signingPrivateJwk }) {
  return {
    identityKey: await crypto.subtle.importKey("jwk", identityPrivateJwk, { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]),
    signingKey: await crypto.subtle.importKey("jwk", signingPrivateJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]),
  };
}

// --- ECIES wrap of the conversation key to a recipient (P-256) ---
export async function eciesWrap(cKey, recipientSpki) {
  const pub = await crypto.subtle.importKey("spki", recipientSpki, { name: "ECDH", namedCurve: "P-256" }, false, []);
  const eph = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
  const z = new Uint8Array(await crypto.subtle.deriveBits({ name: "ECDH", public: pub }, eph.privateKey, 256));
  const ephRaw = new Uint8Array(await crypto.subtle.exportKey("raw", eph.publicKey));
  const out = new Uint8Array(await crypto.subtle.digest("SHA-256", cat(z, new Uint8Array([0, 0, 0, 1]), ephRaw)));
  const key = await crypto.subtle.importKey("raw", out.slice(0, 16), { name: "AES-GCM" }, false, ["encrypt"]);
  const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: out.slice(16, 32) }, key, cKey));
  return b64(cat(ephRaw, ct));
}

// --- body cipher (libsodium crypto_secretbox_easy == tweetnacl secretbox) ---
export function encryptBody(plaintext, cKey) {
  const nonce = crypto.getRandomValues(new Uint8Array(24));
  return cat(nonce, nacl.secretbox(plaintext, nonce, cKey));
}

// --- event signature (ECDSA P-256 / SHA-256), signature_version 7: the format the app uses ---
// payload = ["MessageCreateEvent", conversation_token, sender, conversationId, cKeyVersion, base64url(frame)]
// struct carries signing_public_key (field 4) and a key-info list of just the sender (with its signing key).
export async function eventSignature({ signingKey, signingPublicKeyB64, conversationToken = "", senderId, conversationId, conversationKeyVersion, frame, myVersion }) {
  const payload = ["MessageCreateEvent", conversationToken, String(senderId), String(conversationId), String(conversationKeyVersion), b64url(frame)].join(",");
  const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, signingKey, utf8(payload)));
  const keyInfo = [...fStr(1, String(senderId)), ...fStr(2, String(myVersion)), ...fStr(3, signingPublicKeyB64), ...STOP];
  const struct = new Uint8Array([
    ...fStr(1, b64url(sig)),
    ...fStr(2, String(myVersion)),
    ...fStr(3, "7"),
    ...fStr(4, signingPublicKeyB64),
    ...listOfStructs(5, [keyInfo]),
    ...STOP,
  ]);
  return b64(struct);
}

// the server's encoded_message_event is a Thrift MessageEvent whose field 1 is the sequence_id string
export function readLeadingSequenceId(b64str) {
  try {
    const b = b64decode(b64str);
    if (b[0] === 0x0b && b[1] === 0x00 && b[2] === 0x01) {
      const len = (b[3] << 24) | (b[4] << 16) | (b[5] << 8) | b[6];
      return Buffer.from(b.slice(7, 7 + len)).toString("utf8");
    }
  } catch {}
  return null;
}

const listOfStrings = (id, strings) => [
  ...fieldHeader(T.LIST, id),
  T.STRING,
  ...i32be(strings.length),
  ...strings.flatMap((s) => tString([...utf8(String(s))])),
];

// MessageEventDetail{7: MessageDeleteEvent{1: sequence_ids, 2: delete_message_action}}
export function deleteEventDetail(sequenceIds, actionValue) {
  const deleteEvent = [...listOfStrings(1, sequenceIds), ...fI32(2, actionValue), ...STOP];
  return new Uint8Array([...fStruct(7, deleteEvent), ...STOP]);
}

// build an ActionSignatureInput (a GraphQL JSON object) for admin/mutation actions
export async function buildActionSignature({ signingKey, signingPublicKeyB64, typeName, conversationToken = "", senderId, conversationId, dataElements = [], eventDetailBytes, myVersion }) {
  const payload = [typeName, conversationToken, String(senderId), String(conversationId), ...dataElements.map(String)].join(",");
  const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, signingKey, utf8(payload)));
  return {
    chat_fanout_behavior_version: "V1",
    message_id: crypto.randomUUID(),
    message_event_signature: {
      signature: b64url(sig),
      public_key_version: String(myVersion),
      signature_version: "7",
      signing_public_key: signingPublicKeyB64,
      message_signing_key_info_list: [
        { member_id: String(senderId), public_key_version: String(myVersion), signing_public_key: signingPublicKeyB64 },
      ],
    },
    encoded_message_event_detail: eventDetailBytes ? b64url(eventDetailBytes) : undefined,
    signature_payload: payload,
  };
}

// read a top-level string field by id from a base64 Thrift struct (only walks leading scalar fields)
export function readStringField(b64str, fieldId) {
  try {
    const b = b64decode(b64str);
    let i = 0;
    while (i < b.length) {
      const type = b[i];
      if (type === 0) break;
      const id = (b[i + 1] << 8) | b[i + 2];
      i += 3;
      if (type === 11) {
        const len = (b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3];
        i += 4;
        const val = Buffer.from(b.slice(i, i + len)).toString("utf8");
        i += len;
        if (id === fieldId) return val;
      } else if (type === 2) i += 1;
      else if (type === 8) i += 4;
      else if (type === 10) i += 8;
      else break;
    }
  } catch {}
  return null;
}

// --- Thrift TBinaryProtocol parser (for reading inbound events) ---
function readValue(buf, pos, type) {
  switch (type) {
    case 2: return [buf[pos] !== 0, pos + 1];
    case 3: return [buf[pos], pos + 1];
    case 8: return [(buf[pos] << 24) | (buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3], pos + 4];
    case 10: {
      let v = 0n;
      for (let k = 0; k < 8; k++) v = (v << 8n) | BigInt(buf[pos + k]);
      return [v, pos + 8];
    }
    case 11: {
      const len = (buf[pos] << 24) | (buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3];
      const start = pos + 4;
      return [buf.slice(start, start + len), start + len];
    }
    case 12: return parseStruct(buf, pos);
    case 15: {
      const elemType = buf[pos];
      const count = (buf[pos + 1] << 24) | (buf[pos + 2] << 16) | (buf[pos + 3] << 8) | buf[pos + 4];
      let p = pos + 5;
      const arr = [];
      for (let k = 0; k < count; k++) {
        const [v, np] = readValue(buf, p, elemType);
        arr.push(v);
        p = np;
      }
      return [arr, p];
    }
    case 13: {
      const keyType = buf[pos];
      const valType = buf[pos + 1];
      const count = (buf[pos + 2] << 24) | (buf[pos + 3] << 16) | (buf[pos + 4] << 8) | buf[pos + 5];
      let p = pos + 6;
      const map = {};
      for (let k = 0; k < count; k++) {
        const [key, p1] = readValue(buf, p, keyType);
        const [val, p2] = readValue(buf, p1, valType);
        map[Buffer.from(key).toString("utf8")] = val;
        p = p2;
      }
      return [map, p];
    }
    default: throw new Error(`thrift: unknown type ${type} at ${pos}`);
  }
}
function parseStruct(buf, pos) {
  const fields = {};
  let p = pos;
  while (p < buf.length) {
    const type = buf[p];
    if (type === 0) {
      p += 1;
      break;
    }
    const id = (buf[p + 1] << 8) | buf[p + 2];
    const [val, np] = readValue(buf, p + 3, type);
    fields[id] = val;
    p = np;
  }
  return [fields, p];
}
export function parseThrift(bytes) {
  return parseStruct(bytes, 0)[0];
}
export const thriftStr = (v) => (v == null ? null : Buffer.from(v).toString("utf8"));

// inverse of eciesWrap: recover the cKey with the viewer's identity (ECDH) private key
export async function eciesUnwrap(wrappedB64, identityPrivateKey) {
  const all = b64decode(wrappedB64);
  const ephRaw = all.slice(0, 65);
  const ct = all.slice(65);
  const ephPub = await crypto.subtle.importKey("raw", ephRaw, { name: "ECDH", namedCurve: "P-256" }, false, []);
  const z = new Uint8Array(await crypto.subtle.deriveBits({ name: "ECDH", public: ephPub }, identityPrivateKey, 256));
  const out = new Uint8Array(await crypto.subtle.digest("SHA-256", cat(z, new Uint8Array([0, 0, 0, 1]), ephRaw)));
  const key = await crypto.subtle.importKey("raw", out.slice(0, 16), { name: "AES-GCM" }, false, ["decrypt"]);
  return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: out.slice(16, 32) }, key, ct));
}

export function decryptBody(frame, cKey) {
  return nacl.secretbox.open(frame.slice(24), frame.slice(0, 24), cKey);
}

export function conversationId1on1(a, b) {
  const [x, y] = [BigInt(a), BigInt(b)].sort((p, q) => (p < q ? -1 : 1));
  return `${x}:${y}`;
}

export { b64, b64decode };
