#!/usr/bin/env bash
set -euo pipefail

repo="${1:-$(pwd)}"
repo="$(cd "$repo" && pwd)"

if [[ ! -d "$repo" ]]; then
  echo "repo not found: $repo" >&2
  exit 2
fi

if ! command -v node >/dev/null 2>&1; then
  echo "node is required" >&2
  exit 2
fi

encode_claude_project() {
  local path="$1"
  path="${path%/}"
  printf '%s' "$path" | sed 's#/#-#g'
}

copy_tree_children_overwrite() {
  local src="$1"
  local dst="$2"
  [[ -d "$src" ]] || return 0
  mkdir -p "$dst"
  shopt -s nullglob dotglob
  local item name
  for item in "$src"/*; do
    name="$(basename "$item")"
    rm -rf "$dst/$name"
    cp -a "$item" "$dst/"
  done
  shopt -u nullglob dotglob
}

ensure_skill_frontmatter() {
  local dir="$1"
  [[ -d "$dir" ]] || return 0
  find "$dir" -mindepth 2 -maxdepth 2 -name SKILL.md -print0 | while IFS= read -r -d '' file; do
    if head -n 1 "$file" | grep -qx -- "---"; then
      continue
    fi
    local name tmp
    name="$(basename "$(dirname "$file")")"
    tmp="$(mktemp)"
    {
      printf -- "---\n"
      printf "name: %s\n" "$name"
      printf "description: Imported Claude Code skill. Use when task matches this skill's body or the user names %s.\n" "$name"
      printf -- "---\n\n"
      cat "$file"
    } > "$tmp"
    cp -a "$file" "$file.pre-codex-frontmatter.bak"
    mv "$tmp" "$file"
  done
}

git_ignores() {
  local rel="$1"
  git -C "$repo" check-ignore -q "$rel" 2>/dev/null
}

claude_project="$(encode_claude_project "$repo")"
claude_memory_dir="${CLAUDE_PROJECT_MEMORY_DIR:-$HOME/.claude/projects/$claude_project/memory}"

mkdir -p "$repo/.codex"
copy_tree_children_overwrite "$repo/.claude/skills" "$repo/.codex/skills"
copy_tree_children_overwrite "$repo/.claude/agents" "$repo/.codex/agents"
ensure_skill_frontmatter "$repo/.codex/skills"

cat > "$repo/.codex/.gitignore" <<'EOF'
sessions/
logs/
!*/
!*.md
!*.json
!*.jsonl
!*.mjs
!*.sh
!SKILL.original.md
!scripts/
!scripts/**
EOF

mkdir -p "$repo/.agents"
cat > "$repo/.agents/.gitignore" <<'EOF'
!*/
!*.md
!*.json
!*.jsonl
!*.mjs
!*.sh
!SKILL.original.md
!scripts/
!scripts/**
EOF

kb_rel="docs/memory-kb"
search_rel="scripts/project-memory-search.mjs"
if git_ignores "docs/memory-kb/entries.jsonl" || git_ignores "scripts/project-memory-search.mjs"; then
  kb_rel=".agents/memory-kb"
  search_rel=".agents/project-memory-search.mjs"
  if [[ -f "$repo/docs/memory-kb/README.md" ]] && grep -q "Project Memory KB" "$repo/docs/memory-kb/README.md"; then
    rm -rf "$repo/docs/memory-kb"
  fi
  if [[ -f "$repo/scripts/project-memory-search.mjs" ]] && grep -q "project-memory-search" "$repo/scripts/project-memory-search.mjs"; then
    rm -f "$repo/scripts/project-memory-search.mjs"
  fi
  if [[ -f "$repo/.codex/memory-kb/README.md" ]] && grep -q "Project Memory KB" "$repo/.codex/memory-kb/README.md"; then
    rm -rf "$repo/.codex/memory-kb"
  fi
  if [[ -f "$repo/.codex/project-memory-search.mjs" ]] && grep -q "project-memory-search" "$repo/.codex/project-memory-search.mjs"; then
    rm -f "$repo/.codex/project-memory-search.mjs"
  fi
fi

mkdir -p "$repo/$kb_rel" "$(dirname "$repo/$search_rel")"

node - "$repo" "$claude_memory_dir" "$kb_rel" "$search_rel" <<'NODE'
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const repo = process.argv[2];
const source = process.argv[3];
const kbRel = process.argv[4];
const searchRel = process.argv[5];
const outDir = path.join(repo, kbRel);
fs.mkdirSync(outDir, { recursive: true });

const stop = new Set(
  "the a an and or but for from with without into onto over under this that these those are was were has have had not never always must should when where what why how use used using before after then than any all one two per via its it is in on of to by as at be do does did can could would will you your our we they their if else no yes done next state run".split(/\s+/),
);

function listMemoryFiles(dir) {
  if (!fs.existsSync(dir)) return [];
  return fs
    .readdirSync(dir)
    .filter((file) => file.endsWith(".md"))
    .sort()
    .map((file) => path.join(dir, file))
    .filter((file) => fs.statSync(file).isFile());
}

function words(value) {
  return [
    ...new Set(
      (value.toLowerCase().match(/[a-z0-9][a-z0-9-]{2,}/g) || [])
        .filter((word) => !stop.has(word))
        .slice(0, 80),
    ),
  ];
}

function titleFrom(file, text) {
  const first = text
    .split(/\r?\n/)
    .map((line) => line.trim())
    .find(Boolean);
  if (first && first.length < 140 && !first.startsWith("---")) {
    return first.replace(/^#+\s*/, "");
  }
  return path.basename(file, ".md").replace(/-/g, " ");
}

