/**
 * Qoder COSY (hybrid RSA+AES+MD5) signing, ported from 9router's
 * shared/qoder/cosy.js (originally CLIProxyAPIPlus qoder-provider branch,
 * internal/auth/qoder/cosy.go).
 *
 * Every signed request carries:
 *   - an AES-128-CBC payload of the user info, the AES key wrapped in RSA
 *   - an MD5 signature over `payload || cosyKey || timestamp || body || sigPath`
 *   - the body's MD5 hash + length so the server can validate integrity
 *   - a set of Cosy-* / X-* headers fingerprinting the client (machine id,
 *     IDE version, organization id, etc.)
 *
 * The on-the-wire header keys use the same casing as qodercli:
 *   Cosy-Machineid, not Cosy-MachineID.
 */

import {
  constants,
  createCipheriv,
  createHash,
  publicEncrypt,
  randomUUID,
} from "node:crypto";

import {
  QODER_CLIENT_TYPE,
  QODER_DATA_POLICY,
  QODER_IDE_VERSION,
  QODER_LOGIN_VERSION,
  QODER_MACHINE_OS,
  QODER_MACHINE_TYPE,
  QODER_RSA_PUBLIC_KEY,
} from "./constants";

export interface CosyCreds {
  userId: string;
  authToken: string;
  name?: string;
  email?: string;
  machineId?: string;
}

// AES-128 wants a 16-byte key. Match qodercli/Veria: take the first 16 chars
// of a fresh UUID's canonical string (hyphens included). The key is fresh
// per request so even though the IV reuses the key bytes, each request still
// has a unique IV.
function generateAesKey(): string {
  return randomUUID().slice(0, 16);
}

function pkcs7Pad(data: Buffer, blockSize: number): Buffer {
  const padding = blockSize - (data.length % blockSize);
  const padded = Buffer.alloc(data.length + padding, padding);
  data.copy(padded, 0);
  return padded;
}

function aesEncryptCbcBase64(plaintext: string, keyStr: string): string {
  const keyBytes = Buffer.from(keyStr, "utf8");
  if (keyBytes.length !== 16) {
    throw new Error(`aes key must be 16 bytes, got ${keyBytes.length}`);
  }
  const iv = keyBytes.subarray(0, 16);
  const cipher = createCipheriv("aes-128-cbc", keyBytes, iv);
  cipher.setAutoPadding(false);
  const padded = pkcs7Pad(Buffer.from(plaintext, "utf8"), 16);
  const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
  return encrypted.toString("base64");
}

function rsaEncryptBase64(data: string): string {
  const encrypted = publicEncrypt(
    { key: QODER_RSA_PUBLIC_KEY, padding: constants.RSA_PKCS1_PADDING },
    Buffer.from(data, "utf8"),
  );
  return encrypted.toString("base64");
}

function encryptUserInfo(userInfo: Record<string, string>): {
  cosyKey: string;
  info: string;
} {
  const aesKey = generateAesKey();
  const plaintext = JSON.stringify(userInfo);
  const infoB64 = aesEncryptCbcBase64(plaintext, aesKey);
  const cosyKeyB64 = rsaEncryptBase64(aesKey);
  return { cosyKey: cosyKeyB64, info: infoB64 };
}

function md5Hex(input: Buffer): string {
  return createHash("md5").update(input).digest("hex");
}

/**
 * Strip the leading "/algo" prefix from the request path. Matches qodercli
 * convention. Empty input returns "".
 */
function computeSigPath(requestUrl: string): string {
  let pathname: string;
  try {
    pathname = new URL(requestUrl).pathname || "";
  } catch {
    return "";
  }
  if (pathname.startsWith("/algo")) {
    return pathname.slice("/algo".length);
  }
  return pathname;
}

/**
 * Generate a fresh machine UUID. Persisted on the connection record so
 * every request from the same auth carries the same machineId.
 */
export function generateMachineId(): string {
  return randomUUID();
}

/**
 * Build the full Cosy-* header set for a single Qoder request.
 *
 * @param body The exact bytes that will be sent. For GET requests pass an
 *   empty Buffer.
 * @param requestUrl Full request URL (used for sigPath).
 * @param creds Credentials used for signing.
 * @returns Header map ready to merge onto fetch().
 */
export function buildCosyHeaders(
  body: Buffer | Uint8Array | string,
  requestUrl: string,
  creds: CosyCreds,
): Record<string, string> {
  if (!creds?.userId) throw new Error("cosy: user id is empty");
  if (!creds?.authToken) throw new Error("cosy: auth token is empty");

  const bodyBuf = Buffer.isBuffer(body)
    ? body
    : typeof body === "string"
      ? Buffer.from(body, "latin1")
      : Buffer.from(body || []);

  const { cosyKey, info } = encryptUserInfo({
    uid: creds.userId,
    security_oauth_token: creds.authToken,
    name: creds.name || "",
    aid: "",
    email: creds.email || "",
  });

  const timestamp = String(Math.floor(Date.now() / 1000));
  const requestId = randomUUID();

  const payloadJson = JSON.stringify({
    version: "v1",
    requestId,
    info,
    cosyVersion: QODER_IDE_VERSION,
    ideVersion: "",
  });
  const payloadB64 = Buffer.from(payloadJson, "utf8").toString("base64");

  const sigPath = computeSigPath(requestUrl);
  const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${bodyBuf.toString("latin1")}\n${sigPath}`;
  const sig = md5Hex(Buffer.from(sigInput, "latin1"));

  const machineId = creds.machineId || generateMachineId();
  const bodyHash = md5Hex(bodyBuf);
  const bodyLength = String(bodyBuf.length);

  return {
    Authorization: `Bearer COSY.${payloadB64}.${sig}`,
    "Cosy-Key": cosyKey,
    "Cosy-User": creds.userId,
    "Cosy-Date": timestamp,
    "Cosy-Version": QODER_IDE_VERSION,
    "Cosy-Machineid": machineId,
    "Cosy-Machinetoken": machineId,
    "Cosy-Machinetype": QODER_MACHINE_TYPE,
    "Cosy-Machineos": QODER_MACHINE_OS,
    "Cosy-Clienttype": QODER_CLIENT_TYPE,
    "Cosy-Clientip": "127.0.0.1",
    "Cosy-Bodyhash": bodyHash,
    "Cosy-Bodylength": bodyLength,
    "Cosy-Sigpath": sigPath,
    "Cosy-Data-Policy": QODER_DATA_POLICY,
    "Cosy-Organization-Id": "",
    "Cosy-Organization-Tags": "",
    "Login-Version": QODER_LOGIN_VERSION,
    "X-Request-Id": randomUUID(),
  };
}
