#!/usr/bin/env node

import { execFileSync } from 'node:child_process';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';

const baseURL = (process.env.GATEWAY_BASE_URL || 'http://127.0.0.1:8088').replace(/\/+$/, '');
const model = process.env.DEYUN_VIDEO_MODEL || 'deyun-seedance-2.0-canary';
const expectedPlatform = process.env.DEYUN_VIDEO_PLATFORM || '漫路（火山兼容）';
const expectedProviderModel = process.env.DEYUN_VIDEO_PROVIDER_MODEL || 'doubao-seedance-2-0';
const pollIntervalMs = positiveInteger(process.env.DEYUN_VIDEO_POLL_INTERVAL_MS, 5000);
const taskTimeoutMs = positiveInteger(process.env.DEYUN_VIDEO_TASK_TIMEOUT_MS, 20 * 60 * 1000);
const outputPath =
  process.env.DEYUN_VIDEO_E2E_OUTPUT ||
  `artifacts/deyun-video-e2e-${new Date().toISOString().replaceAll(/[:.]/g, '-')}.json`;
const database = {
  container: process.env.GATEWAY_E2E_DB_CONTAINER || 'postgres',
  name: process.env.GATEWAY_E2E_DB_NAME || 'easyai_ai_gateway',
  user: process.env.GATEWAY_E2E_DB_USER || 'easyai',
};

const validationCases = [
  {
    name: '480p-4s-audio-off',
    request: {
      model,
      prompt: 'A glossy red cube rotates slowly on a clean white studio table, locked camera, soft daylight.',
      resolution: '480p',
      ratio: '16:9',
      duration: 4,
      generate_audio: false,
      seed: 41001,
      watermark: false,
      runMode: 'real',
    },
    expected: {
      height: 480,
      duration: 4,
      audio: false,
      ratio: 16 / 9,
    },
  },
  {
    name: '720p-6s-audio-on',
    request: {
      model,
      prompt: 'A calm ocean wave rolls toward a sandy beach at sunrise, locked camera, natural ambient sound.',
      resolution: '720p',
      ratio: '16:9',
      duration: 6,
      generate_audio: true,
      seed: 42002,
      watermark: false,
      runMode: 'real',
    },
    expected: {
      height: 720,
      duration: 6,
      audio: true,
      ratio: 16 / 9,
    },
  },
];
const requestedCases = new Set(
  String(process.env.DEYUN_VIDEO_CASES || '')
    .split(',')
    .map((value) => value.trim())
    .filter(Boolean),
);
if (requestedCases.size > 0) {
  const selectedCases = validationCases.filter((validationCase) => requestedCases.has(validationCase.name));
  if (selectedCases.length !== requestedCases.size) {
    const knownCases = validationCases.map((validationCase) => validationCase.name).join(', ');
    throw new Error(`Unknown DEYUN_VIDEO_CASES value; known cases: ${knownCases}`);
  }
  validationCases.splice(0, validationCases.length, ...selectedCases);
}
const submittedTaskIds = [];

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

