#!/usr/bin/env node
'use strict';

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const RELEASE = '2.0.0-rc.1';
const RELEASE_DIR = `docs/releases/${RELEASE}`;
const SCHEMA_VERSION = 'ecc.release-approval-gate.v1';
const SCRIPT_PATH = 'scripts/release-approval-gate.js';
const OWNER_PACKET_PATH = `${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`;
const URL_LEDGER_PATH = `${RELEASE_DIR}/release-url-ledger-2026-05-19.md`;
const PREVIEW_MANIFEST_PATH = `${RELEASE_DIR}/preview-pack-manifest.md`;
const REQUIRED_COMMAND = 'npm run release:approval-gate -- --format json';

const REQUIRED_DECISIONS = [
  {
    id: 'github-prerelease',
    label: 'GitHub prerelease',
  },
  {
    id: 'npm-next-publish',
    label: 'npm `next` publish',
  },
  {
    id: 'claude-plugin-tag',
    label: 'Claude plugin tag',
  },
  {
    id: 'codex-repo-marketplace',
    label: 'Codex repo marketplace',
  },
  {
    id: 'ecc-tools-billing-language',
    label: 'ECC Tools billing language',
  },
  {
    id: 'video-upload',
    label: 'Video upload',
  },
  {
    id: 'social-and-longform',
    label: 'X, LinkedIn, GitHub Discussion, longform',
  },
  {
    id: 'outbound-growth',
    label: 'Sponsor, partner, consulting, conference, podcast outreach',
  },
];

const REQUIRED_URL_SURFACES = [
  {
    id: 'github-prerelease-url',
    label: 'GitHub prerelease URL',
    exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1',
  },
  {
    id: 'npm-rc-package-url',
    label: 'npm rc package URL',
    exampleUrl: 'https://www.npmjs.com/package/ecc-universal/v/2.0.0-rc.1',
  },
  {
    id: 'claude-plugin-tag-url',
    label: 'Claude plugin tag URL',
    exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/ecc--v2.0.0-rc.1',
  },
  {
    id: 'codex-repo-marketplace-evidence',
    label: 'Codex repo-marketplace evidence',
    exampleUrl: 'https://github.com/affaan-m/ECC/tree/v2.0.0-rc.1/.codex-plugin',
  },
  {
    id: 'primary-launch-video-url',
    label: 'Primary launch video URL',
    exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000000',
  },
  {
    id: 'short-clip-urls',
    label: 'Short clip URLs',
    exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000001',
  },
  {
    id: 'ecc-tools-billing-readiness-url',
    label: 'ECC Tools billing/readiness URL',
    exampleUrl: 'https://github.com/ECC-Tools',
  },
];

const ANNOUNCEMENT_FILES = [
  `${RELEASE_DIR}/release-notes.md`,
  `${RELEASE_DIR}/x-thread.md`,
  `${RELEASE_DIR}/linkedin-post.md`,
  `${RELEASE_DIR}/article-outline.md`,
  `${RELEASE_DIR}/partner-sponsor-talks-pack.md`,
  'docs/business/social-launch-copy.md',
];

function usage() {
  console.log([
    'Usage: node scripts/release-approval-gate.js [--format <text|json>] [--root <dir>]',
    '',
    'Final approval gate for ECC 2.0 rc.1 publication and outbound actions.',
    '',
    'Options:',
    '  --format <text|json>  Output format (default: text)',
    '  --json                Alias for --format json',
    '  --root <dir>          Repository root to inspect (default: cwd)',
    '  --help, -h            Show this help',
  ].join('\n'));
}

function readArgValue(args, index, flagName) {
  const value = args[index + 1];
  if (!value || value.startsWith('--')) {
    throw new Error(`${flagName} requires a value`);
  }
  return value;
}

