#!/usr/bin/env node
/**
 * Consolidates PostToolUse hooks into one synchronous and one asynchronous
 * entrypoint while preserving each hook's ID, matcher, profile, and output.
 */

'use strict';

const path = require('path');
const { StringDecoder } = require('string_decoder');
const { isHookEnabled } = require('../lib/hook-flags');
const { runPostBash } = require('./bash-hook-dispatcher');
const { run: runQualityGate } = require('./quality-gate');
const { run: runDesignQualityCheck } = require('./design-quality-check');
const { run: runPostEditAccumulator } = require('./post-edit-accumulator');
const { run: runConsoleWarn } = require('./post-edit-console-warn');
const { run: runGovernanceCapture } = require('./governance-capture');
const { run: runSessionActivityTracker } = require('./session-activity-tracker');
const { run: runObserve } = require('./observe-runner');
const { run: runMetricsBridge } = require('./ecc-metrics-bridge');
const { run: runContextMonitor } = require('./ecc-context-monitor');
const { run: runSkillRunTracker } = require('./skill-run-tracker');

const MAX_STDIN = 1024 * 1024;

const SYNC_HOOKS = [
  { id: 'post:edit:design-quality-check', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/design-quality-check.js', run: runDesignQualityCheck },
  { id: 'post:edit:accumulator', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/post-edit-accumulator.js', run: runPostEditAccumulator },
  { id: 'post:edit:console-warn', matcher: 'Edit', profiles: 'standard,strict', script: 'scripts/hooks/post-edit-console-warn.js', run: runConsoleWarn },
  { id: 'post:governance-capture', matcher: 'Bash|Write|Edit|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/governance-capture.js', run: runGovernanceCapture },
  { id: 'post:session-activity-tracker', matcher: '*', profiles: 'standard,strict', script: 'scripts/hooks/session-activity-tracker.js', run: runSessionActivityTracker },
  { id: 'post:ecc-metrics-bridge', matcher: '*', profiles: 'minimal,standard,strict', script: 'scripts/hooks/ecc-metrics-bridge.js', run: runMetricsBridge },
  { id: 'post:ecc-context-monitor', matcher: '*', profiles: 'standard,strict', script: 'scripts/hooks/ecc-context-monitor.js', run: runContextMonitor }
];

const ASYNC_HOOKS = [
  {
    id: 'post:bash:dispatcher',
    matcher: 'Bash',
    // main ran this phase unconditionally; sub-hooks gate themselves internally
    profiles: 'minimal,standard,strict',
    script: 'scripts/hooks/post-bash-dispatcher.js',
    run(raw) {
      const result = runPostBash(raw);
      return { stdout: result.output, stderr: result.stderr, exitCode: result.exitCode };
    }
  },
  { id: 'post:quality-gate', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/quality-gate.js', run: runQualityGate },
  { id: 'post:observe:continuous-learning', matcher: '*', profiles: 'standard,strict', script: 'scripts/hooks/observe-runner.js', run: runObserve },
  { id: 'post:skill:track', matcher: 'Skill', profiles: 'standard,strict', script: 'scripts/hooks/skill-run-tracker.js', run: runSkillRunTracker }
];

function getPluginRoot(env = process.env) {
  return env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
}

function matchesTool(matcher, toolName) {
  return (
    matcher === '*' ||
    String(matcher || '')
      .split('|')
      .map(value => value.trim())
      .filter(Boolean)
      .includes(String(toolName || ''))
  );
}

function isEnabled(hook, env) {
  return isHookEnabled(hook.id, {
    env,
    profiles: hook.profiles,
  });
}

function extractToolName(raw) {
  try {
    return String(JSON.parse(raw)?.tool_name || '');
  } catch {
    return '';
  }
}

function buildDryRunPreview(hook, raw) {
  let target = '';
  try {
    const input = JSON.parse(raw)?.tool_input || {};
    target = String(input.file_path || input.path || input.command || '');
  } catch {
    target = '';
  }
  const suffix = target ? ` target=${target}` : '';
  return `[DryRun] Hook "${hook.id}" would execute: ${hook.script} (enabled=true, profiles=${hook.profiles})${suffix}\n`;
}

function normalizeResult(raw, output) {
  if (typeof output === 'string' || Buffer.isBuffer(output)) {
    const stdout = String(output);
    return { stdout: stdout !== raw ? stdout : '', stderr: '', exitCode: 0 };
  }
  if (!output || typeof output !== 'object') {
    return { stdout: '', stderr: '', exitCode: 0 };
  }

  let stdout = '';
  if (Object.prototype.hasOwnProperty.call(output, 'stdout')) {
    stdout = String(output.stdout ?? '');
  } else if (Object.prototype.hasOwnProperty.call(output, 'output')) {
    stdout = String(output.output ?? '');
  } else if (Object.prototype.hasOwnProperty.call(output, 'additionalContext')) {
    stdout = JSON.stringify({
      hookSpecificOutput: {
        hookEventName: 'PostToolUse',
        additionalContext: String(output.additionalContext ?? '')
      }
    });
  }

  return {
    stdout: stdout !== raw ? stdout : '',
    stderr: typeof output.stderr === 'string' ? output.stderr : '',
    exitCode: Number.isInteger(output.exitCode) ? output.exitCode : 0
  };
}

function appendLine(current, next) {
  if (!next) return current;
  return current + (String(next).endsWith('\n') ? String(next) : `${next}\n`);
}

function parseAdditionalContext(stdout) {
  try {
    const parsed = JSON.parse(stdout);
    const output = parsed?.hookSpecificOutput;
    if (output?.hookEventName !== 'PostToolUse') return null;
    return typeof output.additionalContext === 'string' ? output.additionalContext : null;
  } catch {
    return null;
  }
}

function mergeHookStdout(outputs) {
  if (outputs.length === 0) return { stdout: '', warning: '' };
  if (outputs.length === 1) return { stdout: outputs[0].stdout, warning: '' };

  const contexts = outputs.map(output => parseAdditionalContext(output.stdout));
  if (contexts.every(context => context !== null)) {
    return {
      stdout: JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'PostToolUse',
          additionalContext: contexts.join('\n')
        }
      }),
      warning: ''
    };
  }

  const kept = outputs[outputs.length - 1];
  const dropped = outputs
    .slice(0, -1)
    .map(output => output.id)
    .join(', ');
  return {
    stdout: kept.stdout,
    warning: `[Hook] stdout from ${dropped} dropped in favor of ${kept.id}; raw stdout cannot be merged`
  };
}

