import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import process from 'node:process';

const repoRoot = process.cwd();
const distRoot = path.join(repoRoot, 'apps', 'taro', 'dist');

const portals = [
  {
    portal: 'student',
    dist: 'h5-student',
    landingPath: '/pages/student/home/index',
  },
  {
    portal: 'tenant-admin',
    dist: 'h5-tenant-admin',
    landingPath: '/pages/tenant-admin/workbench/index',
  },
  {
    portal: 'platform-admin',
    dist: 'h5-platform-admin',
    landingPath: '/pages/platform-admin/workbench/index',
  },
];

const allowedRuntimeConfigKeys = new Set([
  'portal',
  'apiBaseUrl',
  'supabaseUrl',
  'supabasePublishableKey',
  'tenantCode',
]);

const forbiddenRuntimeConfigKeys = [
  'SUPABASE_SERVICE_ROLE_KEY',
  'SUPABASE_SECRET_KEY',
  'DATABASE_URL',
  'ALIYUN_OSS_ACCESS_KEY_SECRET',
  'TENCENT_COS_SECRET_KEY',
  'WECHAT_PAY_PRIVATE_KEY',
  'ALIPAY_APP_PRIVATE_KEY',
  'AUTH_SESSION_SECRET',
  'PLATFORM_ADMIN_API_KEY',
];

const forbiddenValuePatterns = [
  /postgres(?:ql)?:\/\//i,
  /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
  /\bAKIA[0-9A-Z]{16}\b/,
  /\bsk_(?:live|test)_[A-Za-z0-9]{10,}\b/,
];

function contentTypeFor(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  if (ext === '.html') return 'text/html; charset=utf-8';
  if (ext === '.js') return 'application/javascript; charset=utf-8';
  if (ext === '.css') return 'text/css; charset=utf-8';
  if (ext === '.json') return 'application/json; charset=utf-8';
  if (ext === '.svg') return 'image/svg+xml';
  if (ext === '.png') return 'image/png';
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.webp') return 'image/webp';
  if (ext === '.woff2') return 'font/woff2';
  if (ext === '.woff') return 'font/woff';
  return 'application/octet-stream';
}

function readText(filePath) {
  return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
}

function normalizeSlashes(value) {
  return value.replace(/\\/g, '/');
}

function relative(filePath) {
  return normalizeSlashes(path.relative(repoRoot, filePath));
}

function jsonResponse(response, statusCode, payload) {
  response.writeHead(statusCode, {
    'content-type': 'application/json; charset=utf-8',
    'cache-control': 'no-store',
  });
  response.end(`${JSON.stringify(payload)}\n`);
}

function textResponse(response, statusCode, body, headers = {}) {
  response.writeHead(statusCode, headers);
  response.end(body);
}

async function request(input, options = {}) {
  const response = await fetch(input, {
    method: options.method || 'GET',
    headers: options.headers || {},
  });
  const text = await response.text();
  return {
    ok: response.ok,
    status: response.status,
    headers: response.headers,
    text,
  };
}

async function createMockApiServer() {
  const requests = [];
  const server = http.createServer((req, res) => {
    const url = new URL(req.url || '/', 'http://127.0.0.1');
    requests.push({
      method: req.method,
      path: url.pathname,
      query: Object.fromEntries(url.searchParams.entries()),
      host: req.headers.host || '',
    });

    if (url.pathname === '/health') {
      jsonResponse(res, 200, { ok: true });
      return;
    }

    if (url.pathname === '/api/tenant/resolve') {
      jsonResponse(res, 200, {
        item: {
          tenantId: '00000000-0000-4000-8000-000000000001',
          tenantSlug: url.searchParams.get('tenantCode') || 'master',
          branding: {
            name: '工学题库',
            theme: {
              primaryColor: '#2563eb',
              accentColor: '#16a34a',
            },
          },
          features: {
            enableLeaderboard: false,
          },
          adminFeatures: {
            tenantAdmin: true,
            platformAdmin: true,
          },
          publicConfig: {
            smoke: true,
          },
        },
      });
      return;
    }

    jsonResponse(res, 404, { code: 'NOT_FOUND', message: 'mock endpoint not found' });
  });

  const port = await listen(server);
  return {
    baseUrl: `http://127.0.0.1:${port}`,
    requests,
    close: () => closeServer(server),
  };
}

