import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import pg from 'pg';

const DEFAULT_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const DATABASE_URL = process.env.DATABASE_URL || DEFAULT_DATABASE_URL;
const OUTPUT_DIR = process.env.PERF_OUTPUT_DIR || 'docs/refactor/performance-reports';
const API_BASE_ENV = process.env.PERF_API_BASE || process.env.API_BASE || '';
const START_SERVER = envBool('PERF_START_SERVER', !API_BASE_ENV);
const DURATION_SECONDS = envNumber('PERF_DURATION_SECONDS', 15);
const CONCURRENCY = envNumber('PERF_CONCURRENCY', 6);
const RAMP_SECONDS = envNumber('PERF_RAMP_SECONDS', 3, { allowZero: true });
const INCLUDE_WRITES = envBool('PERF_INCLUDE_WRITES', false);
const INCLUDE_LEADERBOARD = envBool('PERF_INCLUDE_LEADERBOARD', false);
const PRACTICE_FLOW_RATIO = Math.min(Math.max(envNumber('PERF_PRACTICE_FLOW_RATIO', 0.15, { allowZero: true }), 0), 1);
const PRACTICE_FLOW_ANSWERS = envNumber('PERF_PRACTICE_FLOW_ANSWERS', 3);
const ENSURE_SVIP_FOR_WRITES = envBool('PERF_ENSURE_SVIP_FOR_WRITES', true);
const START_PORT = Number(process.env.PERF_API_PORT || 0) || 0;
const TENANT_CODE = process.env.PERF_TENANT_CODE || 'master';
const QUESTION_LIMIT = envNumber('PERF_QUESTION_LIMIT', 20);
const MAX_ERRORS_TO_KEEP = envNumber('PERF_MAX_ERRORS', 20);
const DEFAULT_TIMEOUT_MS = envNumber('PERF_REQUEST_TIMEOUT_MS', 15_000);

let apiBase = API_BASE_ENV || '';
let serverProcess = null;
let serverLogs = '';

function envBool(key, fallback) {
  const value = process.env[key];
  if (value === undefined || value === '') return fallback;
  return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}

function envNumber(key, fallback, options = {}) {
  const raw = process.env[key];
  if (!raw) return fallback;
  const value = Number(raw);
  if (!Number.isFinite(value)) return fallback;
  if (value > 0) return value;
  if (options.allowZero && value === 0) return 0;
  return fallback;
}

function getFreePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.on('error', reject);
    server.listen(START_PORT, '127.0.0.1', () => {
      const address = server.address();
      server.close(() => resolve(address.port));
    });
  });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function shanghaiTimestampForFile(date = new Date()) {
  const parts = Object.fromEntries(
    new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    }).formatToParts(date).map(part => [part.type, part.value]),
  );
  return `${parts.year}${parts.month}${parts.day}-${parts.hour}${parts.minute}${parts.second}`;
}

function buildUrl(baseUrl, endpoint) {
  const target = new URL(endpoint.path, baseUrl);
  for (const [key, value] of Object.entries(endpoint.query || {})) {
    if (value !== undefined && value !== null && value !== '') {
      target.searchParams.set(key, String(value));
    }
  }
  return target;
}

async function requestJson(endpoint, context, timeoutMs = DEFAULT_TIMEOUT_MS) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  const started = performance.now();
  const tenantId = context.tenantId || context.tenant?.id;
  const userId = context.userId || context.user?.id;
  try {
    const response = await fetch(buildUrl(apiBase, endpoint), {
      method: endpoint.method || 'GET',
      signal: controller.signal,
      headers: {
        'content-type': 'application/json',
        ...(endpoint.tenantHeader === false || !tenantId ? {} : { 'x-tenant-id': tenantId }),
        ...(endpoint.userHeader === false || !userId ? {} : { 'x-user-id': userId }),
        ...(endpoint.headers || {}),
      },
      body: endpoint.body ? JSON.stringify(endpoint.body(context)) : undefined,
    });
    const elapsedMs = performance.now() - started;
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      return {
        ok: false,
        status: response.status,
        elapsedMs,
        error: payload?.code || payload?.error || payload?.message || response.statusText,
        message: payload?.error || payload?.message || response.statusText,
      };
    }
    return { ok: true, status: response.status, elapsedMs, bytes: JSON.stringify(payload).length, payload };
  } catch (error) {
    return {
      ok: false,
      status: 0,
      elapsedMs: performance.now() - started,
      error: error?.name === 'AbortError' ? 'REQUEST_TIMEOUT' : error?.message || String(error),
    };
  } finally {
    clearTimeout(timeout);
  }
}

