import type pg from 'pg';
import { createHash } from 'node:crypto';
import { HttpError, type RequestContext } from '../../core/http.js';
import { intParam, readJsonBody, requiredString, stringParam } from '../../core/request.js';
import { query, transaction } from '../../core/db.js';
import { requireTenantContentEditor, type TenantContentAuth } from './auth.js';
import { boolValue, jsonObjectValue, nullableString } from './utils.js';

const INLINE_EXPORT_FORMATS = ['json', 'paper_json', 'print_payload'];
const BINARY_EXPORT_FORMATS = ['pdf', 'docx', 'daily_practice_zip'];
const EXPORT_FORMATS = [...INLINE_EXPORT_FORMATS, ...BINARY_EXPORT_FORMATS];
const EXPORT_TYPES = ['questions', 'paper', 'daily_practice'];
const SCOPE_TYPES = ['collection', 'entry', 'content_node'];
const SUB_QUESTION_ANSWER_KEYS = new Set([
  'correctOptionIndex',
  'correct_option_index',
  'correctOptionIndices',
  'correct_option_indices',
  'answerText',
  'answer_text',
  'answer',
  'correctAnswer',
  'correct_answer',
  'referenceAnswer',
  'reference_answer',
  'solution',
]);
const SUB_QUESTION_EXPLANATION_KEYS = new Set(['explanation', 'analysis']);

interface ExportScope {
  scopeType: 'collection' | 'entry' | 'content_node';
  scopeId: string;
  regionId: string | null;
  entryId: string | null;
  entryName: string | null;
  contentNodeId: string | null;
  contentNodeName: string | null;
  collectionId: string | null;
  collectionName: string | null;
  collectionType: string | null;
  durationMinutes: number | null;
  totalScore: number | null;
}

interface ExportQuestionRow {
  id: string;
  legacyId: string | null;
  entryId: string | null;
  entryName: string | null;
  contentNodeId: string | null;
  contentNodeName: string | null;
  contentNodePath: string | null;
  collectionId: string | null;
  collectionName: string | null;
  subjectId: string | null;
  subjectName: string | null;
  categoryId: string | null;
  categoryName: string | null;
  questionBankId: string | null;
  type: string;
  typeLabel: string | null;
  difficulty: number | null;
  tags: unknown[];
  mediaUrl: string | null;
  hasVideoExplanation: boolean;
  sectionKey: string | null;
  score: number | null;
  order: number;
  versionId: string | null;
  content: string | null;
  options: unknown[];
  correctOptionIndex: number | null;
  correctOptionIndices: unknown[];
  answerText: string | null;
  explanation: string | null;
  subQuestions: unknown[];
  codeLang: string | null;
  codeTemplate: string | null;
  sourceHash: string | null;
}

export interface BuiltExportPayload {
  _tikuExport: string;
  jobId: string;
  tenantId: string;
  exportedAt: string;
  exportType: string;
  format: string;
  title: string;
  scope: ExportScope;
  options: Record<string, unknown>;
  summary: {
    questionCount: number;
    sectionCount: number;
    totalScore: number;
    durationMinutes: number | null;
    cardCount?: number;
    issue?: string;
    date?: string;
  };
  dailyPractice?: DailyPracticeMetadata;
  sections: Array<{
    key: string;
    title: string;
    questions: ReturnType<typeof formatQuestion>[];
    totalScore: number;
    questionCount: number;
  }>;
  questions: ReturnType<typeof formatQuestion>[];
  files: Array<{
    filename: string;
    mimeType: string;
    encoding: 'base64';
    contentBase64: string;
  }>;
  renderHints: {
    pdfLayout: string;
    pageSize: string;
    answerPlacement: string;
    frontendRenderer: string;
  };
}

interface DailyPracticeBrand {
  name: string;
  english: string;
  slogan: string;
  ctaLine: string;
}

interface DailyPracticeMetadata {
  issue: string;
  date: string;
  theme: string;
  cardFormat: string;
  brand: DailyPracticeBrand;
  showAnswer: boolean;
  centerSlot: {
    type: 'cta' | 'image';
    title: string;
    subtitle: string;
    imageUrl: string | null;
  };
  slots: Array<{
    slot: number;
    kind: 'question' | 'center';
    questionId?: unknown;
    subjectName?: unknown;
    typeLabel?: unknown;
    order?: unknown;
    title?: string;
    subtitle?: string;
  }>;
}

