#!/usr/bin/env node
// handoff-pipeline.mjs — Deterministic JSON pipeline for graceful-handoff v2.0.
//
// Detects handoff type, classifies state, writes NEXT-SESSION artifact, optionally
// commits and pushes. Returns structured JSON to stdout. Designed to be called both
// by the SKILL.md (interactive) and the Stop hook (auto-execute).
//
// Usage:
//   node handoff-pipeline.mjs [topic-slug] [--dry-run] [--no-commit] [--no-push]
//                             [--auto] [--non-interactive] [--json]
//
// Output (JSON to stdout):
//   {
//     "handoff_type": "multi-sesjon | plugin-arbeid | enkelt-oppgave",
//     "write_dir": "/abs/path",
//     "artifact_path": "/abs/path/NEXT-SESSION-...",
//     "next_steps": [...],
//     "git_status": { branch, dirty, ahead },
//     "commit_message": "...",
//     "actions_taken": [...],
//     "errors": [...]
//   }
//
// Exit codes: 0 = success (even if errors[] non-empty); 1 = unrecoverable internal error.

import { execSync, execFileSync, spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, readdirSync } from 'node:fs';
import { dirname, join, basename, resolve } from 'node:path';
import { createInterface } from 'node:readline';

// ---------- Argument parsing ----------

function parseArgs(argv) {
  const args = {
    slug: null,
    dryRun: false,
    noCommit: false,
    noPush: false,
    auto: false,
    nonInteractive: false,
    json: true,
  };
  for (const a of argv) {
    if (a === '--dry-run') args.dryRun = true;
    else if (a === '--no-commit') args.noCommit = true;
    else if (a === '--no-push') args.noPush = true;
    else if (a === '--auto') args.auto = true;
    else if (a === '--non-interactive') args.nonInteractive = true;
    else if (a === '--json') args.json = true;
    else if (!a.startsWith('--') && !args.slug) args.slug = a;
  }
  return args;
}

// ---------- Git helpers ----------

function gitOk(cmd, opts = {}) {
  try {
    return execSync(cmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], ...opts }).trim();
  } catch {
    return null;
  }
}

function gitStatus() {
  const branch = gitOk('git branch --show-current') || gitOk('git rev-parse --abbrev-ref HEAD');
  const porcelain = gitOk('git status --porcelain') || '';
  const dirty = porcelain.length > 0;
  let ahead = 0;
  const upstream = gitOk('git rev-parse --abbrev-ref @{u} 2>/dev/null');
  if (upstream) {
    const counts = gitOk(`git rev-list --left-right --count ${upstream}...HEAD`);
    if (counts) ahead = parseInt(counts.split(/\s+/)[1] || '0', 10);
  }
  const detached = !branch || branch === 'HEAD';
  return { branch, dirty, ahead, upstream, detached, porcelain };
}

// ---------- Plugin-root detection ----------

function findPluginRoot(startDir) {
  let cur = startDir;
  for (let i = 0; i < 5; i++) {
    if (existsSync(join(cur, '.claude-plugin', 'plugin.json'))) return cur;
    const parent = dirname(cur);
    if (parent === cur) break;
    cur = parent;
  }
  return null;
}

// ---------- Multi-session detection ----------

function findActiveProject(cwd) {
  // Look for .claude/projects/*/progress.json that is not completed
  try {
    const out = execSync(
      `find . -maxdepth 5 -path '*/.claude/projects/*/progress.json' 2>/dev/null | sort -r`,
      { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }
    ).trim().split('\n').filter(Boolean);
    for (const rel of out) {
      const abs = resolve(cwd, rel);
      try {
        const data = JSON.parse(readFileSync(abs, 'utf-8'));
        if (data.status && data.status !== 'completed' && data.status !== 'failed') {
          return { progressPath: abs, projectDir: dirname(abs), status: data.status };
        }
      } catch { /* skip malformed */ }
    }
  } catch { /* find failed */ }
  return null;
}

// ---------- Classification ----------

function classifyHandoff(cwd) {
  const project = findActiveProject(cwd);
  if (project) return { type: 'multi-sesjon', writeDir: project.projectDir, projectDir: project.projectDir };

  const pluginRoot = findPluginRoot(cwd);
  if (pluginRoot) return { type: 'plugin-arbeid', writeDir: pluginRoot, pluginRoot };

  return { type: 'enkelt-oppgave', writeDir: cwd };
}

// ---------- Commit-message generation ----------