function sampleFromResult(name, result, startedAt, finishedAt) {
  return {
    name,
    ok: result.ok,
    status: result.status,
    elapsedMs: result.elapsedMs,
    startedAt,
    finishedAt,
  };
}

function pickAnswerPayload(question) {
  const subQuestions = Array.isArray(question.subQuestions) ? question.subQuestions : [];
  if (subQuestions.length) {
    return {
      subAnswers: subQuestions.slice(0, Math.max(PRACTICE_FLOW_ANSWERS, 1)).map((subQuestion, index) => {
        const subCorrectIndices = Array.isArray(subQuestion.correctOptionIndices) ? subQuestion.correctOptionIndices : [];
        const subCorrectIndex = subQuestion.correctOptionIndex;
        const subAnswer = {
          subQuestionId: subQuestion.id || subQuestion.subQuestionId || subQuestion.key || `sub_${index + 1}`,
          selectedOptions: [],
          answerText: '',
        };
        if (subCorrectIndices.length) {
          subAnswer.selectedOptions = subCorrectIndices.map(item => String(item));
        } else if (subCorrectIndex !== null && subCorrectIndex !== undefined && Number.isFinite(Number(subCorrectIndex))) {
          subAnswer.selectedOptions = [String(Number(subCorrectIndex))];
        } else if (Array.isArray(subQuestion.options) && subQuestion.options.length) {
          subAnswer.selectedOptions = ['0'];
        } else {
          subAnswer.answerText = 'performance benchmark answer';
          subAnswer.selfJudgedCorrect = true;
        }
        return subAnswer;
      }),
    };
  }
  const correctIndices = Array.isArray(question.correctOptionIndices) ? question.correctOptionIndices : [];
  const correctIndex = question.correctOptionIndex;
  if (correctIndices.length) {
    return { selectedOptions: correctIndices.map(item => String(item)) };
  }
  if (correctIndex !== null && correctIndex !== undefined && Number.isFinite(Number(correctIndex))) {
    return { selectedOptions: [String(Number(correctIndex))] };
  }
  if (Array.isArray(question.options) && question.options.length) {
    return { selectedOptions: ['0'] };
  }
  return { answerText: 'performance benchmark answer', selfJudgedCorrect: true };
}

async function runPracticeFlow(context, samples, errors) {
  if (!context.blueprint || !context.collection) return false;
  const flowStartedAt = performance.now();
  const create = await requestJson({
    name: 'learning.practice_flow.create',
    method: 'POST',
    path: '/api/learning/practice-sessions',
    body: () => ({
      blueprintId: context.blueprint.id,
      collectionId: context.collection.id,
      mode: context.blueprint.mode || 'sequential',
      questionLimit: Math.min(QUESTION_LIMIT, Math.max(PRACTICE_FLOW_ANSWERS, 3)),
      metadata: { source: 'api-performance-benchmark' },
    }),
  }, context);
  const flowFinishedCreate = performance.now();
  samples.push(sampleFromResult('learning.practice_flow.create', create, flowStartedAt, flowFinishedCreate));
  if (!create.ok) {
    pushError(errors, 'learning.practice_flow.create', create);
    return true;
  }

  const practiceSessionId = create.payload?.item?.id;
  if (!practiceSessionId) {
    pushError(errors, 'learning.practice_flow.create', {
      status: 200,
      elapsedMs: create.elapsedMs,
      error: 'PRACTICE_SESSION_ID_MISSING',
      message: 'Practice session create response did not include item.id',
    });
    return true;
  }

  const detailStartedAt = performance.now();
  const detail = await requestJson({
    name: 'learning.practice_flow.detail',
    method: 'GET',
    path: '/api/learning/practice-sessions/detail',
    query: { practiceSessionId },
  }, context);
  const detailFinishedAt = performance.now();
  samples.push(sampleFromResult('learning.practice_flow.detail', detail, detailStartedAt, detailFinishedAt));
  if (!detail.ok) {
    pushError(errors, 'learning.practice_flow.detail', detail);
    return true;
  }

  const questions = Array.isArray(detail.payload?.item?.questions) ? detail.payload.item.questions : [];
  for (const question of questions.slice(0, PRACTICE_FLOW_ANSWERS)) {
    if (!question?.id) continue;
    const answerStartedAt = performance.now();
    const answer = await requestJson({
      name: 'learning.practice_flow.answer',
      method: 'POST',
      path: '/api/learning/answers',
      body: () => ({
        practiceSessionId,
        questionId: question.id,
        ...pickAnswerPayload(question),
      }),
    }, context);
    const answerFinishedAt = performance.now();
    samples.push(sampleFromResult('learning.practice_flow.answer', answer, answerStartedAt, answerFinishedAt));
    if (!answer.ok) {
      pushError(errors, 'learning.practice_flow.answer', answer);
      return true;
    }
  }

  const submitStartedAt = performance.now();
  const submit = await requestJson({
    name: 'learning.practice_flow.submit',
    method: 'POST',
    path: '/api/learning/practice-sessions/submit',
    body: () => ({ practiceSessionId }),
  }, context);
  const submitFinishedAt = performance.now();
  samples.push(sampleFromResult('learning.practice_flow.submit', submit, submitStartedAt, submitFinishedAt));
  if (!submit.ok) {
    pushError(errors, 'learning.practice_flow.submit', submit);
    return true;
  }

  const reportStartedAt = performance.now();
  const report = await requestJson({
    name: 'learning.practice_flow.report',
    method: 'GET',
    path: '/api/learning/practice-sessions/report',
    query: { practiceSessionId },
  }, context);
  const reportFinishedAt = performance.now();
  samples.push(sampleFromResult('learning.practice_flow.report', report, reportStartedAt, reportFinishedAt));
  if (!report.ok) pushError(errors, 'learning.practice_flow.report', report);
  return true;
}