function choose(value: unknown, allowed: string[], fallback: string, code: string) {
  const candidate = nullableString(value) || fallback;
  if (!allowed.includes(candidate)) {
    throw new HttpError(400, `${candidate} is not supported`, code);
  }
  return candidate;
}

function base64Json(value: unknown) {
  return Buffer.from(JSON.stringify(value, null, 2), 'utf8').toString('base64');
}

function contentHash(value: unknown) {
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}

function exportLimit(value: unknown) {
  const parsed = Number(value ?? 1000);
  if (!Number.isFinite(parsed)) return 1000;
  return Math.max(1, Math.min(Math.trunc(parsed), 5000));
}

function exportLimitForType(exportType: string, value: unknown) {
  const limit = exportLimit(value);
  return exportType === 'daily_practice' ? Math.min(limit, 8) : limit;
}

function isBinaryExportFormat(format: string) {
  return BINARY_EXPORT_FORMATS.includes(format);
}

function assertExportFormatMatchesType(format: string, exportType: string) {
  if (format === 'daily_practice_zip' && exportType !== 'daily_practice') {
    throw new HttpError(
      400,
      'daily_practice_zip is only supported for daily_practice exports',
      'DAILY_PRACTICE_ZIP_REQUIRES_DAILY_PRACTICE',
    );
  }
}

function jsonArray(value: unknown) {
  return Array.isArray(value) ? value : [];
}

function redactSubQuestions(value: unknown, includeAnswers: boolean, includeExplanations: boolean): unknown[] {
  if (!Array.isArray(value)) return [];

  return value.map(item => {
    if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
    const output: Record<string, unknown> = {};
    for (const [key, nested] of Object.entries(item as Record<string, unknown>)) {
      if (!includeAnswers && SUB_QUESTION_ANSWER_KEYS.has(key)) continue;
      if (!includeExplanations && SUB_QUESTION_EXPLANATION_KEYS.has(key)) continue;
      if (key === 'subQuestions' || key === 'sub_questions') {
        output[key] = redactSubQuestions(nested, includeAnswers, includeExplanations);
        continue;
      }
      output[key] = nested;
    }
    return output;
  });
}

function formatQuestion(row: ExportQuestionRow, includeAnswers: boolean, includeExplanations: boolean, includeVideoRefs: boolean) {
  const question: Record<string, unknown> = {
    id: row.id,
    legacyId: row.legacyId,
    type: row.type,
    typeLabel: row.typeLabel,
    content: row.content,
    options: jsonArray(row.options),
    difficulty: row.difficulty,
    tags: jsonArray(row.tags),
    mediaUrl: row.mediaUrl,
    sectionKey: row.sectionKey,
    score: row.score,
    order: row.order,
    subjectId: row.subjectId,
    subjectName: row.subjectName,
    categoryId: row.categoryId,
    categoryName: row.categoryName,
    entryId: row.entryId,
    entryName: row.entryName,
    contentNodeId: row.contentNodeId,
    contentNodeName: row.contentNodeName,
    collectionId: row.collectionId,
    collectionName: row.collectionName,
    versionId: row.versionId,
    sourceHash: row.sourceHash,
  };
  if (includeAnswers) {
    question.correctOptionIndex = row.correctOptionIndex;
    question.correctOptionIndices = jsonArray(row.correctOptionIndices);
    question.answerText = row.answerText;
  }
  if (includeExplanations) {
    question.explanation = row.explanation;
  }
  const subQuestions = redactSubQuestions(row.subQuestions, includeAnswers, includeExplanations);
  if (subQuestions.length) {
    question.subQuestions = subQuestions;
  }
  if (row.codeLang || row.codeTemplate) {
    question.codeLang = row.codeLang;
    question.codeTemplate = row.codeTemplate;
  }
  if (includeVideoRefs) {
    question.hasVideoExplanation = row.hasVideoExplanation;
  }
  return question;
}

