#!/usr/bin/env node
import { createRequire } from 'node:module';

const DEFAULTS = {
  databaseUrl: process.env.DATABASE_URL,
  productId: 'perf-product-001',
  projectId: 'perf-project-001-001',
  versionId: 'perf-version-001-001-001',
  userId: 'perf-user-dev-01',
  search: 'REQ',
  limit: 50,
  dryRun: false,
  json: false,
  strictPlan: false,
};

const REQUIRED_INDEXES = [
  'requirements_product_project_status_created_at_idx',
  'requirements_version_status_created_at_idx',
  'version_plans_version_type_status_idx',
  'version_plans_owner_status_end_idx',
  'dev_tasks_version_status_updated_at_idx',
  'dev_tasks_assignee_unfinished_idx',
  'test_cases_version_round_status_updated_at_idx',
  'test_cases_version_round_status_updated_at_desc_idx',
  'test_cases_assignee_unfinished_idx',
  'bugs_version_status_severity_updated_at_idx',
  'bugs_assignee_open_idx',
  'xiaobao_risk_summaries_warning_score_idx',
  'xiaobao_risk_summaries_dirty_updated_at_idx',
  'projects_product_created_at_idx',
  'versions_product_created_at_idx',
  'versions_product_project_created_at_idx',
  'work_activities_version_occurred_at_idx',
  'task_worklogs_version_date_idx',
  'ai_logs_operation_created_at_idx',
  'ai_logs_status_created_at_idx',
];

function parseArgs(argv) {
  const args = { ...DEFAULTS };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === '--dry-run') {
      args.dryRun = true;
      continue;
    }
    if (arg === '--json') {
      args.json = true;
      continue;
    }
    if (arg === '--strict-plan') {
      args.strictPlan = true;
      continue;
    }
    if (arg === '--database-url') {
      args.databaseUrl = argv[++index] ?? args.databaseUrl;
      continue;
    }
    if (arg.startsWith('--database-url=')) {
      args.databaseUrl = arg.slice('--database-url='.length);
      continue;
    }
    if (arg === '--product-id') {
      args.productId = argv[++index] ?? args.productId;
      continue;
    }
    if (arg === '--project-id') {
      args.projectId = argv[++index] ?? args.projectId;
      continue;
    }
    if (arg === '--version-id') {
      args.versionId = argv[++index] ?? args.versionId;
      continue;
    }
    if (arg === '--user-id') {
      args.userId = argv[++index] ?? args.userId;
      continue;
    }
    if (arg === '--search') {
      args.search = argv[++index] ?? args.search;
      continue;
    }
    if (arg === '--limit') {
      args.limit = parsePositiveInt(argv[++index], args.limit);
      continue;
    }
    if (arg === '--help' || arg === '-h') {
      printHelp();
      process.exit(0);
    }
    throw new Error(`Unknown argument: ${arg}`);
  }
  return args;
}

function printHelp() {
  console.log(`Usage: node scripts/explain-hot-queries.mjs [--dry-run] [--database-url URL]

Audits V2.6 hot query budgets and indexes.

Options:
  --dry-run       Print hot query contracts without opening a database connection.
  --json          Print machine-readable JSON.
  --strict-plan   Fail when a SQL plan contains a sequential scan on a hot table.
  --database-url  PostgreSQL URL. Defaults to DATABASE_URL.
  --product-id    Fixture product id. Defaults to ${DEFAULTS.productId}
  --project-id    Fixture project id. Defaults to ${DEFAULTS.projectId}
  --version-id    Fixture version id. Defaults to ${DEFAULTS.versionId}
  --user-id       Fixture assignee/owner id. Defaults to ${DEFAULTS.userId}
  --search        Requirement search term. Defaults to ${DEFAULTS.search}
  --limit         Requirement page limit. Defaults to ${DEFAULTS.limit}
`);
}

function parsePositiveInt(raw, fallback) {
  const parsed = Number(raw);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.max(1, Math.floor(parsed));
}

