#!/usr/bin/env node
/**
 * Scaffold a new Exxat DS app from the bundled template.
 *
 * Usage:
 *   npx create-exxat-app                 # scaffold into current directory
 *   npx create-exxat-app my-app          # scaffold into ./my-app (created if missing)
 *   npx create-exxat-app . --force       # overwrite-merge into current directory
 *   npx create-exxat-app my-app --no-install   # skip the package install step
 *
 * Also runs as `npx --package=@exxatdesignux/ui create-exxat-app …` and as
 * `pnpm create exxat-app …` when invoked via the standalone create-exxat-app
 * discovery shim.
 */
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"
import { resolve, dirname, basename, join } from "node:path"
import { fileURLToPath } from "node:url"
import { execSync } from "node:child_process"

const __dirname = dirname(fileURLToPath(import.meta.url))
const selfPkgPath = resolve(__dirname, "../package.json")
const selfPkg = JSON.parse(readFileSync(selfPkgPath, "utf8"))

const args = process.argv.slice(2)
const allFlags = args.filter((a) => a.startsWith("--"))
const flags = new Set(allFlags.map((f) => (f.includes("=") ? f.slice(0, f.indexOf("=")) : f)))
const positional = args.filter((a) => !a.startsWith("--"))
const targetArg = positional[0] ?? "."
const targetDir = resolve(process.cwd(), targetArg)

// `template-vite/` is the canonical scaffold source. The `template/`
// (Next.js) directory was retired alongside the apps/web Next removal
// (PR-6, May 2026); customers who need a Next scaffold can copy
// `template-vite/` and migrate it.
const templateDirName = "template-vite"
const templateDir = resolve(__dirname, `../${templateDirName}`)
if (!existsSync(templateDir)) {
  console.error(`❌  Template directory ${templateDirName}/ not found in this @exxatdesignux/ui release.`)
  console.error(`    Try upgrading: npm i @exxatdesignux/ui@latest`)
  process.exit(1)
}
// `displayName` is what we show in messages and use to derive package.json
// `name`. For "." or an absolute path we just use the basename; for a
// relative path the user typed (`my-app`, `apps/sandbox`) we keep it as-is.
const isAbsolute = targetArg.startsWith("/") || /^[a-zA-Z]:\\/.test(targetArg)
const displayName = targetArg === "." || isAbsolute ? basename(targetDir) : targetArg
// `cdHint` is what we tell the user to `cd` into. If they passed an
// absolute path, that's the literal path; otherwise the relative arg.
const cdHint = isAbsolute ? targetDir : displayName
const force = flags.has("--force")
const skipInstall = flags.has("--no-install") || flags.has("--skip-install")
const skipExtras = flags.has("--no-extras") || flags.has("--skip-extras")

if (flags.has("--help") || flags.has("-h")) {
  console.log(`
create-exxat-app — scaffold a starter app on @exxatdesignux/ui

Stack: Vite + React + react-router-dom (low-memory, fast HMR).

Usage:
  create-exxat-app [target-directory] [options]

Arguments:
  target-directory    Where to create the app (default: ".")
                      A relative or absolute path. Created if it does not exist.

Options:
  --force             Allow scaffolding into a non-empty directory (existing
                      files are NOT overwritten unless template ships the
                      same path).
  --no-install        Skip the dependency install step.
  --no-extras         Skip auto-syncing Cursor / Claude agent skills, binding
                      rules, and pattern docs. You can run them later with
                      \`exxat-ui sync-extras\`.
  -h, --help          Show this message.

Examples:
  create-exxat-app my-app
  create-exxat-app .
  create-exxat-app ../sandbox --force --no-install
`)
  process.exit(0)
}

if (!existsSync(targetDir)) {
  mkdirSync(targetDir, { recursive: true })
  console.log(`📁  Created ${displayName}/`)
}

const targetStat = statSync(targetDir)
if (!targetStat.isDirectory()) {
  console.error(`❌  Target ${targetDir} exists but is not a directory.`)
  process.exit(1)
}

const existing = readdirSync(targetDir)
const ignorable = new Set([".git", ".gitignore", ".DS_Store"])
const blockers = existing.filter((name) => !ignorable.has(name))

if (blockers.length > 0 && !force) {
  console.error(`❌  Target directory is not empty (${targetDir}).`)
  console.error("")
  console.error("    Found:")
  for (const name of blockers.slice(0, 8)) console.error(`      - ${name}`)
  if (blockers.length > 8) console.error(`      … and ${blockers.length - 8} more`)
  console.error("")
  console.error("    Options:")
  console.error(`      1. Scaffold into a new folder:   create-exxat-app my-app`)
  console.error(`      2. Allow merging existing files: create-exxat-app . --force`)
  console.error("")
  process.exit(1)
}

console.log(`📦  Copying Exxat DS starter app (Vite) into ${displayName}/ …`)
cpSync(templateDir, targetDir, { recursive: true })