function kind(file, text) {
  const name = path.basename(file).toLowerCase();
  const haystack = text.toLowerCase();
  if (name.includes("feedback")) return "feedback-rule";
  if (name.includes("run-state") || name.includes("runstate") || haystack.includes("run state")) return "run-state";
  if (name.includes("decision") || name.includes("disposition") || name.includes("rule") || name.includes("mandate")) return "decision";
  if (name.includes("program") || name.includes("plan")) return "program-plan";
  if (name.includes("gotcha") || name.includes("flake") || name.includes("gap")) return "gotcha";
  return "memory";
}

function tags(file, text) {
  const haystack = `${file} ${text}`.toLowerCase();
  const candidates = [
    "billing",
    "commerce",
    "checkout",
    "marketplace",
    "orders",
    "cms",
    "theme",
    "i18n",
    "translation",
    "auth",
    "db",
    "cache",
    "uploads",
    "realtime",
    "react",
    "ui",
    "a11y",
    "accessibility",
    "typecheck",
    "test",
    "playwright",
    "vitest",
    "pnpm",
    "git",
    "github",
    "deploy",
    "cloudflare",
    "worker",
    "registry",
    "module",
    "spec",
    "plan",
    "orchestrator",
    "subagent",
    "cursor",
    "worktree",
    "security",
    "audit",
    "mvcc",
    "inventory",
    "ledger",
    "affiliate",
    "referral",
    "template",
    "distro",
    "blueprint",
    "memory",
  ];
  return candidates.filter((candidate) => haystack.includes(candidate));
}

const files = listMemoryFiles(source);
const entries = files.map((file) => {
  const text = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
  const id = path.basename(file, ".md");
  return {
    id,
    title: titleFrom(file, text),
    kind: kind(file, text),
    tags: tags(file, text),
    keywords: words(`${file}\n${text}`),
    source: file,
    imported_at: new Date().toISOString(),
    sha256: crypto.createHash("sha256").update(text).digest("hex"),
    lines: text.split("\n").length,
    text,
  };
});

