import {
  createCipheriv,
  createDecipheriv,
  createHash,
  randomBytes
} from 'node:crypto'
import type { ChannelMediaAttachment } from '../../shared/channel-contracts'
import { CHANNEL_LIMITS } from '../../shared/channel-contracts'
import {
  detectSupportedImage,
  mimeTypeFromFileName
} from '../file-media-type'
import { isAllowedWechatUrl } from './wechat-sidecar-security'

const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c'
const MEDIA_TIMEOUT_MS = 30_000
const MAX_REDIRECTS = 3
const MAX_ENCRYPTED_BYTES =
  CHANNEL_LIMITS.maximumAttachmentBytes + 16

type CdnMedia = {
  encrypt_query_param?: string
  aes_key?: string
  full_url?: string
}

export type WechatImageItem = {
  media?: CdnMedia
  aeskey?: string
  mid_size?: number
  hd_size?: number
}

export type WechatFileItem = {
  media?: CdnMedia
  file_name?: string
  len?: string
}

export type WechatUploadUrlResponse = {
  upload_param?: string
  upload_full_url?: string
}

export type WechatOutboundMediaItem =
  | {
      type: 2
      image_item: {
        media: {
          encrypt_query_param: string
          aes_key: string
          encrypt_type: 1
        }
        mid_size: number
      }
    }
  | {
      type: 4
      file_item: {
        media: {
          encrypt_query_param: string
          aes_key: string
          encrypt_type: 1
        }
        file_name: string
        len: string
      }
    }

function assertAllowedUrl(raw: string): URL {
  if (!isAllowedWechatUrl(raw)) {
    throw new Error('微信媒体地址不受信任')
  }
  return new URL(raw)
}

