/**
 * vWhale: lifecycle hook runner. Hooks are shell commands that fire at key
 * points in the inference pipeline. Each hook receives a JSON payload on
 * stdin and can return JSON on stdout to influence behavior.
 *
 * Inspired by Whale's hook system with 11 lifecycle events. BooCode
 * implements the most relevant subset: PreToolUse, PostToolUse,
 * UserPromptSubmit, Stop, PreCompact, PostCompact.
 *
 * Config: JSON file at HOOKS_CONFIG_PATH (default /data/hooks.json).
 * Format:
 * ```json
 * {
 *   "hooks": {
 *     "PreToolUse": [
 *       { "match": "shell_run", "command": "python3 /data/hooks/check_shell.py", "timeout": 30 }
 *     ],
 *     "Stop": [
 *       { "command": "node /data/hooks/log_turn.mjs" }
 *     ]
 *   }
 * }
 * ```
 */

import { spawn } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';
import type { FastifyBaseLogger } from 'fastify';

export type HookEvent =
  | 'PreToolUse'
  | 'PostToolUse'
  | 'UserPromptSubmit'
  | 'Stop'
  | 'PreCompact'
  | 'PostCompact';

const ALL_EVENTS: HookEvent[] = [
  'PreToolUse',
  'PostToolUse',
  'UserPromptSubmit',
  'Stop',
  'PreCompact',
  'PostCompact',
];

export interface HookConfig {
  /** Glob or exact tool name to match (PreToolUse/PostToolUse only). Omit or '*' for all. */
  match?: string;
  /** Shell command to run. Receives JSON payload on stdin. */
  command: string;
  /** Timeout in seconds (default 30). */
  timeout?: number;
}

export interface HooksConfig {
  hooks: Partial<Record<HookEvent, HookConfig[]>>;
}

export interface PreToolUsePayload {
  event: 'PreToolUse';
  session_id: string;
  tool_name: string;
  tool_args: Record<string, unknown>;
}

export interface PostToolUsePayload {
  event: 'PostToolUse';
  session_id: string;
  tool_name: string;
  tool_args: Record<string, unknown>;
  tool_result: unknown;
  tool_error?: string;
}

export interface UserPromptSubmitPayload {
  event: 'UserPromptSubmit';
  session_id: string;
  chat_id: string;
  prompt: string;
}

export interface StopPayload {
  event: 'Stop';
  session_id: string;
  chat_id: string;
  last_assistant_text: string;
  turn: number;
}

export interface PreCompactPayload {
  event: 'PreCompact';
  session_id: string;
  chat_id: string;
  messages_before: number;
}

export interface PostCompactPayload {
  event: 'PostCompact';
  session_id: string;
  chat_id: string;
  messages_before: number;
  messages_after: number;
  summary: string;
}

export type HookPayload =
  | PreToolUsePayload
  | PostToolUsePayload
  | UserPromptSubmitPayload
  | StopPayload
  | PreCompactPayload
  | PostCompactPayload;

export type HookDecision = 'pass' | 'warn' | 'block';

export interface HookResponse {
  decision?: HookDecision;
  reason?: string;
  updated_input?: Record<string, unknown> | string;
  /** Injected into the model's context for the next turn. */
  additional_context?: string;
}

export interface HookRunner {
  /** Run all hooks for the given event. Returns the effective response. */
  run(event: HookEvent, payload: HookPayload, log?: FastifyBaseLogger): Promise<HookResponse>;
}

let hooksConfig: HooksConfig | null = null;
let hooksPath: string | null = null;

/** Load hooks config from disk. Missing file = no hooks. Never throws. */
export function loadHooksConfig(path: string): HooksConfig {
  hooksPath = path;
  if (!existsSync(path)) {
    hooksConfig = { hooks: {} };
    return hooksConfig;
  }
  try {
    const raw = readFileSync(path, 'utf8');
    const parsed = JSON.parse(raw) as HooksConfig;
    hooksConfig = {
      hooks: { ...parsed.hooks },
    };
    for (const event of Object.keys(hooksConfig.hooks)) {
      if (!ALL_EVENTS.includes(event as HookEvent)) {
        console.warn(`hooks: unknown event '${event}' in ${path} — ignoring`);
        delete hooksConfig.hooks[event as HookEvent];
      }
    }
  } catch (err) {
    console.error(`hooks: failed to load ${path}`, err);
    hooksConfig = { hooks: {} };
  }
  return hooksConfig;
}