function pushError(errors, endpointName, result) {
  if (errors.length >= MAX_ERRORS_TO_KEEP) return;
  errors.push({
    endpoint: endpointName,
    status: result.status,
    error: result.error,
    message: result.message,
    elapsedMs: round(result.elapsedMs),
  });
}

async function waitForHealth(timeoutMs = 20_000) {
  const started = Date.now();
  let lastError = null;
  while (Date.now() - started < timeoutMs) {
    try {
      const response = await fetch(new URL('/health', apiBase));
      const payload = await response.json().catch(() => ({}));
      if (response.ok && payload.ok) return;
    } catch (error) {
      lastError = error;
    }
    await sleep(250);
  }
  throw new Error(`API server did not become healthy. ${lastError?.message || ''}\n${serverLogs}`);
}

async function startServerIfNeeded() {
  if (!START_SERVER) return;
  const port = await getFreePort();
  apiBase = `http://127.0.0.1:${port}`;
  serverProcess = spawn(process.execPath, ['apps/api/dist/apps/api/src/server.js'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      PORT: String(port),
      DATABASE_URL,
      MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '1048576',
      MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '10485760',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  serverProcess.stdout.on('data', chunk => {
    serverLogs += chunk.toString();
  });
  serverProcess.stderr.on('data', chunk => {
    serverLogs += chunk.toString();
  });
  await waitForHealth();
}

function stopServer() {
  if (serverProcess && !serverProcess.killed) {
    serverProcess.kill();
  }
}

async function one(pool, sql, params = []) {
  const result = await pool.query(sql, params);
  return result.rows[0] || null;
}

async function many(pool, sql, params = []) {
  const result = await pool.query(sql, params);
  return result.rows;
}

async function discoverBenchmarkContext() {
  const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 4 });
  try {
    const tenant = await one(
      pool,
      `
      select id, slug, name
      from public.tenants
      where status = 'active'
      order by case when slug = $1 then 0 else 1 end, slug asc
      limit 1
      `,
      [TENANT_CODE],
    );
    if (!tenant) throw new Error('No active tenant found. Run smoke seed or PocketBase import first.');

    const user = await one(
      pool,
      `
      select pu.id, coalesce(pu.name, pu.username, pu.phone, pu.legacy_id, pu.id::text) as name
      from public.tenant_memberships tm
      join public.platform_users pu on pu.id = tm.user_id
      left join public.entitlements e on e.tenant_id = tm.tenant_id
        and e.user_id = pu.id
        and e.entitlement_type = 'svip'
        and e.status = 'active'
        and (e.expires_at is null or e.expires_at > now())
      where tm.tenant_id = $1 and tm.status = 'active' and tm.role = 'student'
      group by pu.id, pu.name, pu.username, pu.phone, pu.legacy_id, pu.created_at
      order by case when count(e.id) > 0 then 0 else 1 end, pu.created_at asc
      limit 1
      `,
      [tenant.id],
    );
    if (!user) throw new Error(`No active student user found for tenant ${tenant.slug}.`);

    const entry = await one(
      pool,
      `
      select id, name
      from public.content_entries
      where tenant_id = $1
        and entry_type = 'question_practice'
        and is_active = true
        and visibility <> 'hidden'
      order by sort_order asc, created_at asc
      limit 1
      `,
      [tenant.id],
    );

    const node = entry
      ? await one(
          pool,
          `
          select id, name
          from public.content_nodes
          where tenant_id = $1 and entry_id = $2 and is_active = true
          order by depth asc, sort_order asc, created_at asc
          limit 1
          `,
          [tenant.id, entry.id],
        )
      : null;

    const collection = await one(
      pool,
      `
      select id, name, question_count as "questionCount", entry_id as "entryId", node_id as "nodeId"
      from public.question_collections
      where tenant_id = $1 and status = 'active' and question_count > 0
      order by question_count desc, sort_order asc, created_at asc
      limit 1
      `,
      [tenant.id],
    );

    const blueprint = collection
      ? await one(
          pool,
          `
          select id, name, mode
          from public.practice_blueprints
          where tenant_id = $1 and status = 'active' and collection_id = $2
          order by case mode when 'sequential' then 0 when 'random' then 1 else 2 end, sort_order asc
          limit 1
          `,
          [tenant.id, collection.id],
        )
      : null;

    const vocabularyUnit = await one(
      pool,
      `
      select id, name
      from public.vocabulary_units
      where tenant_id = $1 and is_active = true
      order by sort_order asc, created_at asc
      limit 1
      `,
      [tenant.id],
    );

    const handbookSubject = await one(
      pool,
      `
      select id, name
      from public.handbook_subjects
      where tenant_id = $1 and is_active = true
      order by sort_order asc, created_at asc
      limit 1
      `,
      [tenant.id],
    );

    const handbookChapter = handbookSubject
      ? await one(
          pool,
          `
          select id, name
          from public.handbook_chapters
          where tenant_id = $1 and subject_id = $2 and is_active = true
          order by sort_order asc, created_at asc
          limit 1
          `,
          [tenant.id, handbookSubject.id],
        )
      : null;

    const hasActiveEntitlement = user
      ? await one(
          pool,
          `
          select id
          from public.entitlements
          where tenant_id = $1
            and user_id = $2
            and entitlement_type = 'svip'
            and status = 'active'
            and (expires_at is null or expires_at > now())
          limit 1
          `,
          [tenant.id, user.id],
        )
      : null;
    let benchmarkEntitlementCreated = false;
    if (INCLUDE_WRITES && ENSURE_SVIP_FOR_WRITES && user && !hasActiveEntitlement) {
      await pool.query(
        `
        insert into public.entitlements (
          tenant_id, user_id, entitlement_type, scope_type, source_type,
          starts_at, expires_at, status, metadata
        )
        values ($1, $2, 'svip', 'tenant', 'performance_benchmark',
                now(), now() + interval '1 day', 'active',
                '{"createdBy":"api-performance-benchmark"}'::jsonb)
        `,
        [tenant.id, user.id],
      );
      benchmarkEntitlementCreated = true;
    }

    const dbStats = {
      before: await captureDbStats(pool),
      tableCounts: await many(
        pool,
        `
        select label, count_value::bigint::text as value
        from (
          select 'questions' as label, count(*) as count_value from public.questions where tenant_id = $1
          union all select 'content_entries', count(*) from public.content_entries where tenant_id = $1
          union all select 'content_nodes', count(*) from public.content_nodes where tenant_id = $1
          union all select 'question_collections', count(*) from public.question_collections where tenant_id = $1
          union all select 'practice_blueprints', count(*) from public.practice_blueprints where tenant_id = $1
          union all select 'vocabulary_words', count(*) from public.vocabulary_words where tenant_id = $1
          union all select 'handbook_entries', count(*) from public.handbook_entries where tenant_id = $1
        ) counts
        order by label asc
        `,
        [tenant.id],
      ),
    };

    return {
      tenant,
      user: {
        ...user,
        entitlement: {
          hadActiveSvip: Boolean(hasActiveEntitlement),
          benchmarkEntitlementCreated,
        },
      },
      entry,
      node,
      collection,
      blueprint,
      vocabularyUnit,
      handbookSubject,
      handbookChapter,
      dbStats,
    };
  } finally {
    await pool.end();
  }
}

