#!/usr/bin/env node

import { createHash, randomUUID } from 'node:crypto'
import { deflateSync } from 'node:zlib'

function assert(condition, message) {
  if (!condition) throw new Error(message)
}

function crc32(buffer) {
  let value = 0xffffffff
  for (const byte of buffer) {
    value ^= byte
    for (let bit = 0; bit < 8; bit += 1) {
      value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0)
    }
  }
  return (value ^ 0xffffffff) >>> 0
}

function pngChunk(type, data) {
  const typeBuffer = Buffer.from(type)
  const length = Buffer.alloc(4)
  length.writeUInt32BE(data.length)
  const checksum = Buffer.alloc(4)
  checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])))
  return Buffer.concat([length, typeBuffer, data, checksum])
}

function createFixturePNG(width = 256, height = 256) {
  const header = Buffer.alloc(13)
  header.writeUInt32BE(width, 0)
  header.writeUInt32BE(height, 4)
  header[8] = 8
  header[9] = 2
  const rows = []
  for (let y = 0; y < height; y += 1) {
    const row = Buffer.alloc(1 + width * 3)
    for (let x = 0; x < width; x += 1) {
      row[1 + x * 3] = Math.round((x / width) * 255)
      row[2 + x * 3] = Math.round((y / height) * 255)
      row[3 + x * 3] = 180
    }
    rows.push(row)
  }
  return Buffer.concat([
    Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
    pngChunk('IHDR', header),
    pngChunk('IDAT', deflateSync(Buffer.concat(rows))),
    pngChunk('IEND', Buffer.alloc(0)),
  ])
}

const baseURL = process.env.AI_GATEWAY_E2E_BASE_URL || 'http://10.77.0.1:18089'
const account = Buffer.from(process.env.E2E_ACCOUNT_B64 || '', 'base64').toString('utf8')
const password = Buffer.from(process.env.E2E_PASSWORD_B64 || '', 'base64').toString('utf8')
const model = process.env.AI_GATEWAY_E2E_IMAGE_MODEL || 'openai:gpt-image-1'
const imageSize = process.env.AI_GATEWAY_E2E_IMAGE_SIZE || '2048x2048'
const platformModelId = process.env.AI_GATEWAY_E2E_PLATFORM_MODEL_ID || ''
assert(account && password, 'E2E login credentials are unavailable')
assert(/^\d+x\d+$/.test(imageSize), 'E2E image size is invalid')
const [imageWidth, imageHeight] = imageSize.split('x').map(Number)
const imageResolution =
  process.env.AI_GATEWAY_E2E_IMAGE_RESOLUTION ||
  (imageWidth === imageHeight && imageWidth % 1024 === 0 ? `${imageWidth / 1024}K` : '')
const imageAspectRatio =
  process.env.AI_GATEWAY_E2E_IMAGE_ASPECT_RATIO ||
  `${imageWidth / greatestCommonDivisor(imageWidth, imageHeight)}:${imageHeight / greatestCommonDivisor(imageWidth, imageHeight)}`
assert(imageResolution, 'E2E image resolution is unavailable')

function greatestCommonDivisor(left, right) {
  while (right !== 0) [left, right] = [right, left % right]
  return left
}

async function request(path, { method = 'GET', token, body, form } = {}) {
  const headers = { Host: 'ai.51easyai.com' }
  if (token) headers.Authorization = `Bearer ${token}`
  if (body !== undefined) headers['Content-Type'] = 'application/json'
  if (form) headers['X-Async'] = 'true'
  const response = await fetch(`${baseURL}${path}`, {
    method,
    headers,
    body: form || (body === undefined ? undefined : JSON.stringify(body)),
    signal: AbortSignal.timeout(3_600_000),
  })
  const text = await response.text()
  let payload
  try {
    payload = text ? JSON.parse(text) : {}
  } catch {
    payload = { raw: text.slice(0, 500) }
  }
  if (!response.ok) {
    throw new Error(`${method} ${path} returned HTTP ${response.status}: ${JSON.stringify(payload)}`)
  }
  return { response, payload }
}

function collectHTTPSURLs(value, output = []) {
  if (typeof value === 'string' && value.startsWith('https://')) output.push(value)
  else if (Array.isArray(value)) value.forEach((item) => collectHTTPSURLs(item, output))
  else if (value && typeof value === 'object') Object.values(value).forEach((item) => collectHTTPSURLs(item, output))
  return output
}

const login = await request('/api/v1/auth/login', {
  method: 'POST',
  body: { account, password },
})
const jwt = login.payload.accessToken
assert(typeof jwt === 'string' && jwt.length > 20, 'login did not return an access token')