async function createStaticServer(portal, apiBaseUrl) {
  const distDir = path.join(distRoot, portal.dist);
  const runtimeConfig = {
    portal: portal.portal,
    apiBaseUrl,
    supabaseUrl: 'https://auth.example.test',
    supabasePublishableKey: 'sb_publishable_mock_key_for_static_smoke',
    tenantCode: 'master',
  };

  const server = http.createServer((req, res) => {
    const url = new URL(req.url || '/', 'http://127.0.0.1');
    const requestPath = decodeURIComponent(url.pathname);

    if (requestPath === '/runtime-config.json') {
      jsonResponse(res, 200, runtimeConfig);
      return;
    }

    const filePath = resolveStaticPath(distDir, requestPath);
    if (!filePath) {
      textResponse(res, 403, 'forbidden', { 'content-type': 'text/plain; charset=utf-8' });
      return;
    }

    const finalPath = fs.existsSync(filePath) && fs.statSync(filePath).isFile()
      ? filePath
      : path.join(distDir, 'index.html');

    if (!fs.existsSync(finalPath)) {
      textResponse(res, 404, 'not found', { 'content-type': 'text/plain; charset=utf-8' });
      return;
    }

    const headers = {
      'content-type': contentTypeFor(finalPath),
      'cache-control': finalPath.endsWith('index.html') ? 'no-store' : 'public, max-age=31536000, immutable',
    };
    res.writeHead(200, headers);
    fs.createReadStream(finalPath).pipe(res);
  });

  const port = await listen(server);
  return {
    baseUrl: `http://127.0.0.1:${port}`,
    runtimeConfig,
    close: () => closeServer(server),
  };
}

function resolveStaticPath(distDir, requestPath) {
  const cleanPath = requestPath === '/' ? '/index.html' : requestPath;
  const resolved = path.resolve(distDir, `.${cleanPath}`);
  const normalizedRoot = path.resolve(distDir);
  if (resolved !== normalizedRoot && !resolved.startsWith(`${normalizedRoot}${path.sep}`)) return null;
  return resolved;
}

function listen(server) {
  return new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      resolve(address.port);
    });
  });
}

function closeServer(server) {
  return new Promise(resolve => {
    server.close(() => resolve());
  });
}

