#!/usr/bin/env node
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');

const { captureClaudeMemory } = require('../lib/okf-capture');
const { projectOkfToHome } = require('../lib/okf-project');
const { promoteJobToProject } = require('../lib/okf-scope');

const VERBS = new Set(['project', 'capture', 'sync', 'new', 'index', 'promote', 'doctor']);

function printJson(stream, payload) {
  stream.write(`${JSON.stringify(payload)}\n`);
}

function failUsage(message) {
  printJson(process.stderr, { ok: false, code: 2, error: message });
  process.exit(2);
}

function failRuntime(message, extra = {}) {
  printJson(process.stderr, { ok: false, code: 3, error: message, ...extra });
  process.exit(3);
}

function resolveRepoRoot(options) {
  return path.resolve(options.repo || process.cwd());
}

function ensureDir(dirPath) {
  fs.mkdirSync(dirPath, { recursive: true });
}

function atomicWrite(filePath, content) {
  ensureDir(path.dirname(filePath));
  const tmpPath = path.join(
    path.dirname(filePath),
    `.okf.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString('hex')}.tmp`,
  );
  fs.writeFileSync(tmpPath, content, 'utf8');
  fs.renameSync(tmpPath, filePath);
}

function parseArgs(argv) {
  if (argv.length === 0) {
    failUsage('missing verb');
  }

  const [verb, ...rest] = argv;
  if (!VERBS.has(verb)) {
    failUsage(`unknown verb: ${verb}`);
  }

  const options = {};
  for (let index = 0; index < rest.length; index += 1) {
    const token = rest[index];
    if (!token.startsWith('--')) {
      failUsage(`unexpected argument: ${token}`);
    }

    const key = token.slice(2);
    const next = rest[index + 1];
    if (next === undefined || next.startsWith('--')) {
      options[key] = true;
      continue;
    }

    options[key] = next;
    index += 1;
  }

  return { verb, options };
}

function parseFrontmatter(content) {
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/u.exec(content);
  if (!match) {
    return { data: {}, body: content };
  }

  const data = {};
  const lines = match[1].split('\n');
  let index = 0;
  while (index < lines.length) {
    const line = lines[index];
    const tagsMatch = /^tags:\s*$/u.exec(line);
    if (tagsMatch) {
      const tags = [];
      index += 1;
      while (index < lines.length) {
        const tagMatch = /^  -\s*(.+)\s*$/u.exec(lines[index]);
        if (!tagMatch) {
          index -= 1;
          break;
        }
        tags.push(tagMatch[1]);
        index += 1;
      }
      data.tags = tags;
      index += 1;
      continue;
    }

    const fieldMatch = /^([A-Za-z0-9_-]+):\s*(.*)\s*$/u.exec(line);
    if (fieldMatch) {
      data[fieldMatch[1]] = fieldMatch[2];
    }
    index += 1;
  }

  return { data, body: match[2].replace(/^\n/u, '') };
}

function serializeConcept(document) {
  const lines = [
    '---',
    `type: ${document.type}`,
    `title: ${document.title}`,
    `description: ${document.description}`,
  ];

  if (document.tags.length > 0) {
    lines.push('tags:');
    for (const tag of document.tags) {
      lines.push(`  - ${tag}`);
    }
  }

  lines.push('---', '', document.body.replace(/\s+$/u, ''), '');
  return `${lines.join('\n')}\n`;
}

function readConcept(filePath) {
  const parsed = parseFrontmatter(fs.readFileSync(filePath, 'utf8'));
  return {
    filePath,
    title: parsed.data.title || path.basename(filePath, '.md'),
    description: parsed.data.description || '',
    type: parsed.data.type || '',
    tags: Array.isArray(parsed.data.tags) ? parsed.data.tags : [],
    body: parsed.body,
  };
}

function listConceptFiles(dirPath) {
  const entries = fs.existsSync(dirPath) ? fs.readdirSync(dirPath, { withFileTypes: true }) : [];
  const files = [];

  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      files.push(...listConceptFiles(fullPath));
      continue;
    }

    if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'index.md') {
      files.push(fullPath);
    }
  }

  return files.sort();
}

function buildIndexContent(dirPath) {
  const entries = listConceptFiles(dirPath)
    .filter((filePath) => path.dirname(filePath) === dirPath)
    .map((filePath) => {
      const concept = readConcept(filePath);
      return `- [${concept.title}](${path.basename(filePath)}) - ${concept.description}`;
    })
    .sort((left, right) => left.localeCompare(right));

  return entries.length === 0 ? '' : `${entries.join('\n')}\n`;
}

function commandCapture(options) {
  const repoRoot = resolveRepoRoot(options);
  const result = captureClaudeMemory({
    homeDir: path.resolve(process.env.HOME || ''),
    repoRoot,
    runSlug: typeof options['run-slug'] === 'string' ? options['run-slug'] : undefined,
  });

  printJson(process.stdout, {
    ok: true,
    verb: 'capture',
    ingested: result.ingested.length,
    skipped: result.skipped.length,
  });
}