function groupBySection(questions: ReturnType<typeof formatQuestion>[]) {
  const groups = new Map<string, { key: string; title: string; questions: ReturnType<typeof formatQuestion>[]; totalScore: number }>();
  for (const question of questions) {
    const key = typeof question.sectionKey === 'string' && question.sectionKey ? question.sectionKey : String(question.type || 'default');
    const title = typeof question.typeLabel === 'string' && question.typeLabel ? question.typeLabel : key;
    const group = groups.get(key) || { key, title, questions: [], totalScore: 0 };
    group.questions.push(question);
    group.totalScore += typeof question.score === 'number' ? question.score : 0;
    groups.set(key, group);
  }
  return Array.from(groups.values()).map(group => ({
    ...group,
    questionCount: group.questions.length,
  }));
}

function todayDateString() {
  return new Date().toISOString().slice(0, 10);
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function stringValue(value: unknown, fallback: string) {
  return typeof value === 'string' && value.trim() ? value.trim().slice(0, 120) : fallback;
}

function dailyPracticeBrand(options: Record<string, unknown>): DailyPracticeBrand {
  const brand = objectValue(options.brand);
  return {
    name: stringValue(brand.name, '恭学教育'),
    english: stringValue(brand.english, 'GONGXUE EDU'),
    slogan: stringValue(brand.slogan, '专注高职升本'),
    ctaLine: stringValue(brand.ctaLine, '每日一练 · 精选八题 · 稳步上岸'),
  };
}

function dailyPracticeMetadata(input: {
  questions: ReturnType<typeof formatQuestion>[];
  options: Record<string, unknown>;
  includeAnswers: boolean;
}): DailyPracticeMetadata {
  const issue = stringValue(input.options.issue, stringValue(input.options.title, '每日一练'));
  const date = stringValue(input.options.date, todayDateString());
  const center = objectValue(input.options.centerSlot);
  const brand = dailyPracticeBrand(input.options);
  const questionSlots = [0, 1, 2, 3, 5, 6, 7, 8];
  const slots: DailyPracticeMetadata['slots'] = [];
  input.questions.slice(0, 8).forEach((question, index) => {
    slots.push({
      slot: questionSlots[index],
      kind: 'question',
      questionId: question.id,
      subjectName: question.subjectName,
      typeLabel: question.typeLabel,
      order: index + 1,
    });
  });
  slots.splice(Math.min(4, slots.length), 0, {
    slot: 4,
    kind: 'center',
    title: stringValue(center.title, brand.name),
    subtitle: stringValue(center.subtitle, brand.ctaLine),
  });
  return {
    issue,
    date,
    theme: stringValue(input.options.theme, 'default'),
    cardFormat: stringValue(input.options.cardFormat, '1:1'),
    brand,
    showAnswer: input.includeAnswers,
    centerSlot: {
      type: stringValue(center.type, 'cta') === 'image' ? 'image' : 'cta',
      title: stringValue(center.title, brand.name),
      subtitle: stringValue(center.subtitle, brand.ctaLine),
      imageUrl: typeof center.imageUrl === 'string' && center.imageUrl.trim() ? center.imageUrl.trim() : null,
    },
    slots,
  };
}

function buildExportPayload(input: {
  auth: TenantContentAuth;
  jobId: string;
  scope: ExportScope;
  exportType: string;
  format: string;
  includeAnswers: boolean;
  includeExplanations: boolean;
  includeVideoRefs: boolean;
  questions: ReturnType<typeof formatQuestion>[];
  options: Record<string, unknown>;
  createdAt: string;
}): BuiltExportPayload {
  const sections = groupBySection(input.questions);
  const title = typeof input.options.title === 'string' && input.options.title.trim()
    ? input.options.title.trim()
    : input.scope.collectionName || input.scope.contentNodeName || input.scope.entryName || '题库导出';
  const payload = {
    _tikuExport: '3.0',
    jobId: input.jobId,
    tenantId: input.auth.tenantId,
    exportedAt: input.createdAt,
    exportType: input.exportType,
    format: input.format,
    title,
    scope: input.scope,
    options: {
      includeAnswers: input.includeAnswers,
      includeExplanations: input.includeExplanations,
      includeVideoRefs: input.includeVideoRefs,
      ...input.options,
    },
    summary: {
      questionCount: input.questions.length,
      sectionCount: sections.length,
      totalScore: sections.reduce((sum, section) => sum + section.totalScore, 0),
      durationMinutes: input.scope.durationMinutes,
      ...(input.exportType === 'daily_practice'
        ? {
          cardCount: Math.min(input.questions.length, 8),
          issue: stringValue(input.options.issue, stringValue(input.options.title, '每日一练')),
          date: stringValue(input.options.date, todayDateString()),
        }
        : {}),
    },
    ...(input.exportType === 'daily_practice'
      ? {
        dailyPractice: dailyPracticeMetadata({
          questions: input.questions,
          options: input.options,
          includeAnswers: input.includeAnswers,
        }),
      }
      : {}),
    sections,
    questions: input.questions,
  };

  return {
    ...payload,
    files: [
      {
        filename: `${title.replace(/[\\/:*?"<>|]+/g, '_') || 'question-export'}.json`,
        mimeType: 'application/json',
        encoding: 'base64',
        contentBase64: base64Json(payload),
      },
    ],
    renderHints: {
      pdfLayout: input.exportType === 'daily_practice' ? 'daily_practice' : input.exportType === 'paper' ? 'paper' : 'practice',
      pageSize: 'A4',
      answerPlacement: input.includeAnswers ? 'inline_or_appendix' : 'hidden',
      frontendRenderer: 'apps/taro admin export renderer',
    },
  };
}

async function loadScope(client: pg.PoolClient, tenantId: string, scopeType: string, scopeId: string): Promise<ExportScope> {
  if (scopeType === 'collection') {
    const result = await client.query<{
      id: string;
      regionId: string | null;
      entryId: string | null;
      entryName: string | null;
      contentNodeId: string | null;
      contentNodeName: string | null;
      collectionName: string;
      collectionType: string;
      durationMinutes: number | null;
      totalScore: string | null;
    }>(
      `
      select qc.id, qc.region_id as "regionId",
             qc.entry_id as "entryId", ce.name as "entryName",
             qc.node_id as "contentNodeId", cn.name as "contentNodeName",
             qc.name as "collectionName", qc.collection_type as "collectionType",
             qc.duration_minutes as "durationMinutes", qc.total_score::text as "totalScore"
      from public.question_collections qc
      left join public.content_entries ce on ce.id = qc.entry_id and ce.tenant_id = qc.tenant_id
      left join public.content_nodes cn on cn.id = qc.node_id and cn.tenant_id = qc.tenant_id
      where qc.tenant_id = $1 and qc.id = $2 and qc.status = 'active'
      limit 1
      `,
      [tenantId, scopeId],
    );
    const row = result.rows[0];
    if (!row) throw new HttpError(404, 'Question collection not found', 'QUESTION_COLLECTION_NOT_FOUND');
    return {
      scopeType: 'collection',
      scopeId,
      regionId: row.regionId,
      entryId: row.entryId,
      entryName: row.entryName,
      contentNodeId: row.contentNodeId,
      contentNodeName: row.contentNodeName,
      collectionId: row.id,
      collectionName: row.collectionName,
      collectionType: row.collectionType,
      durationMinutes: row.durationMinutes,
      totalScore: row.totalScore === null ? null : Number(row.totalScore),
    };
  }

  if (scopeType === 'entry') {
    const result = await client.query<{ id: string; regionId: string | null; name: string }>(
      `
      select id, region_id as "regionId", name
      from public.content_entries
      where tenant_id = $1 and id = $2 and entry_type = 'question_practice'
      limit 1
      `,
      [tenantId, scopeId],
    );
    const row = result.rows[0];
    if (!row) throw new HttpError(404, 'Content entry not found', 'CONTENT_ENTRY_NOT_FOUND');
    return {
      scopeType: 'entry',
      scopeId,
      regionId: row.regionId,
      entryId: row.id,
      entryName: row.name,
      contentNodeId: null,
      contentNodeName: null,
      collectionId: null,
      collectionName: null,
      collectionType: null,
      durationMinutes: null,
      totalScore: null,
    };
  }

  const result = await client.query<{
    id: string;
    regionId: string | null;
    name: string;
    entryId: string;
    entryName: string;
  }>(
    `
    select cn.id, cn.region_id as "regionId", cn.name,
           cn.entry_id as "entryId", ce.name as "entryName"
    from public.content_nodes cn
    join public.content_entries ce on ce.id = cn.entry_id and ce.tenant_id = cn.tenant_id
    where cn.tenant_id = $1 and cn.id = $2
    limit 1
    `,
    [tenantId, scopeId],
  );
  const row = result.rows[0];
  if (!row) throw new HttpError(404, 'Content node not found', 'CONTENT_NODE_NOT_FOUND');
  return {
    scopeType: 'content_node',
    scopeId,
    regionId: row.regionId,
    entryId: row.entryId,
    entryName: row.entryName,
    contentNodeId: row.id,
    contentNodeName: row.name,
    collectionId: null,
    collectionName: null,
    collectionType: null,
    durationMinutes: null,
    totalScore: null,
  };
}

async function loadQuestions(client: pg.PoolClient, tenantId: string, scopeType: string, scopeId: string, limit: number) {
  const params: unknown[] = [tenantId, scopeId, limit];
  let scopeFilter = '';
  let orderBy = 'q.created_at asc';
  if (scopeType === 'collection') {
    scopeFilter = 'q.id in (select question_id from public.question_collection_items where tenant_id = $1 and collection_id = $2)';
    orderBy = 'coalesce(ci.sort_order, 0) asc, q.created_at asc';
  } else if (scopeType === 'entry') {
    scopeFilter = 'q.entry_id = $2';
    orderBy = 'coalesce(cn.path::text, q.created_at::text) asc, coalesce(ci.sort_order, 0) asc, q.created_at asc';
  } else {
    scopeFilter = `
      (
        q.content_node_id = $2
        or q.content_node_id in (
          select child.id
          from public.content_nodes root
          join public.content_nodes child on child.tenant_id = root.tenant_id and child.path <@ root.path
          where root.tenant_id = $1 and root.id = $2
        )
      )
    `;
    orderBy = 'coalesce(cn.path::text, q.created_at::text) asc, coalesce(ci.sort_order, 0) asc, q.created_at asc';
  }

  const result = await client.query<ExportQuestionRow>(
    `
    select q.id, q.legacy_id as "legacyId",
           q.entry_id as "entryId", ce.name as "entryName",
           q.content_node_id as "contentNodeId", cn.name as "contentNodeName", cn.path::text as "contentNodePath",
           coalesce(ci.collection_id, q.primary_collection_id) as "collectionId", qc.name as "collectionName",
           q.subject_id as "subjectId", s.name as "subjectName",
           q.category_id as "categoryId", c.name as "categoryName",
           q.question_bank_id as "questionBankId",
           q.type, q.type_label as "typeLabel", q.difficulty, q.tags,
           q.media_url as "mediaUrl", q.has_video_explanation as "hasVideoExplanation",
           ci.section_key as "sectionKey", ci.score, coalesce(ci.sort_order, 0) as "order",
           v.id as "versionId", v.content, v.options,
           v.correct_option_index as "correctOptionIndex",
           v.correct_option_indices as "correctOptionIndices",
           v.answer_text as "answerText", v.explanation, v.sub_questions as "subQuestions",
           v.code_lang as "codeLang", v.code_template as "codeTemplate", v.source_hash as "sourceHash"
    from public.questions q
    left join public.question_collection_items ci
      on ci.tenant_id = q.tenant_id
     and ci.question_id = q.id
     and (
       ($4::text = 'collection' and ci.collection_id = $2)
       or ($4::text <> 'collection' and ci.collection_id = q.primary_collection_id)
     )
    left join public.question_collections qc on qc.id = coalesce(ci.collection_id, q.primary_collection_id) and qc.tenant_id = q.tenant_id
    left join public.content_entries ce on ce.id = q.entry_id and ce.tenant_id = q.tenant_id
    left join public.content_nodes cn on cn.id = q.content_node_id and cn.tenant_id = q.tenant_id
    left join public.subjects s on s.id = q.subject_id and s.tenant_id = q.tenant_id
    left join public.categories c on c.id = q.category_id and c.tenant_id = q.tenant_id
    left join public.question_versions v on v.id = q.current_version_id
    where q.tenant_id = $1
      and q.status = 'published'
      and ${scopeFilter}
    order by ${orderBy}
    limit $3
    `,
    [...params, scopeType],
  );
  return result.rows;
}

async function createExportJob(input: {
  client: pg.PoolClient;
  auth: TenantContentAuth;
  scope: ExportScope;
  scopeType: ExportScope['scopeType'];
  scopeId: string;
  exportType: string;
  format: string;
  includeAnswers: boolean;
  includeExplanations: boolean;
  includeVideoRefs: boolean;
  questionCount: number;
  options: Record<string, unknown>;
  outputMetadata: Record<string, unknown>;
  status: 'pending' | 'completed';
  finishedAt?: boolean;
}) {
  const job = await input.client.query<{
    id: string;
    createdAt: string;
  }>(
    `
    insert into public.content_export_jobs (
      tenant_id, created_by, export_type, format, scope_type, scope_id,
      status, include_answers, include_explanations, include_video_refs,
      question_count, summary, options, output_metadata, finished_at
    )
    values (
      $1, $2, $3, $4, $5, $6,
      $7, $8, $9, $10,
      $11, $12::jsonb, $13::jsonb, $14::jsonb,
      case when $15::boolean then now() else null end
    )
    returning id, created_at as "createdAt"
    `,
    [
      input.auth.tenantId,
      input.auth.userId,
      input.exportType,
      input.format,
      input.scopeType,
      input.scopeId,
      input.status,
      input.includeAnswers,
      input.includeExplanations,
      input.includeVideoRefs,
      input.questionCount,
      JSON.stringify({
        questionCount: input.questionCount,
        scopeName: input.scope.collectionName || input.scope.contentNodeName || input.scope.entryName,
        format: input.format,
        exportType: input.exportType,
      }),
      jsonObjectValue(input.options),
      JSON.stringify(input.outputMetadata),
      input.finishedAt === true,
    ],
  );
  return job.rows[0];
}

export async function buildExportPayloadForJob(input: {
  client: pg.PoolClient;
  tenantId: string;
  jobId: string;
  createdAt?: string;
}): Promise<BuiltExportPayload> {
  const job = await input.client.query<{
    id: string;
    tenantId: string;
    createdBy: string | null;
    exportType: string;
    format: string;
    scopeType: ExportScope['scopeType'];
    scopeId: string;
    includeAnswers: boolean;
    includeExplanations: boolean;
    includeVideoRefs: boolean;
    options: Record<string, unknown>;
    outputMetadata: Record<string, unknown>;
    createdAt: string;
  }>(
    `
    select id, tenant_id as "tenantId", created_by as "createdBy",
           export_type as "exportType", format,
           scope_type as "scopeType", scope_id as "scopeId",
           include_answers as "includeAnswers",
           include_explanations as "includeExplanations",
           include_video_refs as "includeVideoRefs",
           options, output_metadata as "outputMetadata",
           created_at as "createdAt"
    from public.content_export_jobs
    where tenant_id = $1 and id = $2
    limit 1
    `,
    [input.tenantId, input.jobId],
  );
  const row = job.rows[0];
  if (!row) throw new HttpError(404, 'Export job not found', 'EXPORT_JOB_NOT_FOUND');

  const scope = await loadScope(input.client, row.tenantId, row.scopeType, row.scopeId);
  const maxQuestionLimit = Number(row.outputMetadata?.maxQuestionLimit ?? row.options?.limit ?? 1000);
  const limit = exportLimit(maxQuestionLimit);
  const rows = await loadQuestions(input.client, row.tenantId, row.scopeType, row.scopeId, limit);
  const questions = rows.map(question => formatQuestion(
    question,
    row.includeAnswers,
    row.includeExplanations,
    row.includeVideoRefs,
  ));

  return buildExportPayload({
    auth: {
      tenantId: row.tenantId,
      userId: row.createdBy || row.tenantId,
      role: 'system_worker',
      permissions: { 'content:*': true },
      templatePermissions: {},
    },
    jobId: row.id,
    scope,
    exportType: row.exportType,
    format: row.format,
    includeAnswers: row.includeAnswers,
    includeExplanations: row.includeExplanations,
    includeVideoRefs: row.includeVideoRefs,
    questions,
    options: row.options || {},
    createdAt: input.createdAt || row.createdAt || new Date().toISOString(),
  });
}

export async function createQuestionExportRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);
  const scopeType = choose(body.scopeType, SCOPE_TYPES, 'collection', 'INVALID_EXPORT_SCOPE') as ExportScope['scopeType'];
  const scopeId = requiredString(body, 'scopeId');
  const format = choose(body.format, EXPORT_FORMATS, 'json', 'INVALID_EXPORT_FORMAT');
  const exportType = choose(
    body.exportType,
    EXPORT_TYPES,
    format === 'daily_practice_zip' ? 'daily_practice' : format === 'paper_json' || isBinaryExportFormat(format) ? 'paper' : 'questions',
    'INVALID_EXPORT_TYPE',
  );
  assertExportFormatMatchesType(format, exportType);
  const includeAnswers = boolValue(body.includeAnswers, true);
  const includeExplanations = boolValue(body.includeExplanations, includeAnswers);
  const includeVideoRefs = boolValue(body.includeVideoRefs, false);
  const limit = exportLimitForType(exportType, body.limit);
  const options = body.options && typeof body.options === 'object' && !Array.isArray(body.options) ? body.options as Record<string, unknown> : {};

  const result = await transaction(async client => {
    const scope = await loadScope(client, auth.tenantId, scopeType, scopeId);
    const rows = await loadQuestions(client, auth.tenantId, scopeType, scopeId, limit);
    const questions = rows.map(row => formatQuestion(row, includeAnswers, includeExplanations, includeVideoRefs));
    const createdAt = new Date().toISOString();

    const prePayload = {
      tenantId: auth.tenantId,
      scope,
      exportType,
      format,
      includeAnswers,
      includeExplanations,
      includeVideoRefs,
      questionCount: questions.length,
      questions,
      options,
      createdAt,
    };
    const outputHash = contentHash(prePayload);
    const binaryExport = isBinaryExportFormat(format);

    const job = await createExportJob({
      client,
      auth,
      scope,
      scopeType,
      scopeId,
      exportType,
      format,
      includeAnswers,
      includeExplanations,
      includeVideoRefs,
      questionCount: questions.length,
      options: { ...options, limit },
      outputMetadata: {
        outputHash,
        delivery: binaryExport ? 'content_asset_pending' : 'inline_payload',
        maxQuestionLimit: limit,
        renderer: binaryExport ? 'worker:exports' : 'api:inline-json',
      },
      status: binaryExport ? 'pending' : 'completed',
      finishedAt: !binaryExport,
    });

    const payload = binaryExport
      ? null
      : buildExportPayload({
        auth,
        jobId: job.id,
        scope,
        exportType,
        format,
        includeAnswers,
        includeExplanations,
        includeVideoRefs,
        questions,
        options,
        createdAt: job.createdAt || createdAt,
      });

    await client.query(
      `
      insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
      values ($1, $2, 'content.questions.exported', 'content_export_job', $3, $4::jsonb)
      `,
      [
        auth.tenantId,
        auth.userId,
        job.id,
        JSON.stringify({
          scopeType,
          scopeId,
          format,
          exportType,
          questionCount: questions.length,
          includeAnswers,
          includeExplanations,
          includeVideoRefs,
          outputHash,
          delivery: binaryExport ? 'content_asset_pending' : 'inline_payload',
        }),
      ],
    );

    return {
      job: {
        id: job.id,
        status: binaryExport ? 'pending' : 'completed',
        createdAt: job.createdAt,
        questionCount: questions.length,
        outputHash,
      },
      export: payload,
    };
  });

  return result;
}