function positiveInteger(value, fallback) {
  const parsed = Number.parseInt(String(value || ''), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function nearlyEqual(left, right, tolerance = 1e-6) {
  return Math.abs(Number(left) - Number(right)) <= tolerance;
}

function runPSQL(query) {
  return execFileSync(
    'docker',
    ['exec', database.container, 'psql', '-U', database.user, '-d', database.name, '-At', '-F', '\t', '-c', query],
    { encoding: 'utf8' },
  ).trim();
}

function resolveGatewayAPIKey() {
  if (process.env.GATEWAY_E2E_API_KEY) {
    assert(process.env.GATEWAY_E2E_API_KEY_ID, 'Set GATEWAY_E2E_API_KEY_ID when GATEWAY_E2E_API_KEY is provided');
    const metadata = apiKeyMetadata(process.env.GATEWAY_E2E_API_KEY_ID);
    return {
      ...metadata,
      secret: process.env.GATEWAY_E2E_API_KEY,
      source: 'environment',
    };
  }
  const query = `
SELECT key.id::text,
       key.key_secret,
       key.gateway_user_id::text,
       wallet.balance::float8,
       wallet.frozen_balance::float8
FROM gateway_api_keys key
JOIN gateway_wallet_accounts wallet
  ON wallet.gateway_user_id = key.gateway_user_id
 AND wallet.currency = 'resource'
 AND wallet.status = 'active'
WHERE key.deleted_at IS NULL
  AND key.status = 'active'
  AND COALESCE(key.key_secret, '') <> ''
  AND (key.expires_at IS NULL OR key.expires_at > now())
  AND (key.scopes ? 'video' OR key.scopes ? '*' OR key.scopes ? 'all')
  AND (wallet.balance - wallet.frozen_balance) > 0
ORDER BY (wallet.balance - wallet.frozen_balance) DESC, key.created_at DESC
LIMIT 1`;
  const fields = runPSQL(query).split('\t');
  assert(fields.length === 5 && fields[0] && fields[1], 'No recoverable video-scoped Gateway API Key with positive balance was found');
  return {
    keyId: fields[0],
    secret: fields[1],
    userId: fields[2],
    balance: Number(fields[3]),
    frozenBalance: Number(fields[4]),
    source: 'database',
  };
}

function apiKeyMetadata(keyId) {
  const escapedKeyId = String(keyId).replaceAll("'", "''");
  const output = runPSQL(`
SELECT key.id::text,
       key.gateway_user_id::text,
       wallet.balance::float8,
       wallet.frozen_balance::float8
FROM gateway_api_keys key
JOIN gateway_wallet_accounts wallet
  ON wallet.gateway_user_id = key.gateway_user_id
 AND wallet.currency = 'resource'
WHERE key.id = '${escapedKeyId}'::uuid
LIMIT 1`);
  const fields = output.split('\t');
  assert(fields.length === 4 && fields[0], `Gateway API Key ${keyId} was not found`);
  return {
    keyId: fields[0],
    userId: fields[1],
    balance: Number(fields[2]),
    frozenBalance: Number(fields[3]),
  };
}

function walletState(userId) {
  const escapedUserId = String(userId).replaceAll("'", "''");
  const fields = runPSQL(`
SELECT balance::float8, frozen_balance::float8
FROM gateway_wallet_accounts
WHERE gateway_user_id = '${escapedUserId}'::uuid
  AND currency = 'resource'
LIMIT 1`).split('\t');
  assert(fields.length === 2, `Wallet for Gateway user ${userId} was not found`);
  return {
    balance: Number(fields[0]),
    frozenBalance: Number(fields[1]),
  };
}

async function request(path, init = {}) {
  const response = await fetch(`${baseURL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${authentication.secret}`,
      ...(init.body ? { 'Content-Type': 'application/json' } : {}),
      ...(init.headers || {}),
    },
  });
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`${init.method || 'GET'} ${path} failed ${response.status}: ${text}`);
  }
  return text ? JSON.parse(text) : {};
}

async function createVideoTask(body) {
  const accepted = await request('/api/v1/videos/generations', {
    method: 'POST',
    headers: { 'X-Async': 'true' },
    body: JSON.stringify(body),
  });
  const taskId = accepted.taskId || accepted.task?.id;
  assert(taskId, `Video task acceptance is missing taskId: ${JSON.stringify(accepted)}`);
  submittedTaskIds.push(taskId);
  return taskId;
}

async function pollTask(taskId) {
  const startedAt = Date.now();
  while (Date.now() - startedAt < taskTimeoutMs) {
    const task = await request(`/api/v1/tasks/${taskId}`);
    if (task.status === 'succeeded') return task;
    if (task.status === 'failed' || task.status === 'cancelled') {
      throw new Error(`Task ${taskId} ended as ${task.status}: ${task.errorMessage || task.error || JSON.stringify(task.result)}`);
    }
    await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
  }
  throw new Error(`Timed out after ${taskTimeoutMs}ms waiting for task ${taskId}`);
}

