/**
 * WeChat media handling — AES-128-ECB encryption/decryption for CDN.
 *
 * WeChat CDN requires media to be encrypted before upload and
 * decrypted after download using AES-128-ECB with PKCS7 padding.
 */

import crypto from "crypto";
import type { WeixinCredentials, MessageItem, CDNMedia } from "./weixin-types";
import { MessageItemType, UploadMediaType } from "./weixin-types";
import { getUploadUrl } from "./weixin-api";
import { silkToWav } from "./silk-transcode";

const MAX_MEDIA_SIZE = 100 * 1024 * 1024; // 100 MB

/**
 * Detect image MIME type from magic bytes (first few bytes of the file).
 * Falls back to 'image/jpeg' if no known signature matches.
 *
 * Signatures from https://en.wikipedia.org/wiki/List_of_file_signatures
 */
export function detectImageMime(data: Buffer): string {
  if (data.length < 4) return "image/jpeg";
  // PNG: 89 50 4E 47
  if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
    return "image/png";
  }
  // GIF: 47 49 46 38
  if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x38) {
    return "image/gif";
  }
  // WebP: 52 49 46 46 ... 57 45 42 50
  if (
    data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 &&
    data.length >= 12 &&
    data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50
  ) {
    return "image/webp";
  }
  // JPEG: FF D8 FF
  if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
    return "image/jpeg";
  }
  // BMP: 42 4D
  if (data[0] === 0x42 && data[1] === 0x4d) {
    return "image/bmp";
  }
  return "image/jpeg"; // default fallback
}

/** Map MIME type to file extension */
function mimeToExt(mimeType: string): string {
  const map: Record<string, string> = {
    "image/png": ".png",
    "image/jpeg": ".jpg",
    "image/gif": ".gif",
    "image/webp": ".webp",
    "image/bmp": ".bmp",
  };
  return map[mimeType] || ".jpg";
}

/**
 * Generate a random 16-byte AES key.
 */
export function generateMediaKey(): Buffer {
  return crypto.randomBytes(16);
}

/**
 * AES-128-ECB encrypt with PKCS7 padding.
 */
export function encryptMedia(data: Buffer, key: Buffer): Buffer {
  const cipher = crypto.createCipheriv("aes-128-ecb", key, null);
  return Buffer.concat([cipher.update(data), cipher.final()]);
}

/**
 * AES-128-ECB decrypt with PKCS7 unpadding.
 */
export function decryptMedia(data: Buffer, key: Buffer): Buffer {
  const decipher = crypto.createDecipheriv("aes-128-ecb", key, null);
  return Buffer.concat([decipher.update(data), decipher.final()]);
}

/**
 * Compute padded ciphertext size for AES-128-ECB.
 */
export function aesEcbPaddedSize(plaintextSize: number): number {
  return Math.ceil((plaintextSize + 1) / 16) * 16;
}

/**
 * Parse AES key from a message item.
 *
 * WeChat ships the AES-128 key in three different encodings depending on the
 * media type:
 *   1. `aeskey` field as raw hex string (32 ASCII chars)        — some legacy items
 *   2. `media.aes_key` as base64(raw 16 bytes)                  — IMAGE
 *   3. `media.aes_key` as base64(hex string of 16 bytes)        — FILE / VOICE / VIDEO
 *
 * Without case (3) handling, file/voice/video downloads would receive a
 * 32-byte buffer and crypto.createDecipheriv would throw "Invalid key length".
 */