async function captureDbStats(pool) {
  const database = await one(
    pool,
    `
    select datname, xact_commit::bigint::text as "xactCommit",
           xact_rollback::bigint::text as "xactRollback",
           blks_read::bigint::text as "blocksRead",
           blks_hit::bigint::text as "blocksHit",
           tup_returned::bigint::text as "tuplesReturned",
           tup_fetched::bigint::text as "tuplesFetched",
           tup_inserted::bigint::text as "tuplesInserted",
           tup_updated::bigint::text as "tuplesUpdated",
           tup_deleted::bigint::text as "tuplesDeleted",
           deadlocks::bigint::text as deadlocks
    from pg_stat_database
    where datname = current_database()
    `,
  );
  const activity = await many(
    pool,
    `
    select state, wait_event_type as "waitEventType", wait_event as "waitEvent", count(*)::int
    from pg_stat_activity
    where datname = current_database()
    group by state, wait_event_type, wait_event
    order by count desc
    limit 20
    `,
  );
  return { database, activity };
}

function createEndpointCatalog(context) {
  const endpoints = [
    { name: 'health', weight: 6, method: 'GET', path: '/health', tenantHeader: false, userHeader: false },
    { name: 'tenant.resolve', weight: 6, method: 'GET', path: '/api/tenant/resolve', query: { tenantCode: context.tenant.slug }, tenantHeader: false, userHeader: false },
    { name: 'catalog.regions', weight: 4, method: 'GET', path: '/api/catalog/regions' },
    { name: 'catalog.content_entries', weight: 10, method: 'GET', path: '/api/catalog/content-entries', query: { entryType: 'question_practice' } },
    { name: 'learning.stats', weight: 5, method: 'GET', path: '/api/learning/stats' },
    { name: 'learning.trend', weight: 4, method: 'GET', path: '/api/learning/trend', query: { days: 14 } },
  ];
  if (INCLUDE_LEADERBOARD) {
    endpoints.push({
      name: 'leaderboard.questions_7d',
      weight: 3,
      method: 'GET',
      path: '/api/learning/leaderboard',
      query: { metric: 'questions', period: '7d', limit: 20 },
    });
  }

  if (context.entry) {
    endpoints.push(
      { name: 'catalog.content_nodes.flat', weight: 10, method: 'GET', path: '/api/catalog/content-nodes', query: { entryId: context.entry.id, mode: 'flat' } },
      { name: 'catalog.question_collections.entry', weight: 8, method: 'GET', path: '/api/catalog/question-collections', query: { entryId: context.entry.id, limit: 50 } },
    );
  }
  if (context.node) {
    endpoints.push(
      { name: 'catalog.question_collections.node', weight: 5, method: 'GET', path: '/api/catalog/question-collections', query: { nodeId: context.node.id, limit: 50 } },
    );
  }
  if (context.collection) {
    endpoints.push(
      { name: 'catalog.collection_questions', weight: 12, method: 'GET', path: '/api/catalog/question-collections/questions', query: { collectionId: context.collection.id, limit: QUESTION_LIMIT } },
      { name: 'catalog.practice_blueprints', weight: 7, method: 'GET', path: '/api/catalog/practice-blueprints', query: { collectionId: context.collection.id, limit: 20 } },
    );
  }
  if (context.vocabularyUnit) {
    endpoints.push(
      { name: 'catalog.vocabulary_words', weight: 5, method: 'GET', path: '/api/catalog/vocabulary-words', query: { unitId: context.vocabularyUnit.id, limit: 80 } },
      { name: 'learning.vocabulary.stats', weight: 3, method: 'GET', path: '/api/learning/vocabulary/stats' },
    );
  }
  if (context.handbookSubject) {
    endpoints.push(
      { name: 'catalog.handbook_chapters', weight: 4, method: 'GET', path: '/api/catalog/handbook-chapters', query: { subjectId: context.handbookSubject.id } },
    );
  }
  if (context.handbookChapter) {
    endpoints.push(
      { name: 'catalog.handbook_entries', weight: 5, method: 'GET', path: '/api/catalog/handbook-entries', query: { chapterId: context.handbookChapter.id, includeContent: 'true' } },
    );
  }
  return expandWeightedEndpoints(endpoints);
}

