import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';

const defaultEvidencePath = path.resolve(process.cwd(), 'docs/refactor/production-launch-evidence.json');
const defaultMaxAgeDays = 14;

const gateChecks = [
  {
    id: 'readiness.production.env',
    label: 'Production environment readiness',
    commandIncludes: 'readiness:production',
    summary: { blocker: 0 },
  },
  {
    id: 'readiness.production.db',
    label: 'Production database readiness',
    commandIncludes: 'readiness:production:db',
    summary: { blocker: 0 },
  },
  {
    id: 'auth.remote-smoke',
    label: 'Remote Supabase Auth/JWKS smoke',
    commandIncludes: 'smoke:auth:remote',
    summary: { failed: 0, requireAdminTokens: true },
  },
  {
    id: 'rls.tenant-isolation',
    label: 'Runtime tenant RLS isolation',
    commandIncludes: 'test:rls',
    summary: { failed: 0 },
  },
  {
    id: 'migration.pb-production-dry-run',
    label: 'PocketBase production dry-run',
    commandIncludes: 'pb:import:dry-run',
    summary: {
      blocker: 0,
      warning: 0,
      migrationProfile: 'production',
      requiredCollectionsMissing: 0,
      criticalFieldCoverageWarnings: 0,
    },
  },
  {
    id: 'migration.pb-import-validate',
    label: 'PocketBase import validation',
    commandIncludes: 'pb:import:validate',
    summary: { fail: 0 },
  },
  {
    id: 'migration.pb-import-sample',
    label: 'PocketBase imported business sampling',
    commandIncludes: 'pb:import:sample',
    summary: { fail: 0 },
  },
  {
    id: 'performance.api-real-data-read',
    label: 'Real-data API read benchmark',
    commandIncludes: 'perf:api:local',
    summary: {
      errors: 0,
      errorRate: { lte: 0.001 },
      p95Ms: { lte: 300 },
      p99Ms: { lte: 800 },
      concurrency: { gte: 30 },
      durationSeconds: { gte: 120 },
      includeWrites: false,
    },
  },
  {
    id: 'api.integration',
    label: 'API integration regression',
    commandIncludes: 'test:api',
    summary: { failed: 0 },
  },
  {
    id: 'worker.assets',
    label: 'Asset worker security regression',
    commandIncludes: 'test:worker:assets',
    summary: { failed: 0 },
  },
  {
    id: 'worker.commerce',
    label: 'Commerce/payment worker regression',
    commandIncludes: 'test:worker:commerce',
    summary: { failed: 0 },
  },
  {
    id: 'worker.imports',
    label: 'Async import worker regression',
    commandIncludes: 'test:worker:imports',
    summary: { failed: 0 },
  },
  {
    id: 'worker.public-banks',
    label: 'Public question bank worker regression',
    commandIncludes: 'test:worker:public-banks',
    summary: { failed: 0 },
  },
  {
    id: 'taro.check',
    label: 'Taro type/security guardrails',
    commandIncludes: 'check:taro',
    summary: { failed: 0 },
  },
  {
    id: 'taro.build.student',
    label: 'Student H5 build',
    commandIncludes: 'build:taro:h5:student',
    summary: { failed: 0 },
  },
  {
    id: 'taro.build.tenant',
    label: 'Tenant admin H5 build',
    commandIncludes: 'build:taro:h5:tenant',
    summary: { failed: 0 },
  },
  {
    id: 'taro.build.platform',
    label: 'Platform admin H5 build',
    commandIncludes: 'build:taro:h5:platform',
    summary: { failed: 0 },
  },
  {
    id: 'audit.runtime',
    label: 'Runtime dependency audit',
    commandIncludes: 'audit:runtime',
    summary: { critical: 0, high: 0 },
  },
  {
    id: 'security.codex-scan',
    label: 'Codex Security scan',
    commandIncludes: 'codex-security',
    summary: { critical: 0, high: 0 },
  },
];