function commandProject(options) {
  const repoRoot = resolveRepoRoot(options);
  const projectName = typeof options['project-name'] === 'string' ? options['project-name'] : path.basename(repoRoot);
  const okfRoot = path.join(repoRoot, '.okf');
  const result = projectOkfToHome({
    homeDir: path.resolve(process.env.HOME || ''),
    projectName,
    projectRoot: repoRoot,
    okfRoot,
  });

  printJson(process.stdout, {
    ok: true,
    verb: 'project',
    changed: Object.values(result.files).filter((file) => file.changed).length,
  });
}

function commandSync(options) {
  commandCapture(options);
  commandProject(options);
}

function commandNew(options) {
  const repoRoot = resolveRepoRoot(options);
  const relativePath = options.path;
  const type = options.type;
  const title = options.title;
  const description = options.description;
  const body = options.body;

  if (typeof relativePath !== 'string' || relativePath.length === 0) failUsage('new requires --path');
  if (typeof type !== 'string' || type.length === 0) failUsage('new requires --type');
  if (typeof title !== 'string' || title.length === 0) failUsage('new requires --title');
  if (typeof description !== 'string' || description.length === 0) failUsage('new requires --description');
  if (typeof body !== 'string') failUsage('new requires --body');

  const safeRelativePath = relativePath.replace(/^\/+/u, '');
  const filePath = path.join(repoRoot, '.okf', safeRelativePath);
  const tags = typeof options.tags === 'string' && options.tags.length > 0
    ? options.tags.split(',').map((tag) => tag.trim()).filter(Boolean)
    : [];

  const content = serializeConcept({ type, title, description, tags, body });
  atomicWrite(filePath, content);

  const indexPath = path.join(path.dirname(filePath), 'index.md');
  atomicWrite(indexPath, buildIndexContent(path.dirname(filePath)));

  printJson(process.stdout, { ok: true, verb: 'new', file: filePath });
}

function commandIndex(options) {
  const repoRoot = resolveRepoRoot(options);
  const relativeDir = typeof options.dir === 'string' ? options.dir : '';
  const targetDir = path.join(repoRoot, '.okf', relativeDir);
  ensureDir(targetDir);
  atomicWrite(path.join(targetDir, 'index.md'), buildIndexContent(targetDir));
  printJson(process.stdout, { ok: true, verb: 'index', dir: targetDir });
}

function commandPromote(options) {
  const repoRoot = resolveRepoRoot(options);
  const scopeArg = typeof options.scope === 'string' ? options.scope : '.okf/scope.json';
  const project = options.project;
  const job = options.job;

  if (typeof project !== 'string' || project.length === 0) failUsage('promote requires --project');
  if (typeof job !== 'string' || job.length === 0) failUsage('promote requires --job');

  const scopePath = path.join(repoRoot, scopeArg);
  const current = fs.existsSync(scopePath)
    ? JSON.parse(fs.readFileSync(scopePath, 'utf8'))
    : {};
  const result = promoteJobToProject(current, { project, job });
  atomicWrite(scopePath, `${JSON.stringify(result.scope, null, 2)}\n`);

  printJson(process.stdout, {
    ok: true,
    verb: 'promote',
    promoted: result.promoted,
    tier: result.tier,
  });
}

function doctorChecks(repoRoot) {
  const okfRoot = path.join(repoRoot, '.okf');
  const conceptFiles = listConceptFiles(okfRoot);
  const issues = [];

  for (const filePath of conceptFiles) {
    const concept = readConcept(filePath);
    if (!concept.type) {
      issues.push({ file: filePath, reason: 'missing type' });
    }

    const indexPath = path.join(path.dirname(filePath), 'index.md');
    const expectedLine = `- [${concept.title}](${path.basename(filePath)}) - ${concept.description}`;
    const indexContent = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
    if (!indexContent.includes(expectedLine)) {
      issues.push({ file: filePath, reason: 'missing index entry' });
    }
  }

  return {
    ok: issues.length === 0,
    conceptCount: conceptFiles.length,
    issues,
  };
}

function commandDoctor(options) {
  const repoRoot = resolveRepoRoot(options);
  const result = doctorChecks(repoRoot);
  if (!result.ok) {
    failRuntime('doctor found malformed store', result);
  }

  printJson(process.stdout, { ok: true, verb: 'doctor', conceptCount: result.conceptCount });
}

function main() {
  try {
    const { verb, options } = parseArgs(process.argv.slice(2));
    switch (verb) {
      case 'capture':
        commandCapture(options);
        break;
      case 'project':
        commandProject(options);
        break;
      case 'sync':
        commandSync(options);
        break;
      case 'new':
        commandNew(options);
        break;
      case 'index':
        commandIndex(options);
        break;
      case 'promote':
        commandPromote(options);
        break;
      case 'doctor':
        commandDoctor(options);
        break;
      default:
        failUsage(`unknown verb: ${verb}`);
    }
  } catch (error) {
    if (error instanceof SyntaxError) {
      failRuntime(error.message);
    }
    failRuntime(error && error.message ? error.message : 'unknown error');
  }
}

main();