function parseAesKey(item: {
  aeskey?: string;
  media?: CDNMedia;
}): Buffer | null {
  // (1) hex aeskey field
  if (
    item.aeskey &&
    item.aeskey.length === 32 &&
    /^[0-9a-fA-F]{32}$/.test(item.aeskey)
  ) {
    return Buffer.from(item.aeskey, "hex");
  }

  const b64 = item.media?.aes_key;
  if (!b64) return null;

  const decoded = Buffer.from(b64, "base64");

  // (2) base64(raw 16 bytes) — image
  if (decoded.length === 16) {
    return decoded;
  }

  // (3) base64(hex string of 16 bytes) — file/voice/video
  if (
    decoded.length === 32 &&
    /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))
  ) {
    return Buffer.from(decoded.toString("ascii"), "hex");
  }

  console.warn(
    `[Weixin] parseAesKey: unrecognized aes_key encoding, decoded length=${decoded.length}`,
  );
  return null;
}

/**
 * Download and decrypt media from CDN.
 */
export async function downloadAndDecryptMedia(
  cdnUrl: string,
  aesKey: Buffer,
  label: string = "media",
): Promise<Buffer> {
  const res = await fetch(cdnUrl, {
    signal: AbortSignal.timeout(60_000),
  });

  if (!res.ok) {
    throw new Error(`CDN download failed for ${label}: ${res.status}`);
  }

  const encrypted = Buffer.from(await res.arrayBuffer());

  if (encrypted.length > MAX_MEDIA_SIZE) {
    throw new Error(
      `Media too large: ${encrypted.length} bytes (max ${MAX_MEDIA_SIZE})`,
    );
  }

  return decryptMedia(encrypted, aesKey);
}

/**
 * Extract downloadable media from a message item.
 * Returns null if no media is present or key is missing.
 */
export async function downloadMediaFromItem(
  item: MessageItem,
  cdnBaseUrl: string,
): Promise<{ data: Buffer; mimeType: string; filename: string } | null> {
  let encryptParam: string | undefined;
  let aesKey: Buffer | null = null;
  let mimeType = "application/octet-stream";
  let filename = "file";

  switch (item.type) {
    case MessageItemType.IMAGE:
      if (item.image_item) {
        encryptParam = item.image_item.media?.encrypt_query_param;
        aesKey = parseAesKey(
          item.image_item as { aeskey?: string; media?: CDNMedia },
        );
        // mimeType/filename will be corrected after decryption via magic bytes
        // (defaults set here are fallbacks only)
        mimeType = "image/jpeg";
        filename = `image_${Date.now()}.jpg`;
      }
      break;

    case MessageItemType.VOICE:
      if (item.voice_item) {
        encryptParam = item.voice_item.media?.encrypt_query_param;
        aesKey = parseAesKey(
          item.voice_item as { aeskey?: string; media?: CDNMedia },
        );
        mimeType = "audio/silk";
        filename = `voice_${Date.now()}.silk`;
        console.log(
          `[Weixin] VOICE item: encryptParam=${encryptParam ? "OK(" + encryptParam.length + ")" : "MISSING"} aesKey=${aesKey ? aesKey.length + "B" : "MISSING"} aes_key_b64=${item.voice_item.media?.aes_key?.slice(0, 20) ?? "(none)"}`,
        );
      } else {
        console.warn(
          "[Weixin] VOICE item has no voice_item field:",
          JSON.stringify(item).slice(0, 200),
        );
      }
      break;

    case MessageItemType.FILE:
      if (item.file_item) {
        encryptParam = item.file_item.media?.encrypt_query_param;
        aesKey = parseAesKey(
          item.file_item as { aeskey?: string; media?: CDNMedia },
        );
        filename = item.file_item.file_name || `file_${Date.now()}`;
        // Try to guess MIME from filename
        const ext = filename.split(".").pop()?.toLowerCase();
        if (ext) {
          const mimeMap: Record<string, string> = {
            pdf: "application/pdf",
            doc: "application/msword",
            docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            xls: "application/vnd.ms-excel",
            xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            txt: "text/plain",
            zip: "application/zip",
            png: "image/png",
            jpg: "image/jpeg",
            jpeg: "image/jpeg",
          };
          mimeType = mimeMap[ext] || mimeType;
        }
      }
      break;

    case MessageItemType.VIDEO:
      if (item.video_item) {
        encryptParam = item.video_item.media?.encrypt_query_param;
        aesKey = parseAesKey(
          item.video_item as { aeskey?: string; media?: CDNMedia },
        );
        mimeType = "video/mp4";
        filename = `video_${Date.now()}.mp4`;
      }
      break;
  }

  if (!encryptParam || !aesKey) {
    console.warn(
      `[Weixin] downloadMediaFromItem skipped type=${item.type} reason=${!encryptParam ? "no encryptParam" : "no aesKey"}`,
    );
    return null;
  }

  const cdnUrl = `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptParam)}`;
  const data = await downloadAndDecryptMedia(cdnUrl, aesKey, filename);
  console.log(
    `[Weixin] Downloaded+decrypted type=${item.type} bytes=${data.length} filename=${filename}`,
  );

  // Voice messages arrive as Tencent SILK v3 — transcode to WAV so that
  // multimodal LLMs can actually consume the audio. On any failure we fall
  // back to the raw silk bytes so the message is still observable upstream.
  if (item.type === MessageItemType.VOICE) {
    const wav = await silkToWav(data);
    if (wav) {
      console.log(`[Weixin] SILK→WAV ok: silk=${data.length}B wav=${wav.length}B`);
      return {
        data: wav,
        mimeType: "audio/wav",
        filename: filename.replace(/\.silk$/i, ".wav"),
      };
    }
    console.warn(
      `[Weixin] SILK→WAV failed; passing raw silk through (model probably can't read it)`,
    );
  }

  // Image messages: detect actual MIME type from magic bytes rather than
  // trusting a hardcoded value. WeChat may serve PNG/WebP/GIF images but
  // the MIME was hardcoded to image/jpeg — this causes API 400 errors when
  // the declared media_type doesn't match the actual pixel format.
  if (item.type === MessageItemType.IMAGE) {
    const detected = detectImageMime(data);
    if (detected !== mimeType) {
      console.log(
        `[Weixin] IMAGE MIME corrected: hardcoded=${mimeType} → detected=${detected}`,
      );
    }
    mimeType = detected;
    filename = `image_${Date.now()}${mimeToExt(mimeType)}`;
  }

  return { data, mimeType, filename };
}