const requiredAttestations = [
  {
    id: 'backup.snapshot',
    label: 'Production database backup/snapshot is prepared',
  },
  {
    id: 'rollback.plan',
    label: 'Rollback plan and old PocketBase read-only snapshot are prepared',
  },
  {
    id: 'migration.sampling',
    label: 'Real migrated users/questions/orders/assets were sampled',
  },
  {
    id: 'provider.production-accounts',
    label: 'SMS/OAuth/payment production accounts and callback domains were verified',
  },
  {
    id: 'object-storage.production-controls',
    label: 'Object storage AV/content scan, CDN boundary, watermark and lifecycle controls were verified',
  },
  {
    id: 'payment.reconciliation-sampling',
    label: 'Real payment/refund bill samples were reconciled',
  },
  {
    id: 'frontend.runtime-config-review',
    label: 'Three H5 runtime-config.json files were reviewed for public-only values',
  },
];

function parseArgs(argv) {
  const options = {
    evidencePath: defaultEvidencePath,
    json: false,
    maxAgeDays: defaultMaxAgeDays,
    allowStale: false,
  };
  for (let index = 2; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === '--json') options.json = true;
    else if (arg === '--allow-stale') options.allowStale = true;
    else if (arg === '--evidence') {
      options.evidencePath = path.resolve(process.cwd(), argv[index + 1] || '');
      index += 1;
    } else if (arg.startsWith('--evidence=')) {
      options.evidencePath = path.resolve(process.cwd(), arg.slice('--evidence='.length));
    } else if (arg === '--max-age-days') {
      options.maxAgeDays = Number(argv[index + 1]);
      index += 1;
    } else if (arg.startsWith('--max-age-days=')) {
      options.maxAgeDays = Number(arg.slice('--max-age-days='.length));
    }
  }
  if (!Number.isFinite(options.maxAgeDays) || options.maxAgeDays <= 0) options.maxAgeDays = defaultMaxAgeDays;
  return options;
}

function readJson(filePath) {
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

function normalizeStatus(value) {
  return String(value || '').trim().toLowerCase();
}

function isApproved(value) {
  return ['approved', 'pass', 'passed', 'ok', 'complete', 'completed'].includes(normalizeStatus(value));
}

function findById(items, id) {
  return (Array.isArray(items) ? items : []).find(item => item && item.id === id);
}

function resolveArtifact(evidencePath, artifact) {
  if (!artifact) return '';
  if (path.isAbsolute(artifact)) return artifact;
  return path.resolve(path.dirname(evidencePath), artifact);
}

function daysSince(value, now = Date.now()) {
  const timestamp = Date.parse(value);
  if (!Number.isFinite(timestamp)) return Number.POSITIVE_INFINITY;
  return (now - timestamp) / (24 * 60 * 60 * 1000);
}

function hasPath(object, keyPath) {
  const parts = keyPath.split('.');
  let cursor = object;
  for (const part of parts) {
    if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return false;
    cursor = cursor[part];
  }
  return true;
}

function valueAt(object, keyPath) {
  const parts = keyPath.split('.');
  let cursor = object;
  for (const part of parts) cursor = cursor?.[part];
  return cursor;
}

function compareSummary(actualSummary, expectedSummary) {
  const failures = [];
  for (const [key, expectedValue] of Object.entries(expectedSummary || {})) {
    if (!hasPath(actualSummary || {}, key)) {
      failures.push(`${key} is missing`);
      continue;
    }
    const actualValue = valueAt(actualSummary, key);
    const comparisonFailure = compareExpectedValue(actualValue, expectedValue);
    if (comparisonFailure) {
      failures.push(`${key} ${comparisonFailure}`);
    }
  }
  return failures;
}

function isComparatorSpec(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  return ['eq', 'lt', 'lte', 'gt', 'gte', 'oneOf'].some(key => Object.prototype.hasOwnProperty.call(value, key));
}

function compareExpectedValue(actualValue, expectedValue) {
  if (!isComparatorSpec(expectedValue)) {
    if (actualValue !== expectedValue) {
      return `expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`;
    }
    return '';
  }

  if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq') && actualValue !== expectedValue.eq) {
    return `expected ${JSON.stringify(expectedValue.eq)} but got ${JSON.stringify(actualValue)}`;
  }
  if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) {
    const choices = Array.isArray(expectedValue.oneOf) ? expectedValue.oneOf : [];
    if (!choices.includes(actualValue)) {
      return `expected one of ${JSON.stringify(choices)} but got ${JSON.stringify(actualValue)}`;
    }
  }

  const numericChecks = [
    ['lt', (actual, expected) => actual < expected, '<'],
    ['lte', (actual, expected) => actual <= expected, '<='],
    ['gt', (actual, expected) => actual > expected, '>'],
    ['gte', (actual, expected) => actual >= expected, '>='],
  ];
  for (const [key, predicate, label] of numericChecks) {
    if (!Object.prototype.hasOwnProperty.call(expectedValue, key)) continue;
    const actualNumber = Number(actualValue);
    const expectedNumber = Number(expectedValue[key]);
    if (!Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || !predicate(actualNumber, expectedNumber)) {
      return `expected ${label} ${expectedNumber} but got ${JSON.stringify(actualValue)}`;
    }
  }
  return '';
}