function buildExplainTargets(args) {
  const search = `%${args.search}%`;
  const limit = args.limit + 1;

  return [
    {
      key: 'healthVersion',
      label: 'health/version',
      budget: 'HTTP p95 <= 500ms; no database query',
      partitionKey: 'none',
      expectedIndexes: [],
      sql: null,
    },
    {
      key: 'requirementPool',
      label: 'V2.2 requirement pool search',
      budget: 'HTTP p95 <= 1000ms; SQL should prune by product_id',
      partitionKey: 'requirements.product_id',
      expectedIndexes: [
        'requirements_product_project_status_created_at_idx',
        'requirements_product_id_code_key',
      ],
      sql: `
SELECT r.*
FROM requirements r
LEFT JOIN users u ON u.id = r.creator_id
WHERE r.product_id = $1
  AND (r.code ILIKE $2 OR r.title ILIKE $2)
ORDER BY r.created_at DESC
LIMIT $3`,
      params: [args.productId, search, limit],
    },
    {
      key: 'versionDetail.version',
      label: 'V2.2 version detail root',
      budget: 'point lookup by versions.id',
      partitionKey: 'versions.id',
      expectedIndexes: ['versions_pkey'],
      sql: 'SELECT * FROM versions WHERE id = $1',
      params: [args.versionId],
    },
    {
      key: 'versionDetail.requirements',
      label: 'V2.2 version detail requirements',
      budget: 'child rows constrained by version_id',
      partitionKey: 'requirements.version_id',
      expectedIndexes: ['requirements_version_status_created_at_idx'],
      sql: `
SELECT r.*
FROM requirements r
LEFT JOIN users u ON u.id = r.creator_id
WHERE r.version_id = $1
ORDER BY r.created_at DESC`,
      params: [args.versionId],
    },
    {
      key: 'versionDetail.plans',
      label: 'V2.2 version detail plans',
      budget: 'child rows constrained by version_id',
      partitionKey: 'version_plans.version_id',
      expectedIndexes: ['version_plans_version_type_status_idx'],
      sql: `
SELECT *
FROM version_plans
WHERE version_id = $1
ORDER BY type ASC, created_at DESC`,
      params: [args.versionId],
    },
    {
      key: 'versionDetail.devTasks',
      label: 'V2.2 version detail dev tasks',
      budget: 'partition prune by version_id',
      partitionKey: 'dev_tasks.version_id',
      expectedIndexes: ['dev_tasks_version_status_updated_at_idx'],
      sql: `
SELECT *
FROM dev_tasks
WHERE version_id = $1
ORDER BY status ASC, updated_at DESC`,
      params: [args.versionId],
    },
    {
      key: 'versionDetail.testCases',
      label: 'V2.2 version detail test cases',
      budget: 'partition prune by version_id',
      partitionKey: 'test_cases.version_id',
      expectedIndexes: ['test_cases_version_round_status_updated_at_desc_idx'],
      sql: `
SELECT *
FROM test_cases
WHERE version_id = $1
ORDER BY round_no DESC, status ASC, updated_at DESC`,
      params: [args.versionId],
    },
    {
      key: 'versionDetail.bugs',
      label: 'V2.2 version detail bugs',
      budget: 'partition prune by version_id',
      partitionKey: 'bugs.version_id',
      expectedIndexes: ['bugs_version_status_priority_updated_at_idx'],
      sql: `
SELECT *
FROM bugs
WHERE version_id = $1
ORDER BY status ASC, priority ASC, updated_at DESC`,
      params: [args.versionId],
    },
    {
      key: 'workspace.plans',
      label: 'V2.2 workspace plans',
      budget: 'owner unfinished lookup',
      partitionKey: 'version_plans.owner_id',
      expectedIndexes: ['version_plans_owner_open_due_idx'],
      sql: `
SELECT *
FROM version_plans
WHERE owner_id = $1
  AND status <> 'completed'
ORDER BY expected_end_at ASC, updated_at DESC`,
      params: [args.userId],
    },
    {
      key: 'workspace.devTasks',
      label: 'V2.2 workspace dev tasks',
      budget: 'assignee unfinished lookup',
      partitionKey: 'dev_tasks.assignee_id',
      expectedIndexes: ['dev_tasks_assignee_open_priority_idx'],
      sql: `
SELECT *
FROM dev_tasks
WHERE assignee_id = $1
  AND status <> 'submitted'
ORDER BY priority ASC, updated_at DESC`,
      params: [args.userId],
    },
    {
      key: 'workspace.testCases',
      label: 'V2.2 workspace test cases',
      budget: 'assignee unfinished lookup',
      partitionKey: 'test_cases.assignee_id',
      expectedIndexes: ['test_cases_assignee_open_priority_idx'],
      sql: `
SELECT *
FROM test_cases
WHERE assignee_id = $1
  AND status NOT IN ('passed', 'failed', 'blocked')
ORDER BY priority ASC, updated_at DESC`,
      params: [args.userId],
    },
    {
      key: 'workspace.bugs',
      label: 'V2.2 workspace bugs',
      budget: 'assignee open lookup',
      partitionKey: 'bugs.assignee_id',
      expectedIndexes: ['bugs_assignee_open_priority_idx'],
      sql: `
SELECT *
FROM bugs
WHERE assignee_id = $1
  AND status IN ('open', 'fixing', 'fixed', 'verifying')
ORDER BY priority ASC, updated_at DESC`,
      params: [args.userId],
    },
    {
      key: 'xiaobao.managerWarnings',
      label: 'V2.2 Xiaobao manager warning list',
      budget: 'HTTP p95 <= 1000ms; warning rows sorted by score',
      partitionKey: 'xiaobao_risk_summaries.version_id',
      expectedIndexes: ['xiaobao_risk_summaries_warning_score_idx'],
      sql: `
SELECT *
FROM xiaobao_risk_summaries
WHERE risk_level <> 'on_track'
ORDER BY risk_score DESC, updated_at DESC`,
      params: [],
    },
    {
      key: 'xiaobao.dirtyQueue',
      label: 'V2.6 Xiaobao dirty summary queue',
      budget: 'background worker batch scan',
      partitionKey: 'xiaobao_risk_summaries.version_id',
      expectedIndexes: ['xiaobao_risk_summaries_dirty_updated_at_idx'],
      sql: `
SELECT version_id
FROM xiaobao_risk_summaries
WHERE dirty = true
ORDER BY updated_at ASC
LIMIT 100`,
      params: [],
    },
    {
      key: 'domain.projects',
      label: 'Project list by product',
      budget: 'domain list keeps product_id prefix',
      partitionKey: 'projects.product_id',
      expectedIndexes: ['projects_product_created_at_idx'],
      sql: `
SELECT *
FROM projects
WHERE product_id = $1
ORDER BY created_at DESC`,
      params: [args.productId],
    },
    {
      key: 'domain.versionsByProduct',
      label: 'Version list by product',
      budget: 'domain list keeps product_id prefix',
      partitionKey: 'versions.product_id',
      expectedIndexes: ['versions_product_created_at_idx'],
      sql: `
SELECT *
FROM versions
WHERE product_id = $1
ORDER BY created_at DESC`,
      params: [args.productId],
    },
    {
      key: 'domain.versionsByProject',
      label: 'Version list by product/project',
      budget: 'domain list keeps product_id + project_id prefix',
      partitionKey: 'versions.product_id, versions.project_id',
      expectedIndexes: ['versions_product_project_created_at_idx'],
      sql: `
SELECT *
FROM versions
WHERE product_id = $1
  AND project_id = $2
ORDER BY created_at DESC`,
      params: [args.productId, args.projectId],
    },
    {
      key: 'xiaobao.versionActivities',
      label: 'Xiaobao evidence activity scan',
      budget: 'background summary recompute by version',
      partitionKey: 'work_activities.version_id',
      expectedIndexes: ['work_activities_version_occurred_at_idx'],
      sql: `
SELECT *
FROM work_activities
WHERE version_id = $1
ORDER BY occurred_at DESC
LIMIT 200`,
      params: [args.versionId],
    },
    {
      key: 'audit.searchAdapter',
      label: 'Audit search adapter contract',
      budget: 'V2.5 audit module not landed; AiLog audit uses operation/status created_at indexes',
      partitionKey: 'ai_logs.created_at',
      expectedIndexes: ['ai_logs_operation_created_at_idx', 'ai_logs_status_created_at_idx'],
      sql: `
SELECT *
FROM ai_logs
WHERE operation = $1
ORDER BY created_at DESC
LIMIT 100`,
      params: ['risk-interpret'],
      adapter: 'Replace with audit_events(actor_id, resource_type, created_at) once V2.5 audit contract lands.',
    },
  ];
}

