#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
OUT_DIR="${1:-$ROOT/work_store/apifox/$(date +%F)-smart-canteen}"
PROJECT_ID="5737553"

mkdir -p "$OUT_DIR/raw" "$OUT_DIR/derived" "$OUT_DIR/reports"

apifox project view "$PROJECT_ID" > "$OUT_DIR/raw/project-view.json"
apifox environment list --project "$PROJECT_ID" > "$OUT_DIR/raw/environments.json"
apifox test-scenario list --project "$PROJECT_ID" > "$OUT_DIR/raw/test-scenarios.json"
apifox test-suite list --project "$PROJECT_ID" > "$OUT_DIR/raw/test-suites.json"

jq -r '.data[].id' "$OUT_DIR/raw/environments.json" | while read -r env_id; do
  apifox environment view "$env_id" --project "$PROJECT_ID" > "$OUT_DIR/raw/environment-${env_id}-view.json"
done

OUT_DIR="$OUT_DIR" node <<'NODE'
const fs = require('fs');
const https = require('https');
const path = require('path');
const outDir = process.env.OUT_DIR;
const configPath = path.join(process.env.HOME, '.apifox', 'config.toml');
const config = fs.readFileSync(configPath, 'utf8');
const match = config.match(/access_token\s*=\s*"([^"]+)"/);
if (!match) throw new Error('No access_token in ~/.apifox/config.toml');
const token = match[1];
const body = JSON.stringify({
  scope: { type: 'ALL' },
  options: { includeApifoxExtensionProperties: true, addFoldersToTags: true },
  oasVersion: '3.1',
  exportFormat: 'JSON'
});
const target = path.join(outDir, 'raw', 'openapi.json');
const req = https.request({
  method: 'POST',
  hostname: 'api.apifox.com',
  path: '/v1/projects/5737553/export-openapi?locale=zh-CN',
  headers: {
    'X-Apifox-Api-Version': '2024-03-28',
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body)
  }
}, (res) => {
  const chunks = [];
  res.on('data', (chunk) => chunks.push(chunk));
  res.on('end', () => {
    const buf = Buffer.concat(chunks);
    if (res.statusCode < 200 || res.statusCode >= 300) {
      console.error(`export-openapi failed: HTTP ${res.statusCode}`);
      process.exit(1);
    }
    fs.writeFileSync(target, buf);
  });
});
req.on('error', (err) => {
  console.error(err.message);
  process.exit(1);
});
req.write(body);
req.end();
NODE

