#!/usr/bin/env node
/**
 * Consumer: copy packaged design-system extras into the app repo.
 *
 * Writes (and overwrites) ONLY:
 *   - `.cursor/skills/<bundled exxat-* skill folders>/`
 *   - `.claude/skills/<bundled exxat-* skill folders>/`  (same SKILL.md shape;
 *       Claude Code reads from `.claude/skills/` instead of `.cursor/skills/`)
 *   - `.cursor/rules/<bundled exxat-*.mdc rule files>`
 *   - `.cursor/hooks/exxat-*.mjs` + merged `.cursor/hooks.json`
 *   - `.claude/hooks/exxat-*.mjs` + merged `.claude/settings.json`
 *   - `docs/exxat-ds/*.md`             (pattern reference docs + consumer-upgrade-checklist.md)
 *   - `docs/exxat-ds/handbook/*.md`    (HANDBOOK, glossary, voice-and-tone, reference-implementations)
 *
 * The CLI is **namespaced** — it only touches files starting with `exxat-`
 * (skills + rules) so a consumer's own non-Exxat rules/skills are preserved.
 * Product code under `app/`, `pages/`, `src/`, plus the consumer's `AGENTS.md`
 * and any of their own docs are never modified.
 *
 * Run after every `npm install @exxatdesignux/ui@…` / `pnpm update @exxatdesignux/ui`.
 *
 *   npx --package=@exxatdesignux/ui@latest exxat-ui sync-extras
 */