/** Reload the config file (call after a PATCH). */
export function reloadHooksConfig(): HooksConfig {
  if (hooksPath) return loadHooksConfig(hooksPath);
  hooksConfig = { hooks: {} };
  return hooksConfig;
}

function getConfig(): HooksConfig {
  return hooksConfig ?? { hooks: {} };
}

/** Create a HookRunner for the current config. */
export function createHookRunner(): HookRunner {
  return {
    async run(event, payload, log): Promise<HookResponse> {
      const configs = getConfig().hooks[event];
      if (!configs || configs.length === 0) return { decision: 'pass' };

      // Pre-filter by match pattern for tool events
      const toolName = 'tool_name' in payload ? (payload as PreToolUsePayload).tool_name : undefined;

      let effective: HookResponse = { decision: 'pass' };

      for (const cfg of configs) {
        // Skip if match doesn't apply
        if (toolName && cfg.match && cfg.match !== '*' && cfg.match !== toolName) continue;

        const result = await runSingleHook(cfg, payload, log);
        // Merge decisions: block > warn > pass
        if (result.decision === 'block') {
          effective = { ...result, decision: 'block' };
          break; // block is terminal
        }
        if (result.decision === 'warn' && effective.decision !== 'block') {
          effective = { ...result, decision: 'warn' };
        }
        // Merge additional_context and updated_input
        if (result.additional_context) {
          effective.additional_context = effective.additional_context
            ? effective.additional_context + '\n' + result.additional_context
            : result.additional_context;
        }
        if (result.updated_input && !effective.updated_input) {
          effective.updated_input = result.updated_input;
        }
      }

      return effective;
    },
  };
}

async function runSingleHook(
  cfg: HookConfig,
  payload: HookPayload,
  log?: FastifyBaseLogger,
): Promise<HookResponse> {
  const timeoutMs = (cfg.timeout ?? 30) * 1000;

  return new Promise((resolve) => {
    const child = spawn('sh', ['-c', cfg.command], {
      stdio: ['pipe', 'pipe', 'pipe'],
      timeout: timeoutMs,
      env: { ...process.env },
    });

    const stdout: Buffer[] = [];
    const stderr: Buffer[] = [];

    child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk));
    child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));

    let settled = false;
    const timer = setTimeout(() => {
      if (!settled) {
        settled = true;
        child.kill('SIGTERM');
        log?.warn({ event: payload.event, command: cfg.command }, 'hooks: timeout');
        resolve({ decision: 'warn', reason: 'hook timed out' });
      }
    }, timeoutMs);

    child.on('error', (err) => {
      if (!settled) {
        settled = true;
        clearTimeout(timer);
        log?.warn({ err, event: payload.event }, 'hooks: spawn error');
        resolve({ decision: 'warn', reason: `hook failed: ${err.message}` });
      }
    });

    child.on('close', (code) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);

      const out = Buffer.concat(stdout).toString('utf8').trim();
      const errOut = Buffer.concat(stderr).toString('utf8').trim();

      if (code !== 0 && !out) {
        log?.warn({ event: payload.event, code, stderr: errOut.slice(0, 200) }, 'hooks: non-zero exit');
        resolve({ decision: 'warn', reason: `hook exited ${code}` });
        return;
      }

      if (out) {
        try {
          const parsed = JSON.parse(out) as HookResponse;
          resolve(parsed);
          return;
        } catch {
          // Not JSON — treat as pass with stdout as context
          if (out.length > 0) {
            resolve({ decision: 'pass', additional_context: out });
            return;
          }
        }
      }

      resolve({ decision: 'pass' });
    });

    const json = JSON.stringify(payload);
    child.stdin.write(json);
    child.stdin.end();
  });
}