// Pin DS to this CLI's published version so `latest` cache drift cannot
// trick the consumer into installing an older dist.
const targetPkgPath = resolve(targetDir, "package.json")
const targetPkg = JSON.parse(readFileSync(targetPkgPath, "utf8"))
if (targetPkg.dependencies?.["@exxatdesignux/ui"]) {
  targetPkg.dependencies["@exxatdesignux/ui"] = `^${selfPkg.version}`
}
// Name the project after the target dir for nicer dev-server / process titles.
targetPkg.name = displayName.replace(/[^a-z0-9-_]/gi, "-").toLowerCase() || "my-exxat-app"
writeFileSync(targetPkgPath, `${JSON.stringify(targetPkg, null, 2)}\n`)
console.log(`📌  Pinned @exxatdesignux/ui to ^${selfPkg.version} and named the project "${targetPkg.name}".`)

const pm = detectPackageManager()
if (skipInstall) {
  console.log(`⏭   Skipped install (--no-install).`)
} else {
  console.log(`📥  Installing dependencies with ${pm.label} …`)
  try {
    execSync(pm.installCmd, { stdio: "inherit", cwd: targetDir })
  } catch (err) {
    console.error("")
    console.error(`❌  ${pm.label} install failed. You can retry manually:`)
    console.error(`     cd ${cdHint} && ${pm.installCmd}`)
    process.exit(1)
  }
}

// Drop the full curated bundle (Cursor + Claude skills, binding rules,
// pattern docs, handbook) into the new repo so AI assistants light up on
// first open. The bundle replaces only `exxat-*` namespaced files — the
// consumer's own rules/skills/docs are preserved on every re-run.
if (skipExtras) {
  console.log(`⏭   Skipped agent skills + rules (--no-extras).`)
} else {
  const syncScript = resolve(__dirname, "sync-extras.mjs")
  if (!existsSync(syncScript)) {
    console.warn(`⚠️   Could not find sync-extras at ${syncScript} — skipping.`)
  } else {
    console.log(`🧠  Installing Cursor + Claude agent skills, binding rules, and pattern docs …`)
    try {
      execSync(`node "${syncScript}" --quiet`, { stdio: "inherit", cwd: targetDir })
    } catch (err) {
      console.warn(`⚠️   Agent extras step failed. You can retry manually:`)
      console.warn(`     cd ${cdHint} && ${pm.dlxCmd} --package=@exxatdesignux/ui@${selfPkg.version} exxat-ui sync-extras`)
    }
  }
}

console.log("")
console.log(`✅  Done! Your Exxat DS app is ready in ${isAbsolute ? targetDir : `./${displayName}/`}`)
console.log("")
if (targetArg !== ".") console.log(`   cd ${cdHint}`)
console.log(`   ${pm.runCmd} dev     # start dev server`)
console.log("")
console.log("   Cursor / Claude skills, rules, and pattern docs are already in place.")
console.log("   When you upgrade @exxatdesignux/ui later, refresh them with:")
console.log(`     ${pm.dlxCmd} --package=@exxatdesignux/ui@latest exxat-ui sync-extras`)
console.log("")

/**
 * Detect the package manager that invoked us — npm_config_user_agent is set
 * by every modern package-runner (npm, pnpm, yarn, bun) and looks like
 * "pnpm/10.33.0 npm/? node/v22.0.0 darwin arm64".
 *
 * When the caller is npm/npx (the default for
 * `npx --package=@exxatdesignux/ui@latest exxat-ui init …`) we PREFER pnpm
 * if it's on PATH. Rationale:
 *
 *   • npm's transitive resolver picks `mute-stream@4.0.0` (engines
 *     `^22.22.2 || ^24.15.0 || >=26.0.0`) for the inquirer chain inside
 *     `shadcn` + `vitest` deps. That excludes odd-numbered Node releases
 *     (e.g. Node 25) and triggers `npm warn EBADENGINE …` on install.
 *     pnpm picks the engine-compatible `mute-stream@3.0.0` instead, so
 *     the install stays warning-free.
 *
 *   • pnpm respects `pnpm.allowedDeprecatedVersions` in the scaffold's
 *     `package.json`, which silences the two intentional transitive
 *     deprecations (`node-domexception@1.0.0`, `whatwg-encoding@3.1.1`)
 *     until upstream `jsdom`/`shadcn` upgrade. npm has no equivalent
 *     suppression mechanism.
 *
 *   • Lockfile mixing is the bigger trap. Once we run `pnpm install` the
 *     scaffold has `pnpm-lock.yaml`; the success message then suggests
 *     `pnpm dev` / `pnpm test` so the user stays on pnpm. If a caller
 *     genuinely lacks pnpm (rare in 2026), we fall through to npm.
 */
function detectPackageManager() {
  const ua = process.env.npm_config_user_agent ?? ""
  if (ua.startsWith("pnpm")) {
    return { label: "pnpm", installCmd: "pnpm install", runCmd: "pnpm", dlxCmd: "pnpm dlx" }
  }
  if (ua.startsWith("yarn")) {
    return { label: "yarn", installCmd: "yarn install", runCmd: "yarn", dlxCmd: "yarn dlx" }
  }
  if (ua.startsWith("bun")) {
    return { label: "bun", installCmd: "bun install", runCmd: "bun", dlxCmd: "bunx" }
  }
  // npm / npx invocation — opportunistically upgrade to pnpm.
  try {
    execSync("pnpm --version", { stdio: "ignore" })
    return { label: "pnpm", installCmd: "pnpm install", runCmd: "pnpm", dlxCmd: "pnpm dlx" }
  } catch {
    // pnpm not installed — stay on npm.
  }
  return { label: "npm", installCmd: "npm install", runCmd: "npm run", dlxCmd: "npx" }
}