function parseArgs(argv) {
  const args = argv.slice(2);
  const parsed = {
    format: 'text',
    help: false,
    root: path.resolve(process.cwd()),
  };

  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];

    if (arg === '--help' || arg === '-h') {
      parsed.help = true;
      continue;
    }

    if (arg === '--json') {
      parsed.format = 'json';
      continue;
    }

    if (arg === '--format') {
      parsed.format = readArgValue(args, index, arg).toLowerCase();
      index += 1;
      continue;
    }

    if (arg.startsWith('--format=')) {
      parsed.format = arg.slice('--format='.length).toLowerCase();
      continue;
    }

    if (arg === '--root') {
      parsed.root = path.resolve(readArgValue(args, index, arg));
      index += 1;
      continue;
    }

    if (arg.startsWith('--root=')) {
      parsed.root = path.resolve(arg.slice('--root='.length));
      continue;
    }

    throw new Error(`Unknown argument: ${arg}`);
  }

  if (!['text', 'json'].includes(parsed.format)) {
    throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
  }

  return parsed;
}

function readText(rootDir, relativePath) {
  try {
    return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
  } catch (_error) {
    return '';
  }
}

function fileExists(rootDir, relativePath) {
  return fs.existsSync(path.join(rootDir, relativePath));
}

function safeParseJson(text) {
  if (!text.trim()) {
    return null;
  }

  try {
    return JSON.parse(text);
  } catch (_error) {
    return null;
  }
}

function normalizeLabel(value) {
  return String(value)
    .replace(/[`*_]/g, '')
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase();
}

function normalizeState(value) {
  return String(value)
    .replace(/[`*_]/g, '')
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase();
}

function splitMarkdownRow(row) {
  const trimmed = row.trim();
  if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {
    return [];
  }

  return trimmed
    .slice(1, -1)
    .split('|')
    .map(cell => cell.trim());
}

function parseDecisionRegister(packet) {
  const decisions = new Map();

  for (const line of packet.split('\n')) {
    const cells = splitMarkdownRow(line);
    if (cells.length < 4) {
      continue;
    }

    const [decision, state] = cells;
    const normalizedDecision = normalizeLabel(decision);
    if (
      !normalizedDecision
      || normalizedDecision === 'decision'
      || /^-+$/.test(normalizedDecision)
    ) {
      continue;
    }

    decisions.set(normalizedDecision, normalizeState(state));
  }

  return decisions;
}

function isApproved(state) {
  return state === 'approve' || state === 'approved';
}

function lineNumberForIndex(text, index) {
  return text.slice(0, index).split('\n').length;
}