/**
 * Encrypt and upload media to CDN.
 * Returns the download reference parameters for inclusion in sendMessage.
 */
export async function uploadMediaToCdn(
  creds: WeixinCredentials,
  data: Buffer,
  filename: string,
  mediaType: number,
): Promise<{
  encryptQueryParam: string;
  aesKeyBase64: string;
  cipherSize: number;
}> {
  const plainMd5 = crypto.createHash("md5").update(data).digest("hex");
  const aesKey = generateMediaKey();
  const fileKey = crypto.randomBytes(16).toString("hex");
  const cipherSize = aesEcbPaddedSize(data.length);

  // Get pre-signed upload URL
  const urlResp = await getUploadUrl(
    creds,
    fileKey,
    mediaType,
    data.length,
    plainMd5,
    cipherSize,
  );
  if (!urlResp.upload_param) {
    throw new Error("Failed to get upload URL from WeChat");
  }

  // Encrypt
  const encrypted = encryptMedia(data, aesKey);

  // Upload to CDN
  const cdnUrl = `${creds.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(urlResp.upload_param)}&filekey=${encodeURIComponent(fileKey)}`;
  const uploadRes = await fetch(cdnUrl, {
    method: "PUT",
    body: new Uint8Array(encrypted),
    signal: AbortSignal.timeout(60_000),
  });

  if (!uploadRes.ok) {
    throw new Error(`CDN upload failed: ${uploadRes.status}`);
  }

  return {
    encryptQueryParam: urlResp.upload_param,
    aesKeyBase64: aesKey.toString("base64"),
    cipherSize: encrypted.length,
  };
}