async function taskEvents(taskId) {
  const response = await fetch(`${baseURL}/api/v1/tasks/${taskId}/events`, {
    headers: { Authorization: `Bearer ${authentication.secret}` },
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`GET task events failed ${response.status}: ${text}`);
  return text
    .split(/\r?\n\r?\n/)
    .flatMap((block) => {
      const data = block
        .split(/\r?\n/)
        .filter((line) => line.startsWith('data:'))
        .map((line) => line.slice(5).trim())
        .join('\n');
      return data ? [JSON.parse(data)] : [];
    });
}

async function taskPreprocessingLogs(taskId) {
  return request(`/api/v1/tasks/${taskId}/param-preprocessing`);
}

function videoURLFromTask(task) {
  const data = Array.isArray(task.result?.data) ? task.result.data : [];
  return data.find((item) => item?.type === 'video')?.url || data.find((item) => item?.url)?.url || '';
}

async function downloadVideo(url, filePath) {
  const response = await fetch(url);
  assert(response.ok, `Download ${url} failed ${response.status}`);
  const bytes = Buffer.from(await response.arrayBuffer());
  assert(bytes.length > 0, `Downloaded video ${url} is empty`);
  await writeFile(filePath, bytes);
  return bytes.length;
}

function probeVideo(filePath) {
  const output = execFileSync(
    'ffprobe',
    ['-v', 'error', '-show_streams', '-show_format', '-of', 'json', filePath],
    { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
  );
  return JSON.parse(output);
}

function validateRequestSnapshot(task, expectedRequest) {
  const snapshot = task.request || {};
  for (const key of ['model', 'resolution', 'ratio', 'duration', 'generate_audio', 'seed']) {
    assert(
      snapshot[key] === expectedRequest[key],
      `Task ${task.id} request.${key}=${JSON.stringify(snapshot[key])}, expected ${JSON.stringify(expectedRequest[key])}`,
    );
  }
}

function validateTaskAudit(task, events) {
  const attempts = Array.isArray(task.attempts) ? task.attempts : [];
  assert(attempts.length === 1, `Task ${task.id} has ${attempts.length} attempts, expected exactly one`);
  const attempt = attempts[0];
  assert(attempt.platformName === expectedPlatform, `Task ${task.id} used platform ${attempt.platformName}, expected ${expectedPlatform}`);
  assert(attempt.provider === 'volces', `Task ${task.id} used provider ${attempt.provider}, expected volces`);
  assert(
    attempt.providerModelName === expectedProviderModel,
    `Task ${task.id} used provider model ${attempt.providerModelName}, expected ${expectedProviderModel}`,
  );
  assert(task.remoteTaskId, `Task ${task.id} is missing remoteTaskId`);
  assert(attempt.requestId, `Task ${task.id} attempt is missing requestId`);
  const startedEvents = events.filter((event) => event.eventType === 'task.attempt.started');
  assert(startedEvents.length === 1, `Task ${task.id} emitted ${startedEvents.length} task.attempt.started events`);
  assert(Number(task.finalChargeAmount) > 0, `Task ${task.id} finalChargeAmount must be positive`);
  return attempt;
}

function validateMedia(probe, expected, taskId) {
  const streams = Array.isArray(probe.streams) ? probe.streams : [];
  const video = streams.find((stream) => stream.codec_type === 'video');
  const audioStreams = streams.filter((stream) => stream.codec_type === 'audio');
  assert(video, `Task ${taskId} output has no video stream`);
  const width = Number(video.width);
  const height = Number(video.height);
  const duration = Number(video.duration || probe.format?.duration);
  assert(height === expected.height, `Task ${taskId} output height=${height}, expected ${expected.height}`);
  const ratioError = Math.abs(width / height - expected.ratio) / expected.ratio;
  assert(ratioError <= 0.02, `Task ${taskId} aspect ratio ${width}:${height} differs from 16:9 by ${(ratioError * 100).toFixed(2)}%`);
  assert(Number.isFinite(duration), `Task ${taskId} output duration is unavailable`);
  assert(Math.abs(duration - expected.duration) <= 1, `Task ${taskId} duration=${duration}s, expected ${expected.duration}s ±1s`);
  if (expected.audio) {
    assert(audioStreams.length > 0, `Task ${taskId} requested audio but output has no audio stream`);
    const audioDuration = Number(audioStreams[0].duration || probe.format?.duration);
    if (Number.isFinite(audioDuration)) {
      assert(audioDuration >= duration - 1, `Task ${taskId} audio duration=${audioDuration}s is too short for video duration=${duration}s`);
    }
  } else {
    assert(audioStreams.length === 0, `Task ${taskId} disabled audio but output has ${audioStreams.length} audio stream(s)`);
  }
  return {
    width,
    height,
    duration,
    ratio: width / height,
    videoCodec: video.codec_name || null,
    audioStreams: audioStreams.map((stream) => ({
      codec: stream.codec_name || null,
      channels: Number(stream.channels || 0),
      sampleRate: Number(stream.sample_rate || 0),
      duration: Number(stream.duration || probe.format?.duration || 0),
    })),
  };
}

const authentication = resolveGatewayAPIKey();

function sanitizedFailureMessage(error) {
  return String(error instanceof Error ? error.message : error)
    .replace(/Request id:\s*[A-Za-z0-9_-]+/gi, 'Request id: [redacted]')
    .replace(/\b\d{8,}\b/g, '[redacted-id]');
}

async function writeFailureReport(error) {
  const report = {
    ok: false,
    generatedAt: new Date().toISOString(),
    baseURL,
    model,
    expectedPlatform,
    expectedProviderModel,
    authentication: {
      type: 'gateway_api_key',
      source: authentication.source,
      keyId: authentication.keyId,
    },
    submittedTaskIds,
    error: sanitizedFailureMessage(error),
  };
  await mkdir(dirname(outputPath), { recursive: true });
  await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
  return report;
}

async function main() {
  const tempDirectory = await mkdtemp(join(tmpdir(), 'deyun-video-e2e-'));
  const walletBefore = walletState(authentication.userId);
  const results = [];
  try {
    for (const validationCase of validationCases) {
      const taskId = await createVideoTask(validationCase.request);
      const task = await pollTask(taskId);
      const [events, preprocessing] = await Promise.all([
        taskEvents(taskId),
        taskPreprocessingLogs(taskId),
      ]);
      validateRequestSnapshot(task, validationCase.request);
      const attempt = validateTaskAudit(task, events);
      assert(
        Array.isArray(preprocessing.items) && preprocessing.items.length > 0,
        `Task ${taskId} has no parameter preprocessing audit record`,
      );
      const videoURL = videoURLFromTask(task);
      assert(videoURL, `Task ${taskId} result is missing a video URL`);
      const videoPath = join(tempDirectory, `${validationCase.name}.mp4`);
      const byteSize = await downloadVideo(videoURL, videoPath);
      const probe = probeVideo(videoPath);
      const observed = validateMedia(probe, validationCase.expected, taskId);
      results.push({
        name: validationCase.name,
        taskId,
        remoteTaskId: task.remoteTaskId,
        requestId: attempt.requestId,
        platform: attempt.platformName,
        providerModelName: attempt.providerModelName,
        requested: {
          resolution: validationCase.request.resolution,
          ratio: validationCase.request.ratio,
          duration: validationCase.request.duration,
          generateAudio: validationCase.request.generate_audio,
          seed: validationCase.request.seed,
        },
        observed,
        byteSize,
        finalChargeAmount: Number(task.finalChargeAmount),
        billingSummary: task.billingSummary || {},
        eventTypes: events.map((event) => event.eventType),
        preprocessingChangeCount: preprocessing.items.reduce((sum, item) => sum + Number(item.changeCount || 0), 0),
      });
    }
  } finally {
    await rm(tempDirectory, { recursive: true, force: true });
  }

  const walletAfter = walletState(authentication.userId);
  const totalCharge = results.reduce((sum, result) => sum + result.finalChargeAmount, 0);
  const walletDebit = walletBefore.balance - walletAfter.balance;
  assert(nearlyEqual(walletDebit, totalCharge, 1e-6), `Wallet debit=${walletDebit}, expected total charge=${totalCharge}`);
  assert(
    nearlyEqual(walletAfter.frozenBalance, walletBefore.frozenBalance, 1e-6),
    `Wallet frozen balance changed from ${walletBefore.frozenBalance} to ${walletAfter.frozenBalance}`,
  );

  const report = {
    ok: true,
    generatedAt: new Date().toISOString(),
    baseURL,
    model,
    expectedPlatform,
    expectedProviderModel,
    authentication: {
      type: 'gateway_api_key',
      source: authentication.source,
      keyId: authentication.keyId,
    },
    wallet: {
      balanceBefore: walletBefore.balance,
      balanceAfter: walletAfter.balance,
      frozenBalanceBefore: walletBefore.frozenBalance,
      frozenBalanceAfter: walletAfter.frozenBalance,
      debit: walletDebit,
      totalCharge,
    },
    results,
  };
  await mkdir(dirname(outputPath), { recursive: true });
  await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
  console.log(JSON.stringify({ ...report, outputPath }, null, 2));
}

main().catch(async (error) => {
  const report = await writeFailureReport(error);
  console.error(report.error);
  console.error(`Failure report: ${outputPath}`);
  process.exitCode = 1;
});