function findAnnouncementOffenders(rootDir, relativePaths) {
  const offenders = [];
  const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g;
  const anglePlaceholderPattern = /<(?!(?:https?:\/\/|mailto:|#))[^>\n]*(?:url|link|todo|tbd|placeholder)[^>\n]*>/gi;
  const barePlaceholderPattern = /\bTODO\b|\bTBD\b|\bPLACEHOLDER\b/g;

  for (const relativePath of relativePaths) {
    const text = readText(rootDir, relativePath);
    if (!text) {
      offenders.push({
        path: relativePath,
        line: 1,
        marker: 'missing file',
      });
      continue;
    }

    for (const match of text.matchAll(privatePathPattern)) {
      offenders.push({
        path: relativePath,
        line: lineNumberForIndex(text, match.index),
        marker: match[0],
      });
    }

    for (const match of text.matchAll(anglePlaceholderPattern)) {
      offenders.push({
        path: relativePath,
        line: lineNumberForIndex(text, match.index),
        marker: match[0],
      });
    }

    for (const match of text.matchAll(barePlaceholderPattern)) {
      offenders.push({
        path: relativePath,
        line: lineNumberForIndex(text, match.index),
        marker: match[0],
      });
    }
  }

  return offenders;
}

function ledgerBlockers(ledger) {
  const blockers = [];

  if (/^##\s+Approval-Gated URLs\s*$/im.test(ledger)) {
    blockers.push('approval-gated URL section still present');
  }

  for (const [pattern, label] of [
    [/not published yet/i, 'not-published marker still present'],
    [/must return/i, 'must-return readback marker still present'],
    [/Gate before use/i, 'gate-before-use column still present'],
    [/\bpending\b/i, 'pending marker still present'],
    [/\bblocked\b/i, 'blocked marker still present'],
  ]) {
    if (pattern.test(ledger)) {
      blockers.push(label);
    }
  }

  return blockers;
}

function makeCheck(id, status, evidence, fix) {
  return {
    id,
    status,
    evidence,
    fix: status === 'pass' ? '' : fix,
  };
}

function topActionsForChecks(checks) {
  const actions = [];
  const failedIds = new Set(checks.filter(check => check.status !== 'pass').map(check => check.id));

  if (failedIds.has('release-approval-script-registered')) {
    actions.push('Wire release:approval-gate into package.json, package files, and the preview-pack manifest.');
  }

  if (failedIds.has('owner-decisions-approved')) {
    actions.push('Approve, defer, or block each owner decision row explicitly after final evidence is rerun from the release commit.');
  }

  if (failedIds.has('release-url-ledger-finalized')) {
    actions.push('Replace approval-gated URL ledger rows with live readback URLs from the approved release, package, plugin, video, and billing surfaces.');
  }

  if (failedIds.has('final-evidence-command-listed')) {
    actions.push('Add release:approval-gate to the final evidence command lists before asking for publication approval.');
  }

  if (failedIds.has('announcement-copy-finalized')) {
    actions.push('Remove unresolved placeholders and private local paths from launch, social, and outbound copy.');
  }

  if (failedIds.has('public-action-guard-present')) {
    actions.push('Restore the explicit no-outbound/no-publish authorization boundary in the owner packet.');
  }

  return actions;
}

function buildReport(options = {}) {
  const rootDir = path.resolve(options.root || process.cwd());
  const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
  const packageScripts = packageJson.scripts || {};
  const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
  const ownerPacket = readText(rootDir, OWNER_PACKET_PATH);
  const ledger = readText(rootDir, URL_LEDGER_PATH);
  const manifest = readText(rootDir, PREVIEW_MANIFEST_PATH);
  const decisions = parseDecisionRegister(ownerPacket);

  const missingDecisions = [];
  const unapprovedDecisions = [];
  for (const decision of REQUIRED_DECISIONS) {
    const state = decisions.get(normalizeLabel(decision.label));
    if (!state) {
      missingDecisions.push(decision.label);
    } else if (!isApproved(state)) {
      unapprovedDecisions.push(`${decision.label}=${state}`);
    }
  }

  const missingUrlSurfaces = REQUIRED_URL_SURFACES
    .filter(surface => !ledger.includes(surface.label))
    .map(surface => surface.label);
  const urlBlockers = ledgerBlockers(ledger);
  const announcementOffenders = findAnnouncementOffenders(rootDir, ANNOUNCEMENT_FILES);
  const commandListedIn = [
    ownerPacket.includes(REQUIRED_COMMAND) ? OWNER_PACKET_PATH : '',
    ledger.includes(REQUIRED_COMMAND) ? URL_LEDGER_PATH : '',
    manifest.includes(REQUIRED_COMMAND) ? PREVIEW_MANIFEST_PATH : '',
  ].filter(Boolean);

  const checks = [
    makeCheck(
      'release-approval-script-registered',
      packageScripts['release:approval-gate'] === `node ${SCRIPT_PATH}`
        && packageFiles.includes(SCRIPT_PATH)
        && fileExists(rootDir, SCRIPT_PATH)
        && manifest.includes(`\`${SCRIPT_PATH}\``)
        && manifest.includes(REQUIRED_COMMAND)
        ? 'pass'
        : 'fail',
      'package script, npm package file entry, local script, and preview-pack manifest reference',
      'Add release:approval-gate to package scripts, package files, and preview-pack-manifest.md.'
    ),
    makeCheck(
      'owner-decisions-approved',
      missingDecisions.length === 0 && unapprovedDecisions.length === 0 ? 'pass' : 'fail',
      missingDecisions.length === 0 && unapprovedDecisions.length === 0
        ? `${REQUIRED_DECISIONS.length} owner decision rows are approved`
        : `missing decisions: ${missingDecisions.join(', ') || 'none'}; pending decisions: ${unapprovedDecisions.join(', ') || 'none'}`,
      'Set every required owner decision row to approve only after the final release evidence has been rerun.'
    ),
    makeCheck(
      'release-url-ledger-finalized',
      ledger
        && missingUrlSurfaces.length === 0
        && urlBlockers.length === 0
        ? 'pass'
        : 'fail',
      ledger && missingUrlSurfaces.length === 0 && urlBlockers.length === 0
        ? `${REQUIRED_URL_SURFACES.length} final URL surfaces are recorded without approval-gated blockers`
        : `missing URL surfaces: ${missingUrlSurfaces.join(', ') || 'none'}; blockers: ${urlBlockers.join(', ') || 'none'}`,
      'Regenerate the release URL ledger after the approved publication actions and record live readback URLs.'
    ),
    makeCheck(
      'final-evidence-command-listed',
      commandListedIn.length === 3 ? 'pass' : 'fail',
      commandListedIn.length === 3
        ? `${REQUIRED_COMMAND} is listed in owner packet, URL ledger, and preview manifest`
        : `${REQUIRED_COMMAND} listed in: ${commandListedIn.join(', ') || 'none'}`,
      'List release:approval-gate in every final evidence command block.'
    ),
    makeCheck(
      'announcement-copy-finalized',
      announcementOffenders.length === 0 ? 'pass' : 'fail',
      announcementOffenders.length === 0
        ? `${ANNOUNCEMENT_FILES.length} launch/outbound copy files have no placeholders or private paths`
        : `offenders: ${announcementOffenders.map(item => `${item.path}:${item.line}`).join(', ')}`,
      'Replace placeholders with live URLs and remove private local paths from launch/outbound copy.'
    ),
    makeCheck(
      'public-action-guard-present',
      ownerPacket.includes(
        'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.'
      )
        ? 'pass'
        : 'fail',
      'owner packet preserves the explicit no-public-action authorization boundary',
      'Restore the owner-packet sentence that blocks outbound, posts, package publish, plugin tags, and billing announcements.'
    ),
  ];

  const failed = checks.filter(check => check.status !== 'pass');
  const digest = crypto
    .createHash('sha256')
    .update(JSON.stringify(checks.map(check => [check.id, check.status, check.evidence])))
    .digest('hex')
    .slice(0, 12);

  return {
    schema_version: SCHEMA_VERSION,
    release: RELEASE,
    ready: failed.length === 0,
    digest,
    summary: {
      passed: checks.length - failed.length,
      failed: failed.length,
      total: checks.length,
    },
    top_actions: topActionsForChecks(checks),
    checks,
  };
}

function renderText(report) {
  const lines = [
    'ECC release approval gate',
    `Release: ${report.release}`,
    `Ready: ${report.ready ? 'yes' : 'no'}`,
    `Digest: ${report.digest}`,
    '',
    'Checks:',
  ];

  for (const check of report.checks) {
    lines.push(`- ${check.status} ${check.id}: ${check.evidence}`);
    if (check.fix) {
      lines.push(`  fix: ${check.fix}`);
    }
  }

  if (report.top_actions.length > 0) {
    lines.push('');
    lines.push('Top actions:');
    for (const action of report.top_actions) {
      lines.push(`- ${action}`);
    }
  }

  lines.push('');
  lines.push(`Passed: ${report.summary.passed}`);
  lines.push(`Failed: ${report.summary.failed}`);

  return `${lines.join('\n')}\n`;
}

function main() {
  let parsed;

  try {
    parsed = parseArgs(process.argv);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }

  if (parsed.help) {
    usage();
    return;
  }

  const report = buildReport({ root: parsed.root });

  if (parsed.format === 'json') {
    console.log(JSON.stringify(report, null, 2));
  } else {
    process.stdout.write(renderText(report));
  }

  if (!report.ready) {
    process.exit(2);
  }
}

if (require.main === module) {
  main();
}

module.exports = {
  ANNOUNCEMENT_FILES,
  REQUIRED_COMMAND,
  REQUIRED_DECISIONS,
  REQUIRED_URL_SURFACES,
  buildReport,
  parseArgs,
  renderText,
};