function generateCommitMessage(status) {
  const files = status.porcelain.split('\n').filter(Boolean).map(line => line.slice(3));
  const tests = files.filter(f => f.includes('/tests/') || f.endsWith('.test.mjs') || f.endsWith('.test.js')).length;
  const docs = files.filter(f => /\.(md|mdx)$/i.test(f) && !f.includes('/tests/')).length;
  const code = files.length - tests - docs;

  let type = 'chore';
  if (code > 0 && code >= tests + docs) type = 'feat';
  else if (tests > 0 && tests >= code) type = 'test';
  else if (docs > 0 && docs >= code) type = 'docs';

  // Scope = plugin name if all files in single plugin
  const pluginMatch = files
    .map(f => f.match(/^plugins\/([^/]+)/))
    .filter(Boolean)
    .map(m => m[1]);
  const uniquePlugins = [...new Set(pluginMatch)];
  const scope = uniquePlugins.length === 1 ? uniquePlugins[0] : '';

  const subject = `wip: pågående arbeid (${files.length} fil${files.length === 1 ? '' : 'er'})`;
  return scope ? `${type}(${scope}): ${subject}` : `${type}: ${subject}`;
}

// ---------- Artifact rendering ----------

function renderArtifact(state, classification) {
  const today = new Date().toISOString().slice(0, 10);
  const branch = state.git.branch || 'HEAD';
  const lastCommits = (gitOk('git log --oneline -5') || '').split('\n').filter(Boolean);

  const lines = [];
  lines.push(`# NEXT-SESSION-PROMPT — ${basename(classification.writeDir)} ${today}`);
  lines.push('');
  lines.push('## Hvorfor dette eksisterer');
  lines.push('');
  lines.push(`Sesjons-handoff produsert av graceful-handoff v2.0 ${state.auto ? '(auto-trigget av Stop hook)' : '(manuell trigger)'}.`);
  lines.push(`Type: \`${classification.type}\`. Branch: \`${branch}\`.`);
  if (state.git.dirty) lines.push('Hadde ucommitted endringer ved handoff-tidspunkt.');
  lines.push('');
  lines.push('## Status ved sesjonshåndoff');
  lines.push('');
  lines.push('### ✅ Ferdig');
  lines.push('');
  if (lastCommits.length === 0) lines.push('- Ingen commits funnet.');
  else for (const c of lastCommits) lines.push(`- \`${c}\``);
  lines.push('');
  lines.push('### ⏳ Ikke startet / delvis');
  lines.push('');
  lines.push('- Fyll inn av neste sesjon (graceful-handoff v2.0 pipeline genererer ikke dette automatisk; bruk manuell trigger for spesifikk plan-progresjon).');
  lines.push('');
  lines.push('### ⚠️ Brutt / kjent risiko');
  lines.push('');
  lines.push(state.git.dirty ? '- Uncommitted endringer ved handoff-tidspunkt — sjekk `git status`.' : '- Ingen kjente broken tester ved handoff.');
  lines.push('');
  lines.push('## Slik fortsetter du');
  lines.push('');
  lines.push(`1. \`cd ${classification.writeDir}\``);
  lines.push(`2. \`cat ${state.artifactName}\` — les denne filen igjen`);
  lines.push('3. `git log --oneline -5` og `git status`');
  lines.push('4. Fortsett fra siste pågående arbeid');
  lines.push('');
  lines.push('## Push-policy');
  lines.push('');
  lines.push('- Direkte push til `main` på Forgejo er pre-autorisert');
  lines.push('- Aldri GitHub — kun Forgejo (`git.fromaitochitta.com`)');
  lines.push('');
  lines.push('## Verifiseringskommandoer');
  lines.push('');
  lines.push('```bash');
  lines.push('git log --oneline -5');
  lines.push('git status');
  lines.push('```');
  lines.push('');
  lines.push('## Husk');
  lines.push('');
  lines.push('- Opus 4.7 fyller kontekst raskt — auto-trigger ved estimert 70% er enabled i graceful-handoff v2.0');
  lines.push('- Push gjenstår hvis dette var auto-handoff (Stop hook bruker `--no-push`)');
  lines.push('');
  return lines.join('\n');
}