function benchmarkEndpointNames(weightedEndpoints, context) {
  const names = new Set(weightedEndpoints.map(endpoint => endpoint.name));
  if (INCLUDE_WRITES && context.blueprint && context.collection && PRACTICE_FLOW_RATIO > 0) {
    for (const name of [
      'learning.practice_flow.create',
      'learning.practice_flow.detail',
      'learning.practice_flow.answer',
      'learning.practice_flow.submit',
      'learning.practice_flow.report',
    ]) names.add(name);
  }
  return [...names].sort();
}

function expandWeightedEndpoints(endpoints) {
  const weighted = [];
  for (const endpoint of endpoints) {
    for (let i = 0; i < endpoint.weight; i += 1) weighted.push(endpoint);
  }
  if (!weighted.length) throw new Error('No benchmark endpoints could be built from discovered data.');
  return weighted;
}

function pickEndpoint(weightedEndpoints) {
  return weightedEndpoints[Math.floor(Math.random() * weightedEndpoints.length)];
}

function percentile(sortedValues, p) {
  if (!sortedValues.length) return 0;
  const index = Math.ceil((p / 100) * sortedValues.length) - 1;
  return sortedValues[Math.min(Math.max(index, 0), sortedValues.length - 1)];
}

function summarizeSamples(samples) {
  if (!samples.length) {
    return {
      requests: 0,
      ok: 0,
      errors: 0,
      errorRate: 0,
      throughputRps: 0,
      okThroughputRps: 0,
      latencyAll: { p50Ms: 0, p90Ms: 0, p95Ms: 0, p99Ms: 0, maxMs: 0 },
      latencyOk: { p50Ms: 0, p90Ms: 0, p95Ms: 0, p99Ms: 0, maxMs: 0 },
      endpoints: [],
    };
  }

  const okSamples = samples.filter(sample => sample.ok);
  const allLatencies = samples.map(sample => sample.elapsedMs).sort((a, b) => a - b);
  const okLatencies = okSamples.map(sample => sample.elapsedMs).sort((a, b) => a - b);
  const byEndpoint = new Map();
  for (const sample of samples) {
    const bucket = byEndpoint.get(sample.name) || [];
    bucket.push(sample);
    byEndpoint.set(sample.name, bucket);
  }

  const endpointSummaries = [...byEndpoint.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([name, endpointSamples]) => {
      const ok = endpointSamples.filter(sample => sample.ok);
      const latencies = ok.map(sample => sample.elapsedMs).sort((a, b) => a - b);
      return {
        name,
        requests: endpointSamples.length,
        ok: ok.length,
        errors: endpointSamples.length - ok.length,
        p50Ms: round(percentile(latencies, 50)),
        p90Ms: round(percentile(latencies, 90)),
        p95Ms: round(percentile(latencies, 95)),
        p99Ms: round(percentile(latencies, 99)),
        maxMs: round(latencies.at(-1) || 0),
      };
    });

  const durationMs = Math.max(...samples.map(sample => sample.finishedAt), 0) - Math.min(...samples.map(sample => sample.startedAt), 0);
  const durationSeconds = durationMs > 0 ? durationMs / 1000 : DURATION_SECONDS;

  return {
    requests: samples.length,
    ok: okSamples.length,
    errors: samples.length - okSamples.length,
    errorRate: samples.length ? round((samples.length - okSamples.length) / samples.length) : 0,
    throughputRps: round(samples.length / durationSeconds),
    okThroughputRps: round(okSamples.length / durationSeconds),
    latencyAll: {
      p50Ms: round(percentile(allLatencies, 50)),
      p90Ms: round(percentile(allLatencies, 90)),
      p95Ms: round(percentile(allLatencies, 95)),
      p99Ms: round(percentile(allLatencies, 99)),
      maxMs: round(allLatencies.at(-1) || 0),
    },
    latencyOk: {
      p50Ms: round(percentile(okLatencies, 50)),
      p90Ms: round(percentile(okLatencies, 90)),
      p95Ms: round(percentile(okLatencies, 95)),
      p99Ms: round(percentile(okLatencies, 99)),
      maxMs: round(okLatencies.at(-1) || 0),
    },
    endpoints: endpointSummaries,
  };
}