function resultCollector() {
  const checks = [];
  function pass(id, message, details = {}) {
    checks.push({ status: 'pass', id, message, details });
  }
  function warn(id, message, details = {}) {
    checks.push({ status: 'warn', id, message, details });
  }
  function block(id, message, details = {}) {
    checks.push({ status: 'blocker', id, message, details });
  }
  return { checks, pass, warn, block };
}

function validateTopLevel(evidence, collector) {
  if (evidence.schemaVersion !== 1) {
    collector.block('evidence.schema_version', 'Evidence schemaVersion must be 1', { actual: evidence.schemaVersion });
  } else {
    collector.pass('evidence.schema_version', 'Evidence schema version is supported');
  }

  if (evidence.environment !== 'production') {
    collector.block('evidence.environment', 'Evidence environment must be production', { actual: evidence.environment });
  } else {
    collector.pass('evidence.environment', 'Evidence environment is production');
  }

  if (!evidence.commit || !/^[0-9a-f]{7,40}$/i.test(String(evidence.commit))) {
    collector.block('evidence.commit', 'Evidence must include the reviewed deployment commit hash');
  } else {
    collector.pass('evidence.commit', 'Deployment commit is recorded', { commit: evidence.commit });
  }

  const target = evidence.target || {};
  const requiredTargets = ['apiBaseUrl', 'studentH5Url', 'tenantAdminH5Url', 'platformAdminH5Url'];
  for (const key of requiredTargets) {
    const value = String(target[key] || '');
    if (!value.startsWith('https://')) {
      collector.block(`target.${key}`, `${key} must be an HTTPS production URL`, { value });
    } else {
      collector.pass(`target.${key}`, `${key} is HTTPS`);
    }
  }
}

function validateGateCheck(spec, evidence, options, collector) {
  const item = findById(evidence.checks, spec.id);
  if (!item) {
    collector.block(`check.${spec.id}`, `${spec.label} evidence is missing`);
    return;
  }

  if (normalizeStatus(item.status) !== 'pass') {
    collector.block(`check.${spec.id}.status`, `${spec.label} must have status=pass`, { status: item.status });
  } else {
    collector.pass(`check.${spec.id}.status`, `${spec.label} passed`);
  }

  if (spec.commandIncludes && !String(item.command || '').includes(spec.commandIncludes)) {
    collector.block(`check.${spec.id}.command`, `${spec.label} command must include ${spec.commandIncludes}`, {
      command: item.command || '',
    });
  } else {
    collector.pass(`check.${spec.id}.command`, `${spec.label} command is recorded`);
  }

  const age = daysSince(item.completedAt);
  if (!options.allowStale && age > options.maxAgeDays) {
    collector.block(`check.${spec.id}.freshness`, `${spec.label} evidence is stale or missing completedAt`, {
      completedAt: item.completedAt || '',
      maxAgeDays: options.maxAgeDays,
    });
  } else {
    collector.pass(`check.${spec.id}.freshness`, `${spec.label} evidence is fresh enough`, {
      completedAt: item.completedAt || '',
    });
  }

  if (!item.artifact) {
    collector.block(`check.${spec.id}.artifact`, `${spec.label} must include a saved artifact/log path`);
  } else {
    const artifactPath = resolveArtifact(options.evidencePath, item.artifact);
    if (!fs.existsSync(artifactPath)) {
      collector.block(`check.${spec.id}.artifact`, `${spec.label} artifact file is missing`, { artifact: item.artifact });
    } else {
      collector.pass(`check.${spec.id}.artifact`, `${spec.label} artifact exists`, { artifact: item.artifact });
    }
  }

  const summaryFailures = compareSummary(item.summary || {}, spec.summary);
  if (summaryFailures.length > 0) {
    collector.block(`check.${spec.id}.summary`, `${spec.label} summary does not satisfy gate`, { failures: summaryFailures });
  } else {
    collector.pass(`check.${spec.id}.summary`, `${spec.label} summary satisfies gate`);
  }
}