import { spawnSync } from "node:child_process"
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
import { dirname, join, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"

const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = resolve(__dirname, "..")
const cwd = process.cwd()

const { rewriteConsumerRule } = await import(
  pathToFileURL(join(pkgRoot, "consumer-extras/scripts/rewrite-consumer-rules.mjs")).href
)

// `--quiet` suppresses the per-file "[sync-extras] wrote …" lines so the
// scaffolder (`create-exxat-app`) can call us inline without drowning the
// user in 50+ status lines. The final summary line still prints.
const QUIET = process.argv.includes("--quiet") || process.env.EXXAT_SYNC_EXTRAS_QUIET === "1"
function log(...m) {
  if (!QUIET) console.log(...m)
}

// `--no-postinstall` opts out of the package.json auto-wire that adds
// `exxat-ui sync-extras --quiet` to the consumer's `postinstall` script.
const SKIP_POSTINSTALL =
  process.argv.includes("--no-postinstall") ||
  process.env.EXXAT_SYNC_EXTRAS_NO_POSTINSTALL === "1"

const SKIP_UPGRADE_NOTICE =
  process.argv.includes("--no-upgrade-notice") ||
  process.env.EXXAT_SYNC_EXTRAS_SKIP_UPGRADE_NOTICE === "1"

const SOURCES = {
  skills:    join(pkgRoot, "consumer-extras", "cursor-skills"),
  rules:     join(pkgRoot, "consumer-extras", "cursor-rules"),
  patterns:  join(pkgRoot, "consumer-extras", "patterns"),
  handbook:  join(pkgRoot, "consumer-extras", "handbook"),
  templates: join(pkgRoot, "consumer-extras", "templates"),
  hooks:     join(pkgRoot, "consumer-extras", "cursor-hooks"),
}

const DESTINATIONS = {
  cursorSkills:  join(cwd, ".cursor", "skills"),
  claudeSkills:  join(cwd, ".claude", "skills"),
  cursorRules:   join(cwd, ".cursor", "rules"),
  cursorHooks:   join(cwd, ".cursor", "hooks"),
  cursorHooksFile: join(cwd, ".cursor", "hooks.json"),
  claudeHooks:   join(cwd, ".claude", "hooks"),
  claudeSettingsFile: join(cwd, ".claude", "settings.json"),
  patterns:      join(cwd, "docs", "exxat-ds"),
  handbook:      join(cwd, "docs", "exxat-ds", "handbook"),
  templates:     join(cwd, "docs", "exxat-ds", "templates"),
}

let wrote = 0
let skipped = 0
let cursorHooksTouched = false
let claudeHooksTouched = false

/** Replace `dest` with `src` (directory) — preserves only what we own. */
function replaceDir(src, dest) {
  if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
  cpSync(src, dest, { recursive: true })
}

/**
 * Skills are bundled as directories like `cursor-skills/exxat-board-cards/SKILL.md`.
 * Copy every `exxat-*` directory into the destination, replacing any prior copy.
 * The same source tree is written to BOTH Cursor and Claude (identical SKILL.md
 * shape, two readers) so one upgrade lights up both clients.
 */
function syncSkillsTo(label, dest) {
  if (!existsSync(SOURCES.skills)) {
    console.error("[sync-extras] missing packaged skills:", SOURCES.skills)
    process.exit(1)
  }
  mkdirSync(dest, { recursive: true })
  for (const name of readdirSync(SOURCES.skills)) {
    const src = join(SOURCES.skills, name)
    if (!statSync(src).isDirectory()) continue
    if (!name.startsWith("exxat-")) continue
    replaceDir(src, join(dest, name))
    log(`[sync-extras] wrote ${label}/${name}/`)
    wrote++
  }
}

/**
 * Rules ship as flat `.mdc` files under `consumer-extras/cursor-rules/`.
 * Only files starting with `exxat-` are copied so the consumer's own rules
 * (and any other vendor's rules) survive an upgrade untouched.
 */
function syncRules() {
  if (!existsSync(SOURCES.rules)) {
    console.warn("[sync-extras] no packaged cursor-rules (optional)")
    skipped++
    return
  }
  mkdirSync(DESTINATIONS.cursorRules, { recursive: true })
  for (const name of readdirSync(SOURCES.rules)) {
    const src = join(SOURCES.rules, name)
    if (!statSync(src).isFile()) continue
    if (!name.startsWith("exxat-") || !name.endsWith(".mdc")) continue
    const content = rewriteConsumerRule(readFileSync(src, "utf8"), { filename: name })
    writeFileSync(join(DESTINATIONS.cursorRules, name), content)
    log(`[sync-extras] wrote .cursor/rules/${name}`)
    wrote++
  }
}

/**
 * Pattern docs (data-views-pattern, command-menu-pattern, …) + the
 * `consumer-upgrade-checklist.md` land flat in `docs/exxat-ds/`.
 */
function syncPatterns() {
  if (!existsSync(SOURCES.patterns)) {
    console.warn("[sync-extras] no packaged patterns (optional)")
    skipped++
    return
  }
  mkdirSync(DESTINATIONS.patterns, { recursive: true })
  for (const name of readdirSync(SOURCES.patterns)) {
    const src = join(SOURCES.patterns, name)
    if (!statSync(src).isFile()) continue
    cpSync(src, join(DESTINATIONS.patterns, name))
    log(`[sync-extras] wrote docs/exxat-ds/${name}`)
    wrote++
  }
}

/**
 * Handbook tier (HANDBOOK · glossary · voice-and-tone · reference-implementations)
 * lands in a nested `docs/exxat-ds/handbook/` so the patterns dir stays flat
 * and the handbook's internal cross-links resolve.
 *
 * The bundled handbook files are stage-rewritten copies of the workspace
 * source (`apps/web/docs/*.md`) — links to shipped patterns use `../`, links
 * to unshipped neighbours (blueprints, token-taxonomy, etc.) are absolute
 * GitHub URLs so they continue to work in the consumer's repo.
 */
function syncHandbook() {
  if (!existsSync(SOURCES.handbook)) {
    console.warn("[sync-extras] no packaged handbook (optional)")
    skipped++
    return
  }
  mkdirSync(DESTINATIONS.handbook, { recursive: true })
  for (const name of readdirSync(SOURCES.handbook)) {
    const src = join(SOURCES.handbook, name)
    if (!statSync(src).isFile()) continue
    cpSync(src, join(DESTINATIONS.handbook, name))
    log(`[sync-extras] wrote docs/exxat-ds/handbook/${name}`)
    wrote++
  }
}

/**
 * Templates (handoff.md etc.) land in `docs/exxat-ds/templates/` next to the
 * patterns + handbook. The agent fills them in via the `exxat-ux-discovery-protocol`
 * rule at the close of any design task.
 */
function syncTemplates() {
  if (!existsSync(SOURCES.templates)) {
    console.warn("[sync-extras] no packaged templates (optional)")
    skipped++
    return
  }
  mkdirSync(DESTINATIONS.templates, { recursive: true })
  for (const name of readdirSync(SOURCES.templates)) {
    const src = join(SOURCES.templates, name)
    if (!statSync(src).isFile()) continue
    cpSync(src, join(DESTINATIONS.templates, name))
    log(`[sync-extras] wrote docs/exxat-ds/templates/${name}`)
    wrote++
  }
}

/**
 * Brief-gate hooks for Cursor + Claude:
 *   - Cursor: deploys `.cursor/hooks/exxat-*.mjs` + merges `.cursor/hooks.json`
 *   - Claude: deploys `.claude/hooks/exxat-*.mjs` + merges `.claude/settings.json`
 *
 * Merge rule: any existing entry whose `command` path starts with
 * `.cursor/hooks/exxat-` is REPLACED with our shipped version. Every other
 * entry (consumer's own hooks) is preserved untouched. Net result: upgrading
 * the package always lands the latest exxat hooks; consumer customisations
 * outside the `exxat-` prefix survive.
 */
function syncHooks() {
  if (!existsSync(SOURCES.hooks)) {
    console.warn("[sync-extras] no packaged cursor-hooks (optional)")
    skipped++
    return
  }

  // 1. Copy script files to BOTH `.cursor/hooks/` and `.claude/hooks/`.
  mkdirSync(DESTINATIONS.cursorHooks, { recursive: true })
  mkdirSync(DESTINATIONS.claudeHooks, { recursive: true })
  for (const name of readdirSync(SOURCES.hooks)) {
    const src = join(SOURCES.hooks, name)
    if (!statSync(src).isFile()) continue
    if (name === "hooks.json") continue
    if (!name.startsWith("exxat-")) continue
    const newContent = readFileSync(src, "utf8")

    const cursorDest = join(DESTINATIONS.cursorHooks, name)
    let prevCursorContent = ""
    try { prevCursorContent = readFileSync(cursorDest, "utf8") } catch { /* new file */ }
    cpSync(src, cursorDest)
    try { chmodSync(cursorDest, 0o755) } catch { /* best-effort on non-Unix */ }
    if (prevCursorContent !== newContent) cursorHooksTouched = true

    const claudeDest = join(DESTINATIONS.claudeHooks, name)
    let prevClaudeContent = ""
    try { prevClaudeContent = readFileSync(claudeDest, "utf8") } catch { /* new file */ }
    cpSync(src, claudeDest)
    try { chmodSync(claudeDest, 0o755) } catch { /* best-effort on non-Unix */ }
    if (prevClaudeContent !== newContent) claudeHooksTouched = true

    log(`[sync-extras] wrote .cursor/hooks/${name}`)
    log(`[sync-extras] wrote .claude/hooks/${name}`)
    wrote++
  }

  // 2. Merge hooks.json — preserve consumer entries; replace exxat-* entries.
  const shipped = JSON.parse(readFileSync(join(SOURCES.hooks, "hooks.json"), "utf8"))
  let merged
  if (existsSync(DESTINATIONS.cursorHooksFile)) {
    try {
      merged = JSON.parse(readFileSync(DESTINATIONS.cursorHooksFile, "utf8"))
    } catch {
      merged = { version: 1, hooks: {} }
    }
  } else {
    merged = { version: 1, hooks: {} }
  }
  merged.version = shipped.version ?? merged.version ?? 1
  merged.hooks = merged.hooks ?? {}

  for (const [event, shippedEntries] of Object.entries(shipped.hooks ?? {})) {
    const existing = Array.isArray(merged.hooks[event]) ? merged.hooks[event] : []
    // Drop any prior exxat-* entries for this event so we replace cleanly.
    const consumerOwned = existing.filter(entry => {
      const cmd = typeof entry?.command === "string" ? entry.command : ""
      return !cmd.includes("/exxat-") && !cmd.includes("exxat-brief-gate")
    })
    merged.hooks[event] = [...consumerOwned, ...shippedEntries]
  }

  const nextHooksJson = JSON.stringify(merged, null, 2) + "\n"
  let prevHooksJson = ""
  try { prevHooksJson = readFileSync(DESTINATIONS.cursorHooksFile, "utf8") } catch { /* new file */ }
  if (prevHooksJson !== nextHooksJson) cursorHooksTouched = true
  writeFileSync(DESTINATIONS.cursorHooksFile, nextHooksJson)
  log(`[sync-extras] merged .cursor/hooks.json (exxat brief-gate wired)`)
  wrote++

  // 3. Merge Claude settings.json — preserve consumer hooks; replace exxat hook entries.
  let claudeSettings
  if (existsSync(DESTINATIONS.claudeSettingsFile)) {
    try {
      claudeSettings = JSON.parse(readFileSync(DESTINATIONS.claudeSettingsFile, "utf8"))
    } catch {
      claudeSettings = {}
    }
  } else {
    claudeSettings = {}
  }

  claudeSettings.hooks = claudeSettings.hooks ?? {}
  const existingPreToolUse = Array.isArray(claudeSettings.hooks.PreToolUse)
    ? claudeSettings.hooks.PreToolUse
    : []
  const consumerOwnedPreToolUse = existingPreToolUse.filter(entry => {
    const asText = JSON.stringify(entry ?? {})
    return !asText.includes("exxat-brief-gate") && !asText.includes("/exxat-")
  })

  const exxatClaudePreToolUseEntry = {
    matcher: "Write|Edit|MultiEdit|NotebookEdit",
    hooks: [{ type: "command", command: "node .claude/hooks/exxat-brief-gate.mjs" }],
  }
  claudeSettings.hooks.PreToolUse = [...consumerOwnedPreToolUse, exxatClaudePreToolUseEntry]

  const nextClaudeSettings = JSON.stringify(claudeSettings, null, 2) + "\n"
  let prevClaudeSettings = ""
  try { prevClaudeSettings = readFileSync(DESTINATIONS.claudeSettingsFile, "utf8") } catch { /* new file */ }
  if (prevClaudeSettings !== nextClaudeSettings) claudeHooksTouched = true
  writeFileSync(DESTINATIONS.claudeSettingsFile, nextClaudeSettings)
  log(`[sync-extras] merged .claude/settings.json (exxat brief-gate wired)`)
  wrote++
}

/**
 * Idempotent: wire `postinstall: exxat-ui sync-extras --quiet || true` into
 * the consumer's `package.json` so each subsequent `pnpm install` /
 * `npm install` refreshes skills + rules + hooks automatically. Customers
 * who already have a different `postinstall` keep theirs untouched — we
 * print a warning telling them how to chain ours.
 *
 * Skipped if:
 *   - The user passed `--no-postinstall` or set `EXXAT_SYNC_EXTRAS_NO_POSTINSTALL=1`.
 *   - No `package.json` exists in the working directory.
 *   - The existing `postinstall` already invokes `exxat-ui sync-extras`.
 */
function wirePostinstall() {
  if (SKIP_POSTINSTALL) return

  const pkgPath = join(cwd, "package.json")
  if (!existsSync(pkgPath)) {
    log("[sync-extras] no package.json in cwd — skipping postinstall wire")
    return
  }

  let raw, pkg, indent = 2, trailingNewline = false
  try {
    raw = readFileSync(pkgPath, "utf8")
    pkg = JSON.parse(raw)
    // Preserve the file's existing indentation if it's a flat number of spaces.
    const indentMatch = raw.match(/\n(\s+)"/)
    if (indentMatch && /^ +$/.test(indentMatch[1])) indent = indentMatch[1].length
    trailingNewline = raw.endsWith("\n")
  } catch (err) {
    console.warn(`[sync-extras] could not parse package.json — skipping postinstall wire (${err.message})`)
    return
  }

  pkg.scripts = pkg.scripts ?? {}
  const existing = pkg.scripts.postinstall
  const ours = "exxat-ui sync-extras --quiet || true"

  if (!existing) {
    pkg.scripts.postinstall = ours
    writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + (trailingNewline ? "\n" : ""))
    log("[sync-extras] wired package.json postinstall (auto-refresh on each install)")
    wrote++
    return
  }

  if (existing.includes("exxat-ui sync-extras")) {
    // Already wired (possibly by template-vite or a prior run). No-op.
    return
  }

  // The consumer has their own postinstall. Don't clobber — warn them.
  console.warn(
    `[sync-extras] NOTE: your package.json has an existing "postinstall" script:\n` +
    `              ${existing}\n` +
    `              To keep DS extras fresh on every install, chain ours:\n` +
    `              "postinstall": "${existing} && ${ours}"\n` +
    `              (or pass --no-postinstall to suppress this notice)`,
  )
}

function warnIfShellPortPending() {
  if (SKIP_UPGRADE_NOTICE) return
  const script = resolve(__dirname, "upgrade.mjs")
  if (!existsSync(script)) return
  const res = spawnSync(process.execPath, [script, "--check", "--quiet"], {
    cwd,
    encoding: "utf8",
  })
  if (res.status !== 10) return
  console.log("")
  console.log("┌──────────────────────────────────────────────────────────────────────────┐")
  console.log("│  ⚠  Shell upgrade still pending.                                        │")
  console.log("│                                                                          │")
  console.log("│     Package install updated node_modules and synced .cursor extras,      │")
  console.log("│     but your app still owns some copied shell files (routes/nav/sidebar).│")
  console.log("│                                                                          │")
  console.log("│     Run now:                                                             │")
  console.log("│       npx --package=@exxatdesignux/ui@latest exxat-ui upgrade            │")
  console.log("│                                                                          │")
  console.log("│     That ports safe template shell files and reports any manual merges.  │")
  console.log("└──────────────────────────────────────────────────────────────────────────┘")
  console.log("")
}

syncSkillsTo(".cursor/skills", DESTINATIONS.cursorSkills)
syncSkillsTo(".claude/skills", DESTINATIONS.claudeSkills)
syncRules()
syncPatterns()
syncHandbook()
syncTemplates()
syncHooks()
wirePostinstall()
warnIfShellPortPending()

console.log(`[sync-extras] Done. ${wrote} files / dirs written; ${skipped} optional sources missing.`)
if (!QUIET) console.log("[sync-extras] Product routes and app content were not modified.")

if (cursorHooksTouched) {
  // Loud, hard-to-miss banner. Cursor only loads `.cursor/hooks.json` at
  // session start, so the brief-gate stays DORMANT until the running window
  // is restarted. Without this notice the user assumes the hook is live and
  // is then surprised when the agent edits design-critical files freely.
  console.log("")
  console.log("┌──────────────────────────────────────────────────────────────────────────┐")
  console.log("│  ⚠  Cursor hooks were updated.                                           │")
  console.log("│                                                                          │")
  console.log("│     Restart Cursor (Cmd+Q, then reopen) to load the brief-gate.          │")
  console.log("│     Otherwise the agent will still edit design-critical files freely.    │")
  console.log("│                                                                          │")
  console.log("│     Verify after restart:                                                │")
  console.log("│       npx --package=@exxatdesignux/ui@latest exxat-ui doctor             │")
  console.log("└──────────────────────────────────────────────────────────────────────────┘")
  console.log("")
}

if (claudeHooksTouched) {
  console.log("")
  console.log("┌──────────────────────────────────────────────────────────────────────────┐")
  console.log("│  ⚠  Claude hooks were updated.                                           │")
  console.log("│                                                                          │")
  console.log("│     Restart Claude Code session to load the brief-gate hook.             │")
  console.log("│     Otherwise design-critical edits may bypass the checkpoint.            │")
  console.log("│                                                                          │")
  console.log("│     Verify after restart:                                                │")
  console.log("│       npx --package=@exxatdesignux/ui@latest exxat-ui doctor             │")
  console.log("└──────────────────────────────────────────────────────────────────────────┘")
  console.log("")
}