function round(value, digits = 2) {
  return Number(Number(value || 0).toFixed(digits));
}

async function workerLoop(workerId, weightedEndpoints, context, stopAt, samples, errors) {
  const rampDelay = RAMP_SECONDS > 0 ? (workerId / Math.max(CONCURRENCY, 1)) * RAMP_SECONDS * 1000 : 0;
  if (rampDelay > 0) await sleep(rampDelay);
  while (performance.now() < stopAt) {
    if (INCLUDE_WRITES && PRACTICE_FLOW_RATIO > 0 && Math.random() < PRACTICE_FLOW_RATIO) {
      await runPracticeFlow(context, samples, errors);
      continue;
    }
    const endpoint = pickEndpoint(weightedEndpoints);
    const startedAt = performance.now();
    const result = await requestJson(endpoint, context);
    const finishedAt = performance.now();
    samples.push(sampleFromResult(endpoint.name, result, startedAt, finishedAt));
    if (!result.ok) pushError(errors, endpoint.name, result);
  }
}

async function runBenchmark(context) {
  const weightedEndpoints = createEndpointCatalog(context);
  const endpointNames = benchmarkEndpointNames(weightedEndpoints, context);
  const samples = [];
  const errors = [];
  const startedAt = new Date();
  const startPerf = performance.now();
  const stopAt = startPerf + DURATION_SECONDS * 1000;
  await Promise.all(
    Array.from({ length: CONCURRENCY }, (_, index) => workerLoop(index, weightedEndpoints, context, stopAt, samples, errors)),
  );
  const finishedAt = new Date();

  const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 2 });
  let afterStats = null;
  try {
    afterStats = await captureDbStats(pool);
  } finally {
    await pool.end();
  }

  return {
    startedAt: startedAt.toISOString(),
    finishedAt: finishedAt.toISOString(),
    apiBase,
    config: {
      durationSeconds: DURATION_SECONDS,
      concurrency: CONCURRENCY,
      rampSeconds: RAMP_SECONDS,
      includeWrites: INCLUDE_WRITES,
      practiceFlowRatio: INCLUDE_WRITES ? PRACTICE_FLOW_RATIO : 0,
      practiceFlowAnswers: INCLUDE_WRITES ? PRACTICE_FLOW_ANSWERS : 0,
      questionLimit: QUESTION_LIMIT,
      timeoutMs: DEFAULT_TIMEOUT_MS,
    },
    target: {
      tenant: context.tenant,
      user: context.user,
      entry: context.entry,
      node: context.node,
      collection: context.collection,
      blueprint: context.blueprint,
      vocabularyUnit: context.vocabularyUnit,
      handbookSubject: context.handbookSubject,
      handbookChapter: context.handbookChapter,
      endpointNames,
    },
    db: {
      before: context.dbStats.before,
      after: afterStats,
      tableCounts: context.dbStats.tableCounts,
    },
    summary: summarizeSamples(samples),
    errors,
  };
}