function validateAttestation(spec, evidence, options, collector) {
  const item = findById(evidence.attestations, spec.id);
  if (!item) {
    collector.block(`attestation.${spec.id}`, `${spec.label} attestation is missing`);
    return;
  }

  if (!isApproved(item.status)) {
    collector.block(`attestation.${spec.id}.status`, `${spec.label} must be approved`, { status: item.status });
  } else {
    collector.pass(`attestation.${spec.id}.status`, `${spec.label} is approved`);
  }

  if (!String(item.approver || '').trim()) {
    collector.block(`attestation.${spec.id}.approver`, `${spec.label} must include an approver`);
  } else {
    collector.pass(`attestation.${spec.id}.approver`, `${spec.label} has an approver`);
  }

  const age = daysSince(item.approvedAt);
  if (!options.allowStale && age > options.maxAgeDays) {
    collector.block(`attestation.${spec.id}.freshness`, `${spec.label} approval is stale or missing approvedAt`, {
      approvedAt: item.approvedAt || '',
      maxAgeDays: options.maxAgeDays,
    });
  } else {
    collector.pass(`attestation.${spec.id}.freshness`, `${spec.label} approval is fresh enough`, {
      approvedAt: item.approvedAt || '',
    });
  }
}

function validateEvidence(evidence, options) {
  const collector = resultCollector();
  validateTopLevel(evidence, collector);
  for (const spec of gateChecks) validateGateCheck(spec, evidence, options, collector);
  for (const spec of requiredAttestations) validateAttestation(spec, evidence, options, collector);
  return collector.checks;
}

function summarize(checks) {
  return checks.reduce(
    (summary, item) => {
      summary[item.status] += 1;
      return summary;
    },
    { blocker: 0, warn: 0, pass: 0 },
  );
}

function printHuman(options, evidence, checks) {
  const summary = summarize(checks);
  console.log('Production launch gate');
  console.log(`Evidence: ${options.evidencePath}`);
  console.log(`Host: ${os.hostname()}`);
  console.log(`Target API: ${evidence.target?.apiBaseUrl || '(missing)'}`);
  console.log(`Summary: ${summary.blocker} blocker(s), ${summary.warn} warning(s), ${summary.pass} pass(es)`);
  for (const item of checks) {
    const marker = item.status === 'blocker' ? 'BLOCK' : item.status === 'warn' ? 'WARN ' : 'PASS ';
    console.log(`[${marker}] ${item.id}: ${item.message}`);
  }
}

function main() {
  const options = parseArgs(process.argv);
  if (!fs.existsSync(options.evidencePath)) {
    const message = `Evidence file not found: ${options.evidencePath}`;
    if (options.json) console.log(JSON.stringify({ summary: { blocker: 1, warn: 0, pass: 0 }, checks: [{ status: 'blocker', id: 'evidence.file', message }] }, null, 2));
    else console.error(`${message}\nCreate it from docs/refactor/production-launch-evidence.template.json after running real production checks.`);
    process.exitCode = 1;
    return;
  }

  const evidence = readJson(options.evidencePath);
  const checks = validateEvidence(evidence, options);
  const summary = summarize(checks);
  if (options.json) console.log(JSON.stringify({ summary, checks }, null, 2));
  else printHuman(options, evidence, checks);
  if (summary.blocker > 0) process.exitCode = 1;
}

const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) {
  main();
}

export { gateChecks, requiredAttestations, validateEvidence };