function safeFileName(value: string | undefined, fallback: string): string {
  const candidate = [...(value ?? '')]
    .map((character) => {
      const code = character.codePointAt(0)
      return code !== undefined && (code <= 31 || code === 127)
        ? '_'
        : character
    })
    .join('')
    .replace(/[\\/:*?"<>|]/gu, '_')
    .trim()
    .slice(0, CHANNEL_LIMITS.maximumAttachmentNameLength)
  return candidate && candidate !== '.' && candidate !== '..'
    ? candidate
    : fallback
}

function parseAesKey(value: string, encoding: 'hex' | 'base64'): Buffer {
  if (value.length > 128) {
    throw new Error('微信媒体密钥无效')
  }
  const decoded = Buffer.from(value, encoding)
  if (decoded.byteLength === 16) {
    return decoded
  }
  if (
    encoding === 'base64' &&
    decoded.byteLength === 32 &&
    /^[0-9a-f]{32}$/iu.test(decoded.toString('ascii'))
  ) {
    return Buffer.from(decoded.toString('ascii'), 'hex')
  }
  throw new Error('微信媒体密钥无效')
}

function resolveDownloadUrl(media: CdnMedia): URL {
  if (media.full_url?.trim()) {
    return assertAllowedUrl(media.full_url.trim())
  }
  const parameter = media.encrypt_query_param?.trim()
  if (!parameter || parameter.length > 8_192) {
    throw new Error('微信媒体下载参数无效')
  }
  const url = new URL('/c2c/download', `${CDN_BASE_URL}/`)
  url.searchParams.set('encrypted_query_param', parameter)
  return assertAllowedUrl(url.toString())
}

function withTimeout(
  inputSignal: AbortSignal,
  timeoutMs = MEDIA_TIMEOUT_MS
): {
  signal: AbortSignal
  dispose: () => void
} {
  const controller = new AbortController()
  const abort = (): void => controller.abort(inputSignal.reason)
  inputSignal.addEventListener('abort', abort, { once: true })
  if (inputSignal.aborted) {
    abort()
  }
  const timeout = setTimeout(
    () => controller.abort(new Error('微信媒体传输超时')),
    timeoutMs
  )
  return {
    signal: controller.signal,
    dispose: () => {
      clearTimeout(timeout)
      inputSignal.removeEventListener('abort', abort)
    }
  }
}

async function readBoundedBody(
  response: Response,
  maximumBytes: number
): Promise<Buffer> {
  const declaredLength = Number(response.headers.get('content-length'))
  if (
    Number.isFinite(declaredLength) &&
    declaredLength > maximumBytes
  ) {
    throw new Error('微信媒体超过 12MB 限制')
  }
  if (!response.body) {
    return Buffer.alloc(0)
  }
  const reader = response.body.getReader()
  const chunks: Buffer[] = []
  let total = 0
  try {
    while (true) {
      const chunk = await reader.read()
      if (chunk.done) {
        break
      }
      total += chunk.value.byteLength
      if (total > maximumBytes) {
        await reader.cancel()
        throw new Error('微信媒体超过 12MB 限制')
      }
      chunks.push(Buffer.from(chunk.value))
    }
  } finally {
    reader.releaseLock()
  }
  return Buffer.concat(chunks, total)
}

async function fetchWechatBytes(
  initialUrl: URL,
  signal: AbortSignal
): Promise<Buffer> {
  let url = initialUrl
  for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
    const response = await fetch(url, {
      method: 'GET',
      redirect: 'manual',
      signal
    })
    if (response.status >= 300 && response.status < 400) {
      const location = response.headers.get('location')
      if (!location || redirectCount === MAX_REDIRECTS) {
        throw new Error('微信媒体重定向无效')
      }
      url = assertAllowedUrl(new URL(location, url).toString())
      continue
    }
    if (!response.ok) {
      throw new Error(`微信媒体下载失败（${response.status}）`)
    }
    return readBoundedBody(response, MAX_ENCRYPTED_BYTES)
  }
  throw new Error('微信媒体重定向过多')
}

async function downloadMedia(
  media: CdnMedia,
  key: Buffer | undefined,
  signal: AbortSignal
): Promise<Buffer> {
  const timed = withTimeout(signal)
  try {
    const encrypted = await fetchWechatBytes(
      resolveDownloadUrl(media),
      timed.signal
    )
    timed.signal.throwIfAborted()
    if (!key) {
      if (encrypted.byteLength > CHANNEL_LIMITS.maximumAttachmentBytes) {
        throw new Error('微信媒体超过 12MB 限制')
      }
      return encrypted
    }
    if (encrypted.byteLength === 0 || encrypted.byteLength % 16 !== 0) {
      throw new Error('微信媒体密文无效')
    }
    const decipher = createDecipheriv('aes-128-ecb', key, null)
    const decrypted = Buffer.concat([
      decipher.update(encrypted),
      decipher.final()
    ])
    if (
      decrypted.byteLength === 0 ||
      decrypted.byteLength > CHANNEL_LIMITS.maximumAttachmentBytes
    ) {
      throw new Error('微信媒体超过 12MB 限制')
    }
    return decrypted
  } finally {
    timed.dispose()
  }
}

export async function downloadWechatImage(
  item: WechatImageItem,
  fallbackName: string,
  signal: AbortSignal
): Promise<ChannelMediaAttachment> {
  if (!item.media) {
    throw new Error('微信图片缺少媒体引用')
  }
  const claimedCipherSize = item.hd_size ?? item.mid_size
  if (
    claimedCipherSize !== undefined &&
    (!Number.isSafeInteger(claimedCipherSize) ||
      claimedCipherSize < 1 ||
      claimedCipherSize > MAX_ENCRYPTED_BYTES)
  ) {
    throw new Error('微信图片超过 12MB 限制')
  }
  const key = item.aeskey
    ? parseAesKey(item.aeskey, 'hex')
    : item.media.aes_key
      ? parseAesKey(item.media.aes_key, 'base64')
      : undefined
  const data = await downloadMedia(item.media, key, signal)
  const format = detectSupportedImage(data)
  return {
    name: safeFileName(
      `${fallbackName}.${format.extension}`,
      `微信图片.${format.extension}`
    ),
    mimeType: format.mimeType,
    size: data.byteLength,
    kind: 'image',
    dataBase64: data.toString('base64')
  }
}

export async function downloadWechatFile(
  item: WechatFileItem,
  signal: AbortSignal
): Promise<ChannelMediaAttachment> {
  if (!item.media?.aes_key) {
    throw new Error('微信文件缺少加密信息')
  }
  const claimedSize = Number(item.len)
  if (
    item.len !== undefined &&
    (!Number.isSafeInteger(claimedSize) ||
      claimedSize < 1 ||
      claimedSize > CHANNEL_LIMITS.maximumAttachmentBytes)
  ) {
    throw new Error('微信文件超过 12MB 限制')
  }
  const data = await downloadMedia(
    item.media,
    parseAesKey(item.media.aes_key, 'base64'),
    signal
  )
  if (item.len !== undefined && data.byteLength !== claimedSize) {
    throw new Error('微信文件大小校验失败')
  }
  const name = safeFileName(item.file_name, '微信文件.bin')
  return {
    name,
    mimeType: mimeTypeFromFileName(name),
    size: data.byteLength,
    kind: 'file',
    dataBase64: data.toString('base64')
  }
}

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

async function uploadWechatBytes(
  url: URL,
  body: Buffer,
  signal: AbortSignal
): Promise<string> {
  const timed = withTimeout(signal)
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/octet-stream' },
      body,
      redirect: 'manual',
      signal: timed.signal
    })
    if (response.status >= 300 && response.status < 400) {
      throw new Error('微信媒体上传重定向无效')
    }
    if (!response.ok) {
      throw new Error(`微信媒体上传失败（${response.status}）`)
    }
    const parameter = response.headers.get('x-encrypted-param')?.trim()
    if (!parameter || parameter.length > 8_192) {
      throw new Error('微信媒体上传结果无效')
    }
    return parameter
  } finally {
    timed.dispose()
  }
}