function markdownReport(report) {
  const lines = [];
  lines.push('# API 本地压测报告');
  lines.push('');
  lines.push(`生成时间：${new Date(report.finishedAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`);
  lines.push('');
  lines.push('## 配置');
  lines.push('');
  lines.push(`- API：${report.apiBase}`);
  lines.push(`- 租户：${report.target.tenant.slug} / ${report.target.tenant.name}`);
  lines.push(`- 并发：${report.config.concurrency}`);
  lines.push(`- 持续时间：${report.config.durationSeconds}s`);
  lines.push(`- Ramp：${report.config.rampSeconds}s`);
  lines.push(`- 写入压测：${report.config.includeWrites ? '开启' : '关闭'}`);
  if (report.config.includeWrites) {
    lines.push(`- 刷题闭环比例：${Math.round((report.config.practiceFlowRatio || 0) * 100)}%`);
    lines.push(`- 每个刷题闭环提交答案数：${report.config.practiceFlowAnswers || 0}`);
  }
  lines.push(`- 请求超时：${report.config.timeoutMs}ms`);
  lines.push('');
  lines.push('## 总览');
  lines.push('');
  lines.push('| 指标 | 数值 |');
  lines.push('| --- | ---: |');
  lines.push(`| 请求数 | ${report.summary.requests} |`);
  lines.push(`| 成功数 | ${report.summary.ok} |`);
  lines.push(`| 错误数 | ${report.summary.errors} |`);
  lines.push(`| 错误率 | ${(report.summary.errorRate * 100).toFixed(2)}% |`);
  lines.push(`| 吞吐 | ${report.summary.throughputRps} req/s |`);
  lines.push(`| 成功吞吐 | ${report.summary.okThroughputRps} req/s |`);
  lines.push(`| P50 | ${report.summary.latencyOk.p50Ms} ms |`);
  lines.push(`| P90 | ${report.summary.latencyOk.p90Ms} ms |`);
  lines.push(`| P95 | ${report.summary.latencyOk.p95Ms} ms |`);
  lines.push(`| P99 | ${report.summary.latencyOk.p99Ms} ms |`);
  lines.push(`| Max | ${report.summary.latencyOk.maxMs} ms |`);
  lines.push('');
  lines.push('## 接口明细');
  lines.push('');
  lines.push('| 接口 | 请求 | 成功 | 错误 | P50 ms | P90 ms | P95 ms | P99 ms | Max ms |');
  lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |');
  for (const item of report.summary.endpoints) {
    lines.push(`| ${item.name} | ${item.requests} | ${item.ok} | ${item.errors} | ${item.p50Ms} | ${item.p90Ms} | ${item.p95Ms} | ${item.p99Ms} | ${item.maxMs} |`);
  }
  lines.push('');
  lines.push('## 数据规模');
  lines.push('');
  lines.push('| 表 | 当前租户记录数 |');
  lines.push('| --- | ---: |');
  for (const item of report.db.tableCounts) {
    lines.push(`| ${item.label} | ${item.value} |`);
  }
  lines.push('');
  if (report.errors.length) {
    lines.push('## 错误样本');
    lines.push('');
    lines.push('| 接口 | 状态 | 错误 | 耗时 ms |');
    lines.push('| --- | ---: | --- | ---: |');
    for (const error of report.errors) {
      const errorText = [error.error, error.message && error.message !== error.error ? error.message : ''].filter(Boolean).join(': ');
      lines.push(`| ${error.endpoint} | ${error.status} | ${String(errorText).replaceAll('|', '\\|')} | ${error.elapsedMs} |`);
    }
    lines.push('');
  }
  lines.push('## 说明');
  lines.push('');
  lines.push('- 默认压测是只读混合工作负载，适合在真实迁移库上做烟测；开启写入后会执行“创建练习 session -> 读取详情 -> 提交答案 -> 交卷 -> 读取报告”的刷题闭环。');
  lines.push('- 并发 worker 是无停顿请求流，不能直接等同于真实在线人数；真实学生有读题、思考、翻页和网络间隔，应结合前端埋点估算每人 RPS。');
  lines.push('- 结果只能代表当前本机 Docker、API 进程和数据库状态；4 核 16G 云服务器需要按 runbook 跑阶梯并发。');
  lines.push('- 若 P95 明显升高，下一步应结合 PostgreSQL 慢 SQL、`pg_stat_activity` 和 API 日志定位。');
  lines.push('');
  return `${lines.join('\n')}\n`;
}

