#!/usr/bin/env node
// scaffold-registry-reminder.mjs
// PostToolUse hook — reads the compiled scaffold registry and emits advisory
// reminders when an edited file matches a registry source pattern.
//
// Template twin of the monorepo hook. Gracefully no-ops when the compiled
// registry is absent (e.g. before SDK delivers scaffold-registry.compiled.json
// to external projects — Step 7/SDK milestone).
//
// Exit 0 always (advisory hook — never blocks).

import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
import { join, normalize, relative } from 'node:path'

const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
const LOG_DIR = join(ROOT, '.claude', 'logs')
const LOG_FILE = join(LOG_DIR, 'scaffold-registry-reminder.log')
const STATE_FILE = join(LOG_DIR, 'scaffold-registry-reminder.state.json')
const REGISTRY_FILE = join(ROOT, '.claude', 'registries', 'scaffold-registry.compiled.json')

const DEFAULT_COOLDOWN_MS = 300_000 // 5 minutes

const GENERATED_DIR_SEGMENTS = ['_generated', '_gen']
const GENERATED_CONTENT_MARKER = '@generated'

function log(msg) {
  try {
    mkdirSync(LOG_DIR, { recursive: true })
    appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
  } catch {}
}

function pathMatchesPattern(filePath, pattern) {
  const normalizedFile = filePath.replace(/\\/g, '/')
  const normalizedPattern = pattern.replace(/\\/g, '/')

  if (normalizedFile === normalizedPattern) return true

  if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
    const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
    return normalizedFile.startsWith(prefix + '/')
  }

  if (normalizedPattern.includes('*')) {
    const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
    return new RegExp(`^${escaped}$`).test(normalizedFile)
  }

  return normalizedFile.startsWith(normalizedPattern + '/')
}

function loadState() {
  try {
    return JSON.parse(readFileSync(STATE_FILE, 'utf-8'))
  } catch {
    return {}
  }
}

function saveState(state) {
  try {
    mkdirSync(LOG_DIR, { recursive: true })
    writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8')
  } catch {}
}

function throttleKey(entryId, filePath) {
  return `${entryId}:${filePath}`
}

function isCoolingDown(state, key, cooldownMs) {
  const last = state[key]
  if (!last) return false
  return Date.now() - last < cooldownMs
}

function formatDependentLine(dep) {
  const regen = dep.regen === 'manual' ? 'manual check' : dep.regen
  const hint = dep.hint ? ` [${dep.hint}]` : ''
  return `    - ${dep.path} -> ${regen}${hint}`
}

function emitReminder(entry, relFilePath) {
  const lines = [
    `\uD83D\uDD14 Scaffold reminder -- ${entry.id} (${relFilePath})`,
    `  Downstream scaffolds that may need updating:`
  ]
  for (const dep of entry.dependents) {
    lines.push(formatDependentLine(dep))
  }
  lines.push(
    `  If this is a scaffold-sensitive pattern not in the registry, also add an entry to .claude/registries/scaffold-registry.yml.`
  )
  return lines.join('\n')
}

function emitMissingEntryHint(relFilePath) {
  return [
    `\uD83D\uDD14 Scaffold reminder -- unregistered generated path (${relFilePath})`,
    `  This path looks scaffold-generated but has no registry entry.`,
    `  If it is scaffold-sensitive, add a new entry to .claude/registries/scaffold-registry.yml`,
    `  so the reminder hook and /work handoff can track it.`
  ].join('\n')
}

function looksLikeGeneratedPath(filePath) {
  const normalizedFile = filePath.replace(/\\/g, '/')
  const segments = normalizedFile.split('/')
  return segments.some((seg) => GENERATED_DIR_SEGMENTS.includes(seg))
}

function looksLikeGeneratedContent(absFilePath) {
  try {
    const content = readFileSync(absFilePath, 'utf-8').slice(0, 500)
    return content.includes(GENERATED_CONTENT_MARKER)
  } catch {
    return false
  }
}

try {
  const chunks = []
  for await (const chunk of process.stdin) chunks.push(chunk)
  const input = JSON.parse(Buffer.concat(chunks).toString())

  const rawFilePath = input.tool_input?.file_path
  if (!rawFilePath) process.exit(0)

  const absFilePath = normalize(rawFilePath)
  const relFilePath = relative(ROOT, absFilePath).replace(/\\/g, '/')

  // Graceful no-op when registry is absent (pre-SDK-delivery state)
  let registry
  try {
    const raw = readFileSync(REGISTRY_FILE, 'utf-8')
    registry = JSON.parse(raw)
  } catch {
    log(`SKIP — registry not found (pre-SDK-delivery)`)
    process.exit(0)
  }

  const entries = registry?.entries ?? []

  const matched = entries.filter((entry) =>
    (entry.sources ?? []).some((pattern) => pathMatchesPattern(relFilePath, pattern))
  )

  const state = loadState()
  const now = Date.now()
  const messages = []

  if (matched.length > 0) {
    for (const entry of matched) {
      const key = throttleKey(entry.id, relFilePath)
      const cooldown = entry.cooldown_ms ?? DEFAULT_COOLDOWN_MS
      if (isCoolingDown(state, key, cooldown)) {
        log(`THROTTLED — ${entry.id} for ${relFilePath}`)
        continue
      }
      messages.push(emitReminder(entry, relFilePath))
      state[key] = now
      log(`EMITTED — ${entry.id} for ${relFilePath}`)
    }
  } else {
    const isGenerated = looksLikeGeneratedPath(relFilePath) || looksLikeGeneratedContent(absFilePath)

    if (isGenerated) {
      const key = throttleKey('__missing__', relFilePath)
      if (!isCoolingDown(state, key, DEFAULT_COOLDOWN_MS)) {
        messages.push(emitMissingEntryHint(relFilePath))
        state[key] = now
        log(`EMITTED missing-entry hint for ${relFilePath}`)
      } else {
        log(`THROTTLED missing-entry hint for ${relFilePath}`)
      }
    }
  }

  if (messages.length > 0) {
    saveState(state)
    process.stderr.write(messages.join('\n\n') + '\n')
    process.exit(2)
  }
} catch (err) {
  log(`ERROR: ${err.message}`)
}

process.exit(0)