export async function uploadWechatAttachment(input: {
  attachment: ChannelMediaAttachment
  recipientId: string
  signal: AbortSignal
  getUploadUrl: (request: {
    filekey: string
    media_type: 1 | 3
    to_user_id: string
    rawsize: number
    rawfilemd5: string
    filesize: number
    no_need_thumb: true
    aeskey: string
  }) => Promise<WechatUploadUrlResponse>
}): Promise<WechatOutboundMediaItem> {
  const attachment = input.attachment
  const plaintext = Buffer.from(attachment.dataBase64, 'base64')
  if (
    plaintext.byteLength !== attachment.size ||
    plaintext.byteLength === 0 ||
    plaintext.byteLength > CHANNEL_LIMITS.maximumAttachmentBytes
  ) {
    throw new Error('待发送附件大小无效')
  }
  if (attachment.kind === 'image') {
    detectSupportedImage(plaintext)
  }
  const filekey = randomBytes(16).toString('hex')
  const key = randomBytes(16)
  const response = await input.getUploadUrl({
    filekey,
    media_type: attachment.kind === 'image' ? 1 : 3,
    to_user_id: input.recipientId,
    rawsize: plaintext.byteLength,
    rawfilemd5: createHash('md5').update(plaintext).digest('hex'),
    filesize: encryptedSize(plaintext.byteLength),
    no_need_thumb: true,
    aeskey: key.toString('hex')
  })
  const fullUrl = response.upload_full_url?.trim()
  const uploadParameter = response.upload_param?.trim()
  const uploadUrl = fullUrl
    ? assertAllowedUrl(fullUrl)
    : (() => {
        if (!uploadParameter || uploadParameter.length > 8_192) {
          throw new Error('微信媒体上传地址缺失')
        }
        const url = new URL('/c2c/upload', `${CDN_BASE_URL}/`)
        url.searchParams.set('encrypted_query_param', uploadParameter)
        url.searchParams.set('filekey', filekey)
        return assertAllowedUrl(url.toString())
      })()
  const cipher = createCipheriv('aes-128-ecb', key, null)
  const encrypted = Buffer.concat([
    cipher.update(plaintext),
    cipher.final()
  ])
  const downloadParameter = await uploadWechatBytes(
    uploadUrl,
    encrypted,
    input.signal
  )
  const aesKey = Buffer.from(key.toString('hex'), 'ascii').toString(
    'base64'
  )
  if (attachment.kind === 'image') {
    return {
      type: 2,
      image_item: {
        media: {
          encrypt_query_param: downloadParameter,
          aes_key: aesKey,
          encrypt_type: 1
        },
        mid_size: encrypted.byteLength
      }
    }
  }
  return {
    type: 4,
    file_item: {
      media: {
        encrypt_query_param: downloadParameter,
        aes_key: aesKey,
        encrypt_type: 1
      },
      file_name: safeFileName(attachment.name, 'GoodBuddy 文件.bin'),
      len: String(plaintext.byteLength)
    }
  }
}