// ---------- Main ----------

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const cwd = process.cwd();
  const errors = [];
  const actionsTaken = [];

  // Validate flag combos
  if (args.nonInteractive && !args.auto && !args.dryRun && !args.noCommit) {
    errors.push('--non-interactive uten --auto er ikke gyldig (commit-bekreftelse må enten være interaktiv, auto-godkjent, eller skipped via --no-commit)');
    output({ args, cwd, classification: null, errors, actionsTaken });
    return;
  }

  // 1. Get git state
  const git = gitStatus();
  if (!git.branch && !args.dryRun) {
    errors.push('Kunne ikke detektere git-state — er denne mappen et git-repo?');
    output({ args, cwd, classification: null, git, errors, actionsTaken });
    return;
  }

  // 2. Classify handoff
  const classification = classifyHandoff(cwd);

  // 3. Determine artifact path
  const artifactName = args.slug ? `NEXT-SESSION-${args.slug}.local.md` : 'NEXT-SESSION-PROMPT.local.md';
  const artifactPath = join(classification.writeDir, artifactName);

  // 4. Idempotency check: if artifact exists and was modified < 60s ago, and no new git changes, no-op
  if (!args.dryRun && existsSync(artifactPath) && !git.dirty) {
    try {
      const stat = statSync(artifactPath);
      const ageMs = Date.now() - stat.mtimeMs;
      if (ageMs < 60_000) {
        output({
          args, cwd, classification, git,
          artifactPath, commitMessage: '',
          errors, actionsTaken: ['idempotent-no-op (recent artifact, clean tree)'],
          nextSteps: nextStepsFor(classification, artifactName),
        });
        return;
      }
    } catch { /* statSync failed; proceed */ }
  }

  // 5. Generate commit message
  const commitMessage = git.dirty ? generateCommitMessage(git) : '';

  // 6. Build state for rendering
  const state = {
    git,
    auto: args.auto,
    artifactName,
  };

  // 7. Write artifact
  const artifactContent = renderArtifact(state, classification);
  if (!args.dryRun) {
    try {
      mkdirSync(classification.writeDir, { recursive: true });
      writeFileSync(artifactPath, artifactContent, 'utf-8');
      actionsTaken.push(`wrote-artifact: ${artifactPath}`);
    } catch (e) {
      errors.push(`artifact write failed: ${e.message}`);
    }
  } else {
    actionsTaken.push(`dry-run: would write artifact to ${artifactPath}`);
  }

  // 8. Commit (unless --no-commit / --dry-run / nothing to commit)
  if (!args.dryRun && !args.noCommit && (git.dirty || existsSync(artifactPath))) {
    // Check robustness: detached HEAD, no remote
    if (git.detached) {
      errors.push('detached HEAD — skipping commit (no branch to commit on)');
    } else {
      // Confirmation gate
      let proceed = false;
      if (args.auto) {
        proceed = true;
      } else if (args.nonInteractive) {
        errors.push('non-interactive without --auto cannot confirm commit');
      } else {
        // Interactive: print message to stderr, read y/n from stdin
        process.stderr.write(`\nCommit-melding:\n---\n${commitMessage}\n---\nFortsett med commit? (y/n): `);
        proceed = await readYesNo();
      }
      if (proceed) {
        try {
          // CRITICAL: never `git add -A` — that scoops up unrelated work-in-progress.
          // Stage ONLY the handoff artifact + optional REMEMBER.md/TODO.md if present.
          // Other dirty files stay in working tree for the user.
          const stageList = [artifactPath];
          for (const candidate of ['REMEMBER.md', 'TODO.md']) {
            const p = join(classification.writeDir, candidate);
            if (existsSync(p)) stageList.push(p);
          }
          execFileSync('git', ['add', '--', ...stageList], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
          // git commit with -- pathspec limits commit to those paths from index.
          execFileSync('git', ['commit', '-m', commitMessage, '--', ...stageList], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
          actionsTaken.push('committed');
        } catch (e) {
          errors.push(`commit failed: ${(e.stderr || e.message || '').toString().slice(0, 200)}`);
        }
      } else {
        actionsTaken.push('commit-cancelled-by-user');
      }
    }
  }

  // 9. Push (unless --no-push / --dry-run / no commit happened)
  if (!args.dryRun && !args.noPush && actionsTaken.includes('committed')) {
    if (!git.upstream) {
      errors.push('no upstream branch — skipping push (set with: git push -u origin <branch>)');
    } else {
      try {
        execFileSync('git', ['push', 'origin', git.branch], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
        actionsTaken.push('pushed');
      } catch (e) {
        errors.push(`push failed: ${(e.stderr || e.message || '').toString().slice(0, 200)}`);
      }
    }
  }

  output({
    args, cwd, classification, git,
    artifactPath, commitMessage,
    errors, actionsTaken,
    nextSteps: nextStepsFor(classification, artifactName),
  });
}

function nextStepsFor(classification, artifactName) {
  return [
    `cd ${classification.writeDir}`,
    `cat ${artifactName}`,
    'git log --oneline -5',
    'git status',
    'Fortsett fra siste pågående arbeid (se artefakt-fil).',
  ];
}

function output({ args, cwd, classification, git, artifactPath, commitMessage, errors, actionsTaken, nextSteps }) {
  const result = {
    handoff_type: classification?.type || 'unknown',
    write_dir: classification?.writeDir || cwd,
    artifact_path: artifactPath || null,
    next_steps: nextSteps || [],
    git_status: git ? { branch: git.branch, dirty: git.dirty, ahead: git.ahead, detached: git.detached } : null,
    commit_message: commitMessage || '',
    actions_taken: actionsTaken,
    errors,
    args: { dryRun: args.dryRun, noCommit: args.noCommit, noPush: args.noPush, auto: args.auto, slug: args.slug },
  };
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}

function readYesNo() {
  return new Promise((resolveP) => {
    const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: false });
    rl.question('', (answer) => {
      rl.close();
      const normalized = (answer || '').trim().toLowerCase();
      resolveP(normalized === 'y' || normalized === 'yes' || normalized === 'ja' || normalized === 'j');
    });
  });
}

main().catch((e) => {
  process.stderr.write(`pipeline-fatal: ${e.message}\n${e.stack}\n`);
  process.exit(1);
});