function printDryRun(targets) {
  console.log('Hot query explain dry-run');
  console.log('key\tpartition_key\texpected_indexes\tbudget');
  for (const target of targets) {
    console.log([
      target.key,
      target.partitionKey,
      target.expectedIndexes.join(',') || '-',
      target.budget,
    ].join('\t'));
  }
}

async function loadPrismaClient(databaseUrl) {
  if (databaseUrl) process.env.DATABASE_URL = databaseUrl;
  const serverRequire = createRequire(new URL('../apps/server/package.json', import.meta.url));
  const { PrismaClient } = serverRequire('@prisma/client');
  return new PrismaClient();
}

async function auditIndexes(prisma, indexNames) {
  const uniqueNames = Array.from(new Set(indexNames));
  const valuesSql = uniqueNames.map((_, index) => `($${index + 1})`).join(', ');
  const rows = await prisma.$queryRawUnsafe(
    `
WITH wanted(index_name) AS (
  VALUES ${valuesSql}
)
SELECT wanted.index_name, to_regclass('public.' || quote_ident(wanted.index_name)) IS NOT NULL AS present
FROM wanted
ORDER BY wanted.index_name
`,
    ...uniqueNames,
  );
  return rows.map((row) => ({
    indexName: row.index_name,
    present: Boolean(row.present),
  }));
}

