/**
 * WeChat CDN upload/download with AES-128-ECB encryption.
 * Aligned with @tencent-weixin/openclaw-weixin@2.0.1 cdn/ module.
 *
 * Flow: readFile → md5 → genKey → getUploadUrl → AES encrypt → POST to CDN → get downloadParam
 */
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import type { FastifyBaseLogger } from 'fastify';

const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
const UPLOAD_MAX_RETRIES = 3;

export const UploadMediaType = { IMAGE: 1, VIDEO: 2, FILE: 3, VOICE: 4 } as const;

export interface UploadedFileInfo {
  filekey: string;
  downloadEncryptedQueryParam: string;
  aeskey: string;
  fileSize: number;
  fileSizeCiphertext: number;
}

interface CdnPlatformKey {
  encryptQueryParam?: string;
  fullUrl?: string;
  aesKey?: string;
}

interface GetUploadUrlResponse {
  upload_param?: string;
  upload_full_url?: string;
}

// ── AES-128-ECB ──

export function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer {
  const cipher = createCipheriv('aes-128-ecb', key, null);
  return Buffer.concat([cipher.update(plaintext), cipher.final()]);
}

export function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer {
  const decipher = createDecipheriv('aes-128-ecb', key, null);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

function aesEcbPaddedSize(plaintextSize: number): number {
  return Math.ceil((plaintextSize + 1) / 16) * 16;
}

// ── AES Key Decode ──

/**
 * Decode aesKey string from iLink protocol into a 16-byte Buffer.
 * Handles: hex (32 chars), standard base64, base64url (- and _), and missing padding.
 */
export function decodeAesKey(aesKey: string, log?: FastifyBaseLogger): Buffer {
  // 1. Hex: exactly 32 hex chars → 16 bytes
  if (/^[0-9a-f]{32}$/i.test(aesKey)) {
    return Buffer.from(aesKey, 'hex');
  }

  // 2. Normalize base64url → standard base64 (replace - with +, _ with /)
  let normalized = aesKey.replace(/-/g, '+').replace(/_/g, '/');
  // 3. Restore padding if missing
  const pad = normalized.length % 4;
  if (pad === 2) normalized += '==';
  else if (pad === 3) normalized += '=';

  const key = Buffer.from(normalized, 'base64');
  if (key.length === 16) return key;

  // 4. Official iLink compat: base64(hex-string) — 32 ASCII hex chars → 16 bytes
  if (key.length === 32) {
    const hexStr = key.toString('ascii');
    if (/^[0-9a-f]{32}$/i.test(hexStr)) {
      return Buffer.from(hexStr, 'hex');
    }
  }

  log?.warn(
    { aesKeyLen: aesKey.length, decodedLen: key.length, prefix: aesKey.slice(0, 8) },
    '[weixin-cdn] Invalid AES key length after decode',
  );
  throw new Error(`Invalid AES key: decoded ${key.length} bytes from ${aesKey.length}-char string (expected 16)`);
}

// ── CDN Download Pipeline ──

function resolveWeChatCdnFullUrl(fullUrl: string, fieldName: 'full_url' | 'upload_full_url'): string {
  let parsed: URL;
  try {
    parsed = new URL(fullUrl);
  } catch {
    throw new Error(`[weixin-cdn] Invalid ${fieldName}: ${fullUrl.slice(0, 80)}`);
  }

  const host = parsed.hostname.toLowerCase();
  const isWeChatCdnHost = host === 'cdn.weixin.qq.com' ? true : host.endsWith('.cdn.weixin.qq.com');
  if (parsed.protocol !== 'https:') {
    throw new Error(`[weixin-cdn] Refusing ${fieldName} from non-WeChat CDN host: ${host}`);
  }
  if (!isWeChatCdnHost) {
    throw new Error(`[weixin-cdn] Refusing ${fieldName} from non-WeChat CDN host: ${host}`);
  }
  return parsed.toString();
}

/**
 * Download encrypted media from WeChat CDN and decrypt.
 * `platformKey` is a JSON-encoded string:
 * - { encryptQueryParam, aesKey } for legacy CDN download API
 * - { fullUrl, aesKey } for iLink full_url responses
 */
export async function downloadMediaFromCdn(params: {
  platformKey: string;
  cdnBaseUrl: string;
  log: FastifyBaseLogger;
  fetchFn?: typeof fetch;
}): Promise<Buffer> {
  const { platformKey, cdnBaseUrl, log, fetchFn = globalThis.fetch } = params;

  const { encryptQueryParam, fullUrl, aesKey } = JSON.parse(platformKey) as CdnPlatformKey;

  if (!aesKey) {
    throw new Error('[weixin-cdn] CDN platformKey missing aesKey');
  }

  const cdnUrl = fullUrl
    ? resolveWeChatCdnFullUrl(fullUrl, 'full_url')
    : encryptQueryParam
      ? `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptQueryParam)}`
      : '';
  if (!cdnUrl) {
    throw new Error('[weixin-cdn] CDN platformKey missing encryptQueryParam or fullUrl');
  }

  log.info({ cdnUrl: cdnUrl.slice(0, 80) }, '[weixin-cdn] Downloading media from CDN');

  const res = await fetchFn(cdnUrl, {
    method: 'GET',
    signal: AbortSignal.timeout(30_000),
  });

  if (!res.ok) {
    const errText = await res.text().catch(() => '');
    throw new Error(`CDN download HTTP ${res.status}: ${errText}`);
  }

  const ciphertext = Buffer.from(await res.arrayBuffer());
  const key = decodeAesKey(aesKey, log);
  const plaintext = decryptAesEcb(ciphertext, key);

  log.info({ ciphertextLen: ciphertext.length, plaintextLen: plaintext.length }, '[weixin-cdn] Media decrypted');

  return plaintext;
}

// ── CDN Upload Pipeline ──

export async function uploadMediaToCdn(params: {
  filePath: string;
  toUserId: string;
  mediaType: number;
  botToken: string;
  cdnBaseUrl: string;
  log: FastifyBaseLogger;
  fetchFn?: typeof fetch;
}): Promise<UploadedFileInfo> {
  const { filePath, toUserId, mediaType, botToken, cdnBaseUrl, log, fetchFn = globalThis.fetch } = params;

  const plaintext = await readFile(filePath);
  const rawsize = plaintext.length;
  const rawfilemd5 = createHash('md5').update(plaintext).digest('hex');
  const filesize = aesEcbPaddedSize(rawsize);
  const filekey = randomBytes(16).toString('hex');
  const aeskey = randomBytes(16);

  log.info({ filePath: basename(filePath), rawsize, filesize, mediaType }, '[weixin-cdn] Uploading media');

  const uploadUrlResp = await callGetUploadUrl({
    filekey,
    mediaType,
    toUserId,
    rawsize,
    rawfilemd5,
    filesize,
    aeskey: aeskey.toString('hex'),
    botToken,
    fetchFn,
  });

  if (!uploadUrlResp.upload_param && !uploadUrlResp.upload_full_url) {
    throw new Error('[weixin-cdn] getUploadUrl returned no upload_param/upload_full_url');
  }

  const downloadParam = await uploadBufferToCdn({
    buf: plaintext,
    uploadParam: uploadUrlResp.upload_param,
    uploadFullUrl: uploadUrlResp.upload_full_url,
    filekey,
    cdnBaseUrl,
    aeskey,
    log,
    fetchFn,
  });

  return {
    filekey,
    downloadEncryptedQueryParam: downloadParam,
    aeskey: aeskey.toString('hex'),
    fileSize: rawsize,
    fileSizeCiphertext: filesize,
  };
}

// ── Internal API calls ──

function getHeaders(botToken: string): Record<string, string> {
  const uin = Buffer.from(String(Math.floor(Math.random() * 0xffffffff))).toString('base64');
  return {
    'Content-Type': 'application/json',
    AuthorizationType: 'ilink_bot_token',
    Authorization: `Bearer ${botToken}`,
    'X-WECHAT-UIN': uin,
  };
}

async function callGetUploadUrl(params: {
  filekey: string;
  mediaType: number;
  toUserId: string;
  rawsize: number;
  rawfilemd5: string;
  filesize: number;
  aeskey: string;
  botToken: string;
  fetchFn: typeof fetch;
}): Promise<GetUploadUrlResponse> {
  const body = JSON.stringify({
    filekey: params.filekey,
    media_type: params.mediaType,
    to_user_id: params.toUserId,
    rawsize: params.rawsize,
    rawfilemd5: params.rawfilemd5,
    filesize: params.filesize,
    no_need_thumb: true,
    aeskey: params.aeskey,
    base_info: { channel_version: '1.0.0' },
  });

  const res = await params.fetchFn(`${ILINK_BASE_URL}/ilink/bot/getuploadurl`, {
    method: 'POST',
    headers: getHeaders(params.botToken),
    body,
    signal: AbortSignal.timeout(15_000),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`getuploadurl HTTP ${res.status}: ${text}`);
  }
  return (await res.json()) as GetUploadUrlResponse;
}

async function uploadBufferToCdn(params: {
  buf: Buffer;
  uploadParam?: string;
  uploadFullUrl?: string;
  filekey: string;
  cdnBaseUrl: string;
  aeskey: Buffer;
  log: FastifyBaseLogger;
  fetchFn: typeof fetch;
}): Promise<string> {
  const { buf, uploadParam, uploadFullUrl, filekey, cdnBaseUrl, aeskey, log, fetchFn } = params;
  const ciphertext = encryptAesEcb(buf, aeskey);
  const cdnUrl = uploadFullUrl
    ? resolveWeChatCdnFullUrl(uploadFullUrl, 'upload_full_url')
    : uploadParam
      ? `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`
      : '';
  if (!cdnUrl) {
    throw new Error('[weixin-cdn] Missing upload_param/upload_full_url for CDN upload');
  }

  for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) {
    try {
      const res = await fetchFn(cdnUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/octet-stream' },
        body: new Uint8Array(ciphertext),
      });
      if (res.status >= 400 && res.status < 500) {
        const msg = res.headers.get('x-error-message') ?? (await res.text());
        throw new Error(`CDN client error ${res.status}: ${msg}`);
      }
      if (res.status !== 200) {
        throw new Error(`CDN server error ${res.status}`);
      }
      const downloadParam = res.headers.get('x-encrypted-param');
      if (!downloadParam) {
        throw new Error('CDN response missing x-encrypted-param header');
      }
      log.info({ attempt, filekey }, '[weixin-cdn] CDN upload success');
      return downloadParam;
    } catch (err) {
      if (err instanceof Error && err.message.includes('client error')) throw err;
      if (attempt === UPLOAD_MAX_RETRIES) throw err;
      log.warn({ attempt, err: String(err) }, '[weixin-cdn] CDN upload retry');
    }
  }
  throw new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
}
