#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright 2024-2026 Lukasz Krzemien (biuro@softspark.eu)
# Source: https://github.com/softspark/ai-toolkit

"""Generate the ai-toolkit opencode plugin at ``.opencode/plugins/ai-toolkit-hooks.js``.

opencode plugins are JS/TS modules with a NAMED export that receive a
context (``$``, ``project``, ``client``, ``directory``, ``worktree``) and
return a hooks object. Docs: https://opencode.ai/docs/plugins/

This generator emits a single-file plugin that bridges ai-toolkit's
shared Bash hooks (``$HOME/.softspark/ai-toolkit/hooks/*.sh``) to
opencode lifecycle events. Coverage map:

  opencode event              -> ai-toolkit Bash hook(s)
  ---------------------------------------------------------------
  session.created             -> session-start.sh + mcp-health.sh
  session.compacted           -> pre-compact.sh + pre-compact-save.sh
  session.deleted             -> session-end.sh + save-session.sh
  message.updated             -> user-prompt-submit.sh + track-usage.sh
  message.part.updated        -> user-prompt-submit.sh + track-usage.sh
  tool.execute.before (bash)  -> guard-destructive.sh + commit-quality.sh
  tool.execute.after          -> post-tool-use.sh
  permission.asked            -> guard-destructive.sh
  command.executed            -> post-tool-use.sh

Security: hooks are invoked via ``$`` with the script path bound to a
JS constant (no opencode-payload interpolation into the command). Event
payloads are passed on stdin as JSON. Non-zero exit codes are logged to
stderr; ``exit 2`` is preserved as a block signal for PreToolUse guards.

Usage:
  python3 scripts/generate_opencode_plugin.py [target-dir] [--config-root PATH]
"""
from __future__ import annotations

import argparse
from pathlib import Path

PLUGIN_BODY = r"""// ai-toolkit opencode plugin — bridges shared Bash hooks to opencode events.
// Auto-generated by ai-toolkit. Do not edit by hand; re-run to update:
//   ai-toolkit opencode-plugin
// Docs: https://opencode.ai/docs/plugins/
//
// Hooks live at $HOME/.softspark/ai-toolkit/hooks/*.sh and are shared with
// Claude Code and Codex CLI. This plugin invokes them via Bun's `$` shell
// with the script path bound as a JS constant — no string interpolation of
// opencode event payloads into the shell, so event data cannot inject
// shell metacharacters. Payloads are passed on stdin as JSON.

const HOOKS_DIR = `${process.env.HOME}/.softspark/ai-toolkit/hooks`;

/** Invoke a Bash hook with a JSON payload on stdin. */
async function runHook($, script, payload, blockOnExit2 = false) {
  const scriptPath = `${HOOKS_DIR}/${script}`;
  let result;
  try {
    const input = JSON.stringify(payload ?? {});
    const proc = $`bash ${scriptPath}`.env({
      ...process.env,
      AI_TOOLKIT_EVENT: payload?.event || "unknown",
    });
    proc.stdin.write(input);
    proc.stdin.end();
    result = await proc.quiet().nothrow();
  } catch (err) {
    process.stderr.write(`[ai-toolkit] failed to run ${script}: ${err.message}\n`);
    return 0;
  }
  if (result.exitCode === 2 && blockOnExit2) {
    const detail = result.stderr.toString().trim();
    throw new Error(
      `[ai-toolkit] ${script} blocked tool execution${detail ? `: ${detail}` : ""}`
    );
  }
  if (result.exitCode !== 0 && result.exitCode !== 2) {
    process.stderr.write(
      `[ai-toolkit] ${script} exited ${result.exitCode}\n${result.stderr.toString()}`
    );
  }
  return result.exitCode;
}

export const AiToolkitHooks = async ({ $, project, directory, worktree }) => ({
  event: async ({ event }) => {
    const payload = {
      event: event.type,
      session_id: event?.properties?.sessionID,
      project,
      directory,
      worktree,
    };
    switch (event.type) {
      case "session.created":
        await runHook($, "session-start.sh", payload);
        await runHook($, "mcp-health.sh", payload);
        break;
      case "session.compacted":
        await runHook($, "pre-compact.sh", payload);
        await runHook($, "pre-compact-save.sh", payload);
        break;
      case "session.deleted":
        await runHook($, "session-end.sh", payload);
        await runHook($, "save-session.sh", payload);
        break;
      case "message.updated":
      case "message.part.updated":
        await runHook($, "user-prompt-submit.sh", payload);
        await runHook($, "track-usage.sh", payload);
        break;
      case "permission.asked":
        await runHook($, "guard-destructive.sh", payload);
        break;
      case "command.executed":
        await runHook($, "post-tool-use.sh", payload);
        break;
    }
  },

  "tool.execute.before": async (input, output) => {
    const payload = {
      event: "tool.execute.before",
      session_id: input?.sessionID,
      tool_name: input?.tool,
      tool_input: output?.args,
      tool: input?.tool,
      args: output?.args,
      project,
    };
    if (input?.tool === "bash") {
      await runHook($, "guard-destructive.sh", payload, true);
      await runHook($, "commit-quality.sh", payload);
    }
  },

  "tool.execute.after": async (input, output) => {
    const payload = {
      event: "tool.execute.after",
      session_id: input?.sessionID,
      tool_name: input?.tool,
      tool: input?.tool,
      result: output,
      project,
    };
    await runHook($, "post-tool-use.sh", payload);
  },
});
"""


def generate(target_dir: Path, config_root: Path | None = None) -> Path:
    """Write the opencode plugin file and return its path.

    ``config_root`` lets the caller override the default project-local
    layout. When omitted, writes to ``target_dir/.opencode/plugins/``.
    Pass ``config_root=~/.config/opencode`` to lay down the plugin
    globally at ``~/.config/opencode/plugins/ai-toolkit-hooks.js``.
    """
    base = config_root if config_root is not None else target_dir / ".opencode"
    plugins_dir = base / "plugins"
    plugins_dir.mkdir(parents=True, exist_ok=True)
    path = plugins_dir / "ai-toolkit-hooks.js"
    path.write_text(PLUGIN_BODY, encoding="utf-8")
    return path


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate opencode plugin")
    parser.add_argument("target", nargs="?", default=".", help="Target directory")
    parser.add_argument(
        "--config-root",
        type=Path,
        default=None,
        help="Override base (e.g. ~/.config/opencode for global install).",
    )
    args = parser.parse_args()
    target = Path(args.target)
    path = generate(target, config_root=args.config_root)
    try:
        rel = path.relative_to(target)
    except ValueError:
        rel = path
    print(f"Generated: {rel}")


if __name__ == "__main__":
    main()