const existingKeys = await request('/api/v1/api-keys', { token: jwt })
for (const item of existingKeys.payload.items || []) {
  if (typeof item?.id === 'string' && item?.name?.startsWith('cluster-cross-node-')) {
    await request(`/api/v1/api-keys/${item.id}`, { method: 'DELETE', token: jwt })
  }
}

const keyName = `cluster-cross-node-${Date.now()}`
const keyResult = await request('/api/v1/api-keys', {
  method: 'POST',
  token: jwt,
  body: { name: keyName },
})
const apiKey = keyResult.payload.secret
const apiKeyId = keyResult.payload.apiKey?.id
assert(typeof apiKey === 'string' && apiKey.startsWith('sk-gw-'), 'API key creation failed')
assert(typeof apiKeyId === 'string' && apiKeyId.length > 0, 'API key ID is unavailable')

let result
let taskId
let taskSucceeded = false
try {
  const assignableModels = await request('/api/v1/api-keys/assignable-models', { token: jwt })
  const targetModels = (assignableModels.payload.items || []).filter(
    (item) =>
      [item?.modelName, item?.modelAlias].includes(model) &&
      Array.isArray(item?.modelType) &&
      item.modelType.includes('image_edit') &&
      (!platformModelId || item.id === platformModelId),
  )
  assert(targetModels.length > 0, `no assignable image_edit platform model found for ${model}`)
  const resourceMap = new Map()
  for (const item of targetModels) {
    for (const [resourceType, resourceId] of [
      ['platform', item.platformId],
      ['platform_model', item.id],
      ['base_model', item.baseModelId],
    ]) {
      if (typeof resourceId === 'string' && resourceId) {
        resourceMap.set(`${resourceType}:${resourceId}`, {
          resourceType,
          resourceId,
          priority: 100,
          minPermissionLevel: 0,
          status: 'active',
        })
      }
    }
  }
  await request('/api/v1/api-keys/access-rules/batch', {
    method: 'POST',
    token: jwt,
    body: {
      subjectType: 'api_key',
      subjectId: apiKeyId,
      effect: 'allow',
      upsertResources: [...resourceMap.values()],
      deleteResources: [],
    },
  })

  const fixture = createFixturePNG()
  const sourceSHA256 = createHash('sha256').update(fixture).digest('hex')
  const form = new FormData()
  form.append('model', model)
  form.append('prompt', `Cross-node HA acceptance ${randomUUID()}: add a thin white border`)
  form.append('size', imageSize)
  form.append('width', String(imageWidth))
  form.append('height', String(imageHeight))
  form.append('resolution', imageResolution)
  form.append('aspect_ratio', imageAspectRatio)
  form.append('images', new Blob([fixture], { type: 'image/png' }), 'cluster-e2e.png')
  const accepted = await request('/api/v1/images/edits', {
    method: 'POST',
    token: apiKey,
    form,
  })
  assert(accepted.response.status === 202, `image edit returned ${accepted.response.status}, expected 202`)
  taskId = accepted.payload.taskId || accepted.payload.task_id || accepted.payload.task?.id
  assert(/^[0-9a-f-]{36}$/.test(taskId || ''), 'image edit response has no task ID')

  let task
  const deadline = Date.now() + 20 * 60_000
  while (Date.now() < deadline) {
    const taskResult = await request(`/api/v1/tasks/${taskId}`, { token: apiKey })
    task = taskResult.payload.task || taskResult.payload
    if (task.status === 'succeeded') break
    if (['failed', 'cancelled'].includes(task.status)) {
      throw new Error(`task ${taskId} ended as ${task.status}: ${task.errorMessage || task.error || ''}`)
    }
    await new Promise((resolve) => setTimeout(resolve, 3000))
  }
  assert(task?.status === 'succeeded', `task ${taskId} did not succeed before timeout`)
  const resultURLs = [...new Set(collectHTTPSURLs(task.result))]
  assert(resultURLs.length > 0, 'task result contains no shared HTTPS URL')
  taskSucceeded = true
  result = { taskId, sourceSHA256, resultURL: resultURLs[0], model }
} finally {
  if (taskId && !taskSucceeded) {
    await request(`/api/v1/tasks/${taskId}/cancel`, { method: 'POST', token: apiKey }).catch(() => {})
  }
  if (apiKeyId) {
    await request(`/api/v1/api-keys/${apiKeyId}`, { method: 'DELETE', token: jwt }).catch(() => {})
  }
}

console.log(JSON.stringify(result))