async function explainTarget(prisma, target) {
  if (!target.sql) {
    return {
      key: target.key,
      label: target.label,
      skipped: true,
      reason: 'no SQL query',
    };
  }

  const rows = await prisma.$queryRawUnsafe(
    `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${target.sql}`,
    ...(target.params ?? []),
  );
  const plan = readExplainPlan(rows);
  const summary = summarizePlan(plan);
  return {
    key: target.key,
    label: target.label,
    skipped: false,
    summary,
    seqScans: collectSeqScans(plan),
  };
}

function readExplainPlan(rows) {
  const first = rows?.[0];
  if (!first) return null;
  const raw = first['QUERY PLAN'] ?? first['QUERY PLAN'.toLowerCase()] ?? first.query_plan;
  if (Array.isArray(raw)) return raw[0];
  if (typeof raw === 'string') return JSON.parse(raw)[0];
  return raw?.[0] ?? raw;
}

function summarizePlan(plan) {
  const root = plan?.Plan ?? plan;
  return {
    nodeType: root?.['Node Type'] ?? 'unknown',
    totalCost: Number(root?.['Total Cost'] ?? 0),
    actualTotalTimeMs: Number(root?.['Actual Total Time'] ?? 0),
    actualRows: Number(root?.['Actual Rows'] ?? 0),
    sharedHitBlocks: Number(root?.['Shared Hit Blocks'] ?? 0),
    sharedReadBlocks: Number(root?.['Shared Read Blocks'] ?? 0),
  };
}

function collectSeqScans(plan) {
  const found = [];
  visitPlan(plan?.Plan ?? plan, (node) => {
    if (node?.['Node Type'] === 'Seq Scan') {
      found.push({
        relation: node['Relation Name'] ?? 'unknown',
        alias: node.Alias ?? '',
        filter: node.Filter ?? '',
      });
    }
  });
  return found;
}

function visitPlan(node, callback) {
  if (!node) return;
  callback(node);
  for (const child of node.Plans ?? []) visitPlan(child, callback);
}

function printResults(indexAudit, explainResults, strictPlan) {
  console.log('Index audit');
  for (const item of indexAudit) {
    console.log(`${item.present ? 'ok' : 'missing'}\t${item.indexName}`);
  }

  console.log('\nExplain plans');
  console.log('key\tnode\tactual_ms\tactual_rows\tshared_read_blocks\tseq_scan');
  for (const result of explainResults) {
    if (result.skipped) {
      console.log(`${result.key}\tskipped\t-\t-\t-\t${result.reason}`);
      continue;
    }
    const seqScan = result.seqScans.map((scan) => scan.relation).join(',') || '-';
    console.log([
      result.key,
      result.summary.nodeType,
      result.summary.actualTotalTimeMs.toFixed(3),
      result.summary.actualRows,
      result.summary.sharedReadBlocks,
      seqScan,
    ].join('\t'));
  }

  if (strictPlan) {
    console.log('\nstrict-plan enabled: sequential scans on SQL targets are treated as failures.');
  }
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const targets = buildExplainTargets(args);
  const expectedIndexes = [
    ...REQUIRED_INDEXES,
    ...targets.flatMap((target) => target.expectedIndexes),
  ];

  if (args.dryRun) {
    const payload = {
      targets: targets.map(({ sql, params, ...target }) => ({
        ...target,
        sql: sql?.trim() ?? null,
        params,
      })),
      requiredIndexes: Array.from(new Set(expectedIndexes)).sort(),
    };
    if (args.json) console.log(JSON.stringify(payload, null, 2));
    else printDryRun(targets);
    return;
  }

  if (!args.databaseUrl) {
    throw new Error('DATABASE_URL is required unless --dry-run is used');
  }

  const prisma = await loadPrismaClient(args.databaseUrl);
  try {
    const indexAudit = await auditIndexes(prisma, expectedIndexes);
    const explainResults = [];
    for (const target of targets) {
      explainResults.push(await explainTarget(prisma, target));
    }

    if (args.json) {
      console.log(JSON.stringify({ indexAudit, explainResults }, null, 2));
    } else {
      printResults(indexAudit, explainResults, args.strictPlan);
    }

    const hasMissingIndexes = indexAudit.some((item) => !item.present);
    const hasSeqScans = explainResults.some((result) => !result.skipped && result.seqScans.length > 0);
    if (hasMissingIndexes || (args.strictPlan && hasSeqScans)) {
      process.exitCode = 1;
    }
  } finally {
    await prisma.$disconnect();
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  process.exitCode = 1;
});