function runHooks(raw, hooks, options = {}) {
  const env = options.env || process.env;
  const toolName = options.toolName ?? extractToolName(raw);
  const pluginRoot = getPluginRoot(env);
  const outputs = [];
  let stderr = '';
  let exitCode = 0;

  for (const hook of hooks) {
    if (!matchesTool(hook.matcher, toolName) || !isEnabled(hook, env)) continue;
    if (env.ECC_DRY_RUN === '1') {
      stderr += buildDryRunPreview(hook, raw);
      continue;
    }

    try {
      const result = normalizeResult(
        raw,
        hook.run(raw, {
          hookId: hook.id,
          pluginRoot,
          scriptPath: path.join(pluginRoot, hook.script || ''),
          truncated: options.truncated === true,
          maxStdin: MAX_STDIN
        })
      );
      if (result.stdout) outputs.push({ id: hook.id, stdout: result.stdout });
      stderr = appendLine(stderr, result.stderr);
      if (result.exitCode !== 0) {
        if (exitCode === 0) exitCode = result.exitCode;
        stderr = appendLine(stderr, `[Hook] ${hook.id} exited with code ${result.exitCode}; continuing`);
      }
    } catch (error) {
      stderr = appendLine(stderr, `[Hook] ${hook.id} failed: ${error.message}`);
    }
  }

  const merged = mergeHookStdout(outputs);
  if (merged.warning) stderr = appendLine(stderr, merged.warning);
  return { stdout: merged.stdout, stderr, exitCode };
}

function readStdinRaw() {
  return new Promise(resolve => {
    const decoder = new StringDecoder('utf8');
    let raw = '';
    let bytesRead = 0;
    let truncated = false;
    let settled = false;
    process.stdin.on('data', chunk => {
      const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
      const remaining = Math.max(0, MAX_STDIN - bytesRead);
      const accepted = buffer.subarray(0, remaining);
      if (accepted.length > 0) {
        raw += decoder.write(accepted);
        bytesRead += accepted.length;
      }
      if (buffer.length > accepted.length) truncated = true;
    });
    const finish = () => {
      if (settled) return;
      settled = true;
      if (!truncated) raw += decoder.end();
      resolve({ raw, truncated });
    };
    process.stdin.once('end', finish);
    process.stdin.once('error', finish);
  });
}

function resolveMainStdout(raw, result, options = {}) {
  if (result.stdout) return result.stdout;
  if (options.truncated || result.exitCode !== 0 || !options.passthrough) return '';
  return raw;
}

async function main() {
  const mode = process.argv[2] === 'async' ? 'async' : 'sync';
  const { raw, truncated } = await readStdinRaw();
  const dispatcherId = `post:dispatcher:${mode}`;
  const dispatcherEnabled = isEnabled(
    {
      id: dispatcherId,
      profiles: 'minimal,standard,strict'
    },
    process.env
  );
  const hooks = dispatcherEnabled ? (mode === 'async' ? ASYNC_HOOKS : SYNC_HOOKS) : [];
  const result = runHooks(raw, hooks, { truncated });
  if (truncated) {
    process.stderr.write(`[Hook] stdin exceeded ${MAX_STDIN} bytes for PostToolUse ${mode}; suppressing pass-through\n`);
  }
  if (result.stderr) process.stderr.write(result.stderr);
  const stdout = resolveMainStdout(raw, result, {
    passthrough: process.env.ECC_POSTTOOLUSE_PASSTHROUGH === '1',
    truncated
  });
  if (stdout) process.stdout.write(stdout);
  process.exitCode = result.exitCode;
}

function cli() {
  main().catch(error => {
    process.stderr.write(`[Hook] PostToolUse dispatcher failed: ${error.message}\n`);
    process.exitCode = 0;
  });
}

if (require.main === module) cli();

module.exports = {
  ASYNC_HOOKS,
  SYNC_HOOKS,
  cli,
  matchesTool,
  main,
  mergeHookStdout,
  normalizeResult,
  resolveMainStdout,
  runHooks
};