OUT_DIR="$OUT_DIR" node <<'NODE'
const fs = require('fs');
const path = require('path');
const root = process.env.OUT_DIR;
const raw = path.join(root, 'raw');
const derived = path.join(root, 'derived');
const read = (name) => JSON.parse(fs.readFileSync(path.join(raw, name), 'utf8'));
const oas = read('openapi.json');
const scenarios = read('test-scenarios.json').data || [];
const suites = read('test-suites.json').data || [];
const envs = read('environments.json').data || [];
const esc = (v) => String(v ?? '').replace(/[\t\r\n]+/g, ' ').trim();
const methods = new Set(['get','post','put','delete','patch','head','options','trace']);
const ops = [];
for (const [apiPath, item] of Object.entries(oas.paths || {})) {
  for (const [method, op] of Object.entries(item || {})) {
    if (!methods.has(method)) continue;
    const folder = op['x-apifox-folder'] || (op.tags || []).at(-1) || '';
    const reqTypes = Object.keys(op.requestBody?.content || {});
    const resCodes = Object.keys(op.responses || {});
    const params = op.parameters || [];
    ops.push({
      key: `${method.toUpperCase()} ${apiPath}`,
      method: method.toUpperCase(),
      path: apiPath,
      summary: op.summary || '',
      folder,
      topModule: String(folder).split('/')[0] || '',
      status: op['x-apifox-status'] || '',
      operationId: op.operationId || '',
      requestTypes: reqTypes.join(','),
      responseCodes: resCodes.join(','),
      requiredParams: params.filter(p => p.required).map(p => `${p.in}:${p.name}`).join(','),
      hasBody: reqTypes.length ? 'Y' : 'N',
      runInApifox: op['x-run-in-apifox'] || ''
    });
  }
}
ops.sort((a,b) => a.folder.localeCompare(b.folder, 'zh-Hans-CN') || a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
fs.writeFileSync(path.join(derived, 'api-index.tsv'), ['key\tmethod\tpath\tsummary\tfolder\ttop_module\tstatus\toperation_id\trequest_types\tresponse_codes\trequired_params\thas_body\trun_in_apifox', ...ops.map(o => [o.key,o.method,o.path,o.summary,o.folder,o.topModule,o.status,o.operationId,o.requestTypes,o.responseCodes,o.requiredParams,o.hasBody,o.runInApifox].map(esc).join('\t'))].join('\n') + '\n');
const byFolder = new Map();
for (const o of ops) {
  const row = byFolder.get(o.folder) || {folder:o.folder,total:0,methods:{},statuses:{},withBody:0,withRequired:0};
  row.total++;
  row.methods[o.method] = (row.methods[o.method] || 0) + 1;
  row.statuses[o.status || '(blank)'] = (row.statuses[o.status || '(blank)'] || 0) + 1;
  if (o.hasBody === 'Y') row.withBody++;
  if (o.requiredParams) row.withRequired++;
  byFolder.set(o.folder, row);
}
const moduleRows = [...byFolder.values()].sort((a,b) => b.total - a.total || a.folder.localeCompare(b.folder, 'zh-Hans-CN'));
fs.writeFileSync(path.join(derived, 'module-coverage.tsv'), ['folder\tapi_count\tmethods\tstatuses\twith_body\twith_required_params', ...moduleRows.map(r => [r.folder,r.total,Object.entries(r.methods).map(([k,v])=>`${k}:${v}`).join(','),Object.entries(r.statuses).map(([k,v])=>`${k}:${v}`).join(','),r.withBody,r.withRequired].map(esc).join('\t'))].join('\n') + '\n');
fs.writeFileSync(path.join(derived, 'test-scenarios.tsv'), ['scenario_id\tname\tfolder_id\tfolder_name\tdescription', ...scenarios.map(s => [s.id,s.name,s.folderId,s.folderName,s.description].map(esc).join('\t'))].join('\n') + '\n');
fs.writeFileSync(path.join(derived, 'test-suites.tsv'), ['suite_id\tname\tdescription', ...suites.map(s => [s.id,s.name,s.description].map(esc).join('\t'))].join('\n') + '\n');
const scenarioKeywords = scenarios.map(s => ({
  id: s.id,
  name: s.name,
  tokens: String(s.name).split(/[\s,，、：:；;\/]+/).filter(t => t.length >= 2)
}));
const coverageRows = ops.map(o => {
  const hay = `${o.folder} ${o.summary} ${o.path}`;
  const matches = scenarioKeywords
    .filter(s => s.tokens.some(t => hay.includes(t)))
    .map(s => `${s.id}:${s.name}`);
  return [
    o.key,
    o.folder,
    o.summary,
    matches.length ? matches.join(' | ') : '',
    matches.length ? 'keyword-match' : 'no-current-scenario-name-match'
  ];
});
fs.writeFileSync(path.join(derived, 'interface-test-name-coverage.tsv'), ['api_key\tfolder\tsummary\tmatched_scenarios\tmatch_basis', ...coverageRows.map(r => r.map(esc).join('\t'))].join('\n') + '\n');
const envRows = envs.map(e => {
  const f = path.join(raw, `environment-${e.id}-view.json`);
  const view = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')).data : null;
  const baseUrls = view?.baseUrls ? Object.entries(view.baseUrls).map(([k,v]) => `${k}=${v}`).join('; ') : '';
  const paramCounts = view?.parameters ? Object.entries(view.parameters).map(([k,v]) => `${k}:${Array.isArray(v) ? v.length : Object.keys(v || {}).length}`).join(',') : '';
  return [e.id,e.name,baseUrls,paramCounts];
});
fs.writeFileSync(path.join(derived, 'environments.tsv'), ['environment_id\tname\tbase_urls\tparameter_counts', ...envRows.map(r => r.map(esc).join('\t'))].join('\n') + '\n');
fs.writeFileSync(path.join(derived, 'snapshot-summary.json'), JSON.stringify({
  generatedAt: new Date().toISOString(),
  projectId: 5737553,
  projectName: '智慧营养健康餐厅',
  paths: Object.keys(oas.paths || {}).length,
  operations: ops.length,
  tags: (oas.tags || []).length,
  schemas: Object.keys(oas.components?.schemas || {}).length,
  environments: envs.length,
  testScenarios: scenarios.length,
  testSuites: suites.length
}, null, 2) + '\n');
NODE

echo "$OUT_DIR"