async function writeReport(report) {
  await fs.mkdir(OUTPUT_DIR, { recursive: true });
  const stamp = shanghaiTimestampForFile(new Date(report.finishedAt));
  const jsonPath = path.join(OUTPUT_DIR, `api-benchmark-${stamp}.json`);
  const mdPath = path.join(OUTPUT_DIR, `api-benchmark-${stamp}.md`);
  await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
  await fs.writeFile(mdPath, markdownReport(report), 'utf8');
  return { jsonPath, mdPath };
}

async function main() {
  try {
    if (!apiBase && !START_SERVER) {
      throw new Error('PERF_API_BASE is required when PERF_START_SERVER=false.');
    }
    const context = await discoverBenchmarkContext();
    await startServerIfNeeded();
    if (!apiBase) throw new Error('API base URL was not resolved.');
    console.log(`[perf] target api=${apiBase} tenant=${context.tenant.slug} user=${context.user.id}`);
    console.log(`[perf] duration=${DURATION_SECONDS}s concurrency=${CONCURRENCY} includeWrites=${INCLUDE_WRITES}`);
    const report = await runBenchmark(context);
    const files = await writeReport(report);
    console.log(`[perf] requests=${report.summary.requests} ok=${report.summary.ok} errors=${report.summary.errors} rps=${report.summary.throughputRps} p95=${report.summary.latencyOk.p95Ms}ms`);
    console.log(`[perf] wrote ${files.jsonPath}`);
    console.log(`[perf] wrote ${files.mdPath}`);
    if (report.summary.errors > 0) {
      process.exitCode = 1;
    }
  } finally {
    stopServer();
  }
}

main().catch(error => {
  console.error(error);
  if (serverLogs) console.error(serverLogs);
  stopServer();
  process.exitCode = 1;
});
