#!/usr/bin/env node
// build-time gate: prevents NEW files from gaining a `conversationId` /
// `sessionId` reference while the 146-file rename slice is in progress.
//
// The legacy identifiers are being collapsed into the single canonical
// `sessionKey`. This sprint shipped the schema-side load-bearing pieces
// (backfill, new index, boot-time adherence probe) plus the core lib rename.
// The mechanical tail (routes, MCP plugin parameters, UI props, WhatsApp,
// tests, specialist templates) is split across follow-up tasks 1009 and 1010.
//
// During the transition window, every file currently containing the legacy
// identifiers is named in `conversation-id-allowlist.txt`. Each follow-up
// rename PR removes file paths from the allowlist as those files are migrated.
// This gate fails if:
//   (a) a file NOT in the allowlist contains any of the four legacy tokens, or
//   (b) the allowlist references a path that no longer exists (stale entry).
//
// Once tasks 1009 and 1010 land, the allowlist is empty; the gate then enforces
// the task's outcome contract (zero hits anywhere outside .tasks/archive/).

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { dirname, join, relative, basename } from 'node:path'
import { fileURLToPath } from 'node:url'

const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = join(SCRIPT_DIR, '..', '..')
const ALLOWLIST_PATH = join(SCRIPT_DIR, 'conversation-id-allowlist.txt')

const SCAN_ROOTS = [
  join(REPO_ROOT, 'platform'),
  join(REPO_ROOT, 'packages'),
  join(REPO_ROOT, 'brands'),
  join(REPO_ROOT, '.docs'),
  join(REPO_ROOT, '.claude', 'skills'),
]

const ALLOWED_EXTS = new Set([
  '.sh', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
  '.md', '.cypher', '.json',
])
const SKIP_DIR_NAMES = new Set([
  'node_modules', 'dist', 'build', '.next', 'payload',
])
const SKIP_PATH_FRAGMENTS = ['.tasks/archive/']

// Whole-word matches only — avoids hits inside longer identifiers like
// `agentSessionId` (Claude Agent SDK per-process session, intentionally kept)
// and `conversationIdentity` (conversation-document :KnowledgeDocument hash key, unrelated).
const LEGACY_TOKEN_RE = /\b(?:conversationId|sessionId|CONVERSATION_ID|SESSION_ID)\b/

// Comment-line skip — matches the convention in check-no-task-id-leaks.mjs.
// Comments that *describe* the legacy field (inside the migration code itself,
// or inline docstrings explaining the rename) are not new executing code
// introducing the token, so they don't trip the gate.
function isCommentLine(line) {
  const trimmed = line.replace(/^\s+/, '')
  if (trimmed.startsWith('#')) return true
  if (trimmed.startsWith('//')) return true
  if (trimmed.startsWith('/*')) return true
  if (trimmed.startsWith('*')) return true
  if (trimmed.startsWith('{/*')) return true
  return false
}

function loadAllowlist() {
  if (!existsSync(ALLOWLIST_PATH)) return new Set()
  const text = readFileSync(ALLOWLIST_PATH, 'utf-8')
  return new Set(
    text.split('\n')
      .map(l => l.trim())
      .filter(l => l && !l.startsWith('#'))
  )
}

function shouldScanFile(filePath) {
  const name = basename(filePath)
  const ext = name.includes('.') ? '.' + name.split('.').pop() : ''
  if (!ALLOWED_EXTS.has(ext)) return false
  const rel = relative(REPO_ROOT, filePath)
  for (const frag of SKIP_PATH_FRAGMENTS) {
    if (rel.includes(frag)) return false
  }
  return true
}

function* walk(root) {
  let entries
  try { entries = readdirSync(root) } catch { return }
  for (const name of entries) {
    if (SKIP_DIR_NAMES.has(name)) continue
    const full = join(root, name)
    let st
    try { st = statSync(full) } catch { continue }
    if (st.isDirectory()) {
      yield* walk(full)
    } else if (st.isFile()) {
      yield full
    }
  }
}

const allowlist = loadAllowlist()
const newLeaks = []
const hitFiles = new Set()

for (const root of SCAN_ROOTS) {
  if (!existsSync(root)) continue
  for (const file of walk(root)) {
    if (!shouldScanFile(file)) continue
    let content
    try { content = readFileSync(file, 'utf-8') } catch { continue }
    if (!LEGACY_TOKEN_RE.test(content)) continue
    const rel = relative(REPO_ROOT, file)
    const lines = content.split('\n')
    // For non-allowlisted files: only flag hits in non-comment lines. For
    // allowlisted files: any hit counts (so the allowlist accurately tracks
    // the surface still using the legacy name, comments and code alike).
    if (allowlist.has(rel)) {
      hitFiles.add(rel)
      continue
    }
    let firstLine = 0, firstText = ''
    for (let i = 0; i < lines.length; i++) {
      if (!LEGACY_TOKEN_RE.test(lines[i])) continue
      if (isCommentLine(lines[i])) continue
      firstLine = i + 1
      firstText = lines[i].trim().slice(0, 120)
      break
    }
    if (firstLine > 0) {
      newLeaks.push({ file: rel, line: firstLine, content: firstText })
    }
  }
}

// Stale allowlist entries: files in the allowlist that no longer have a hit
// (renamed already, or no longer exist) — remove them.
const staleAllowlistEntries = [...allowlist].filter(p => !hitFiles.has(p))

let failed = false

if (newLeaks.length > 0) {
  failed = true
  console.error(`check-no-conversation-id-leaks: ${newLeaks.length} NEW file(s) introduced legacy identifier(s):`)
  for (const leak of newLeaks) {
    console.error(`  ${leak.file}:${leak.line}: ${leak.content}`)
  }
  console.error('')
  console.error('The single canonical identifier is `sessionKey`. New code must use it.')
  console.error('Legacy tokens: conversationId, sessionId, CONVERSATION_ID, SESSION_ID.')
  console.error('Follow-up tasks 1009 + 1010 are migrating existing files off the allowlist.')
}

if (staleAllowlistEntries.length > 0) {
  failed = true
  console.error('')
  console.error(`check-no-conversation-id-leaks: ${staleAllowlistEntries.length} stale allowlist entry(ies) — remove from platform/scripts/conversation-id-allowlist.txt:`)
  for (const entry of staleAllowlistEntries) {
    console.error(`  ${entry}`)
  }
}

if (failed) process.exit(1)

console.error(`check-no-conversation-id-leaks: ok (${allowlist.size} files in transition allowlist; 0 new leaks)`)