fs.writeFileSync(path.join(outDir, "entries.jsonl"), entries.map((entry) => JSON.stringify(entry)).join("\n") + (entries.length ? "\n" : ""));
fs.writeFileSync(
  path.join(outDir, "manifest.json"),
  JSON.stringify(
    {
      source,
      generated_at: new Date().toISOString(),
      count: entries.length,
      files: entries.map(({ id, title, kind, tags, source, sha256, lines }) => ({
        id,
        title,
        kind,
        tags,
        source,
        sha256,
        lines,
      })),
    },
    null,
    2,
  ) + "\n",
);

fs.writeFileSync(
  path.join(outDir, "README.md"),
  `# Project Memory KB

Audience: AI coding agents first.

Source: Claude project memory from \`${source}\`.

Files:

- \`entries.jsonl\` — full memory records. One JSON object per memory file.
- \`manifest.json\` — import metadata, source hashes, tags, line counts.
- \`${searchRel}\` — context-cheap query tool.

Use:

\`\`\`bash
node ${searchRel} "query terms" --limit 8
\`\`\`

Do not read \`entries.jsonl\` directly unless editing the KB. Query first; open source memory only when a hit must be audited verbatim.
`,
);

console.log(`memory_source=${source}`);
console.log(`memory_kb=${kbRel}`);
console.log(`memory_search=${searchRel}`);
console.log(`memory_records=${entries.length}`);
NODE

cat > "$repo/$search_rel" <<'NODE'
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const here = path.dirname(fileURLToPath(import.meta.url));
const candidates = [
  path.join(root, "docs", "memory-kb", "entries.jsonl"),
  path.join(root, ".agents", "memory-kb", "entries.jsonl"),
  path.join(root, ".codex", "memory-kb", "entries.jsonl"),
  path.join(here, "memory-kb", "entries.jsonl"),
];
const kbPath = candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];

function usage() {
  console.error("Usage: node scripts/project-memory-search.mjs <query> [--limit N]");
  process.exit(2);
}

const args = process.argv.slice(2);
const limitFlag = args.indexOf("--limit");
let limit = 8;
if (limitFlag !== -1) {
  const raw = args[limitFlag + 1];
  limit = Number.parseInt(raw ?? "", 10);
  args.splice(limitFlag, 2);
  if (!Number.isFinite(limit) || limit < 1) usage();
}

const query = args.join(" ").trim();
if (!query) usage();

const terms = [...new Set(query.toLowerCase().match(/[a-z0-9][a-z0-9-]{1,}/g) ?? [])];
if (!terms.length) usage();

function readEntries() {
  if (!fs.existsSync(kbPath)) return [];
  const raw = fs.readFileSync(kbPath, "utf8").trim();
  if (!raw) return [];
  return raw.split("\n").map((line) => JSON.parse(line));
}

function count(haystack, term) {
  let total = 0;
  let offset = 0;
  while (true) {
    const next = haystack.indexOf(term, offset);
    if (next === -1) return total;
    total += 1;
    offset = next + term.length;
  }
}

function score(entry) {
  const title = `${entry.id} ${entry.title}`.toLowerCase();
  const tags = `${entry.kind} ${(entry.tags ?? []).join(" ")}`.toLowerCase();
  const keywords = (entry.keywords ?? []).join(" ").toLowerCase();
  const text = (entry.text ?? "").toLowerCase();
  let value = 0;
  for (const term of terms) {
    value += count(title, term) * 20;
    value += count(tags, term) * 12;
    value += count(keywords, term) * 8;
    value += count(text, term);
  }
  return value;
}

function snippet(entry) {
  const lines = String(entry.text ?? "").split("\n");
  const scored = lines
    .map((line, index) => ({
      line,
      index,
      hits: terms.reduce((sum, term) => sum + count(line.toLowerCase(), term), 0),
    }))
    .filter((row) => row.hits > 0)
    .sort((a, b) => b.hits - a.hits || a.index - b.index);

  const selected = scored.slice(0, 3);
  if (!selected.length) {
    return lines
      .map((line) => line.trim())
      .filter(Boolean)
      .slice(0, 2)
      .join(" ");
  }
  return selected.map((row) => `L${row.index + 1}: ${row.line.trim()}`).join("\n");
}