export async function questionExportJobsRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const scopeType = stringParam(ctx, 'scopeType');
  const scopeId = stringParam(ctx, 'scopeId');
  const limit = intParam(ctx, 'limit', 50, 200);
  const params: unknown[] = [auth.tenantId];
  const filters = ['tenant_id = $1'];
  if (scopeType) {
    if (!SCOPE_TYPES.includes(scopeType)) {
      throw new HttpError(400, `${scopeType} is not supported`, 'INVALID_EXPORT_SCOPE');
    }
    params.push(scopeType);
    filters.push(`scope_type = $${params.length}`);
  }
  if (scopeId) {
    params.push(scopeId);
    filters.push(`scope_id = $${params.length}`);
  }
  params.push(limit);

  const items = await query(
    `
    select id, export_type as "exportType", format, scope_type as "scopeType",
           scope_id as "scopeId", status, include_answers as "includeAnswers",
           include_explanations as "includeExplanations",
           include_video_refs as "includeVideoRefs", question_count as "questionCount",
           summary, options, output_metadata as "outputMetadata",
           asset_id as "assetId", error_message as "errorMessage",
           started_at as "startedAt", finished_at as "finishedAt",
           attempt_count as "attemptCount", max_attempts as "maxAttempts",
           created_by as "createdBy", created_at as "createdAt", updated_at as "updatedAt"
    from public.content_export_jobs
    where ${filters.join(' and ')}
    order by created_at desc
    limit $${params.length}
    `,
    params,
  );

  return { items };
}