function parseAssetUrls(indexHtml) {
  const urls = new Set();
  for (const match of indexHtml.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)) {
    urls.add(match[1]);
  }
  for (const match of indexHtml.matchAll(/<link\b[^>]*\bhref=["']([^"']+)["'][^>]*>/gi)) {
    const tag = match[0];
    if (/rel=["']stylesheet["']/i.test(tag)) urls.add(match[1]);
  }
  return [...urls];
}

function validateRuntimeConfig(config, portal) {
  const errors = [];
  const keys = Object.keys(config);
  const unknownKeys = keys.filter(key => !allowedRuntimeConfigKeys.has(key));
  const forbiddenKeys = keys.filter(key => forbiddenRuntimeConfigKeys.includes(key));
  if (unknownKeys.length) errors.push(`unknown runtime config keys: ${unknownKeys.join(', ')}`);
  if (forbiddenKeys.length) errors.push(`forbidden runtime config keys: ${forbiddenKeys.join(', ')}`);
  if (config.portal !== portal.portal) errors.push(`portal mismatch: expected ${portal.portal}, got ${config.portal}`);
  if (!String(config.apiBaseUrl || '').startsWith('http://127.0.0.1:')) errors.push('apiBaseUrl must point to the local mock API in smoke');
  if (!String(config.supabaseUrl || '').startsWith('https://')) errors.push('supabaseUrl must be HTTPS even in smoke runtime config');
  if (!config.supabasePublishableKey) errors.push('supabasePublishableKey is required');
  for (const [key, value] of Object.entries(config)) {
    if (forbiddenValuePatterns.some(pattern => pattern.test(String(value)))) errors.push(`secret-looking value in ${key}`);
  }
  return errors;
}

function assertDistExists(portal) {
  const distDir = path.join(distRoot, portal.dist);
  const indexPath = path.join(distDir, 'index.html');
  if (!fs.existsSync(distDir)) throw new Error(`${relative(distDir)} does not exist. Build H5 before running smoke:taro:h5.`);
  if (!fs.existsSync(indexPath)) throw new Error(`${relative(indexPath)} does not exist. Build output is not deployable.`);
}

async function smokePortal(portal, api) {
  assertDistExists(portal);
  const staticServer = await createStaticServer(portal, api.baseUrl);
  try {
    const checks = [];
    const index = await request(`${staticServer.baseUrl}/index.html`);
    checks.push({
      id: `${portal.portal}.index`,
      ok: index.ok && /<div id="app">/.test(index.text) && /<script\b/i.test(index.text),
      detail: `status=${index.status}`,
    });

    const assetUrls = parseAssetUrls(index.text);
    checks.push({
      id: `${portal.portal}.assets.discovered`,
      ok: assetUrls.length >= 2,
      detail: `${assetUrls.length} asset reference(s)`,
    });
    for (const assetUrl of assetUrls) {
      const target = new URL(assetUrl, `${staticServer.baseUrl}/index.html`);
      const asset = await request(target);
      checks.push({
        id: `${portal.portal}.asset.${assetUrl}`,
        ok: asset.ok && asset.text.length > 0,
        detail: `status=${asset.status} bytes=${asset.text.length}`,
      });
    }

    const runtime = await request(`${staticServer.baseUrl}/runtime-config.json`);
    let runtimeConfig = null;
    try {
      runtimeConfig = JSON.parse(runtime.text);
    } catch {
      runtimeConfig = null;
    }
    const runtimeErrors = runtimeConfig ? validateRuntimeConfig(runtimeConfig, portal) : ['runtime-config.json is not valid JSON'];
    checks.push({
      id: `${portal.portal}.runtime_config`,
      ok: runtime.ok && runtimeErrors.length === 0,
      detail: runtimeErrors.length ? runtimeErrors.join('; ') : 'public runtime config accepted',
    });

    const fallback = await request(`${staticServer.baseUrl}${portal.landingPath}`);
    checks.push({
      id: `${portal.portal}.history_fallback`,
      ok: fallback.ok && /<div id="app">/.test(fallback.text),
      detail: `status=${fallback.status} path=${portal.landingPath}`,
    });

    if (runtimeConfig) {
      const resolveUrl = new URL('/api/tenant/resolve', runtimeConfig.apiBaseUrl);
      resolveUrl.searchParams.set('tenantCode', runtimeConfig.tenantCode);
      resolveUrl.searchParams.set('host', new URL(staticServer.baseUrl).host);
      const tenantResolve = await request(resolveUrl, { headers: { 'x-smoke-portal': portal.portal } });
      let tenantPayload = null;
      try {
        tenantPayload = JSON.parse(tenantResolve.text);
      } catch {
        tenantPayload = null;
      }
      checks.push({
        id: `${portal.portal}.tenant_resolve_contract`,
        ok: tenantResolve.ok && Boolean(tenantPayload?.item?.tenantId) && tenantPayload.item.features?.enableLeaderboard === false,
        detail: `status=${tenantResolve.status}`,
      });
    } else {
      checks.push({
        id: `${portal.portal}.tenant_resolve_contract`,
        ok: false,
        detail: 'runtime config unavailable',
      });
    }

    return {
      portal: portal.portal,
      staticBaseUrl: staticServer.baseUrl,
      landingPath: portal.landingPath,
      checks,
    };
  } finally {
    await staticServer.close();
  }
}

function summarize(results) {
  const checks = results.flatMap(result => result.checks);
  return {
    pass: checks.filter(item => item.ok).length,
    fail: checks.filter(item => !item.ok).length,
    checks,
  };
}

async function main() {
  const api = await createMockApiServer();
  try {
    const results = [];
    for (const portal of portals) {
      results.push(await smokePortal(portal, api));
    }
    const summary = summarize(results);
    console.log(`Taro H5 static smoke: ${summary.fail} fail(s), ${summary.pass} pass(es)`);
    for (const result of results) {
      console.log(`[PORTAL] ${result.portal} ${result.staticBaseUrl} -> ${result.landingPath}`);
      for (const check of result.checks) {
        console.log(`[${check.ok ? 'PASS' : 'FAIL'}] ${check.id}: ${check.detail}`);
      }
    }
    const resolveRequests = api.requests.filter(item => item.path === '/api/tenant/resolve');
    console.log(`[MOCK API] tenant resolve requests=${resolveRequests.length}`);
    if (resolveRequests.length !== portals.length) {
      console.log(`[FAIL] mock_api.tenant_resolve_count: expected ${portals.length}, got ${resolveRequests.length}`);
      process.exitCode = 1;
      return;
    }
    if (summary.fail > 0) process.exitCode = 1;
  } finally {
    await api.close();
  }
}

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