const results = readEntries()
  .map((entry) => ({ entry, score: score(entry) }))
  .filter((row) => row.score > 0)
  .sort((a, b) => b.score - a.score || a.entry.id.localeCompare(b.entry.id))
  .slice(0, limit);

if (!results.length) {
  console.log(`No project memory hits for: ${query}`);
  process.exit(0);
}

for (const { entry, score: value } of results) {
  console.log(`## ${entry.id}  score=${value}`);
  console.log(`title: ${entry.title}`);
  console.log(`kind: ${entry.kind}`);
  console.log(`tags: ${(entry.tags ?? []).join(", ") || "(none)"}`);
  console.log(`source: ${entry.source}`);
  console.log(snippet(entry));
  console.log("");
}
NODE
chmod +x "$repo/$search_rel"

agents="$repo/AGENTS.md"
touch "$agents"
node - "$agents" "$search_rel" <<'NODE'
const fs = require("fs");
const file = process.argv[2];
const searchRel = process.argv[3];
const start = "<!-- cc2codex-memory-kb:start -->";
const end = "<!-- cc2codex-memory-kb:end -->";
const block = `${start}
# Project Memory KB

Audience: AI coding agents first.

Before decisions on prior work, old failures, module disposition, run state, deployment, worktrees, gates, project doctrine, or user feedback patterns, query project memory:

\`\`\`bash
node ${searchRel} "<keywords>" --limit 8
\`\`\`

Trigger keywords/topics: \`memory\`, \`remember\`, \`previous\`, \`before\`, \`again\`, \`run state\`, \`gotcha\`, \`feedback\`, \`decision\`, \`disposition\`, \`module\`, \`platform\`, \`deploy\`, \`Cloudflare\`, \`GHP\`, \`worktree\`, \`gate\`, \`typecheck\`, \`Playwright\`, \`Vitest\`, \`pnpm\`, \`registry\`, \`orchestrator\`, \`subagent\`, \`audit\`, \`security\`, \`MVCC\`.

Use search results as routing facts, not as automatic truth. If a hit affects code or spec, open the cited source/memory and verify current repo state before acting.
${end}`;

const current = fs.readFileSync(file, "utf8");
const pattern = new RegExp(`${start}[\\s\\S]*?${end}\\n?`);
const legacyPattern = /# Project Memory KB\n\nAudience: AI coding agents first\.\n\nBefore decisions on prior work, old failures, module disposition, run state, deployment, worktrees, gates, (?:project|platform) doctrine, or user feedback patterns, query project memory:\n\n```bash\nnode scripts\/project-memory-search\.mjs "<keywords>" --limit 8\n```\n\nTrigger keywords\/topics: [\s\S]*?\n\nUse search results as routing facts, not as automatic truth\. If a hit affects code or spec, open the cited source\/memory and verify current repo state before acting\.\n\n?/g;
const cleaned = current.replace(pattern, "").replace(legacyPattern, "");
let next;
if (pattern.test(current)) {
  next = `${block}\n\n${cleaned}`;
} else {
  next = `${block}\n\n${cleaned}`;
}
fs.writeFileSync(file, next);
NODE

skill_count="$(find "$repo/.codex/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')"
agent_count="$(find "$repo/.codex/agents" -mindepth 1 -maxdepth 2 -type f 2>/dev/null | wc -l | tr -d ' ')"
memory_count="$(node -e "const fs=require('fs'); const p='$repo/$kb_rel/manifest.json'; console.log(JSON.parse(fs.readFileSync(p,'utf8')).count)")"

echo "repo=$repo"
echo "codex_skills=$skill_count"
echo "codex_agent_files=$agent_count"
echo "memory_kb=$kb_rel"
echo "memory_search=$search_rel"
echo "memory_records=$memory_count"
echo "updated=$repo/AGENTS.md"
