#!/usr/bin/env node
// Generate typed command builders for Swift/Kotlin/TypeScript by parsing
// `XxxCommand` interface blocks inside shared/src/protocol.ts.
//
// Output:
//   generated/protocol/AgentCommand.swift — Swift enum + var dictionary
//   generated/protocol/AgentCommand.kt    — Kotlin sealed class + toJson()
//   shared/src/command-builders.ts        — TS factory object (part of @agentdeck/shared build)
//
// The generator enforces the single source of truth: every command interface in
// protocol.ts with a `type: '<literal>'` tag gets a matching builder in every
// language, so field name typos (e.g. `response` vs `value`) become compile errors.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectDir = path.resolve(__dirname, "..");
const protocolPath = path.join(projectDir, "shared/src/protocol.ts");
const outDir = path.join(projectDir, "generated/protocol");

const source = fs.readFileSync(protocolPath, "utf8");

// ---- 1. Locate the PluginCommand union ----------------------------------

const unionMatch = source.match(/export\s+type\s+PluginCommand\s*=\s*([^;]+);/);
if (!unionMatch) {
  console.error("Could not find `export type PluginCommand = ...;`");
  process.exit(1);
}
const unionBody = unionMatch[1];
const unionMembers = unionBody
  .split("|")
  .map((s) => s.trim())
  .filter(Boolean);

// ---- 2. For each member, extract its interface fields -------------------

function extractInterface(name) {
  const re = new RegExp(
    `export\\s+interface\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`,
    "m"
  );
  const m = source.match(re);
  if (!m) throw new Error(`Cannot find interface ${name}`);

  // Split the interface body into top-level field declarations. A field
  // terminates at a `;` that lives outside any nested brace pair. This handles
  // multi-line literal unions (`'a' | 'b' | 'c'`) and nested object types
  // (`{ [key: string]: unknown }`).
  const body = m[1];
  const entries = [];
  let buf = "";
  let depth = 0;
  for (const ch of body) {
    if (ch === "{") depth++;
    if (ch === "}") depth--;
    if (ch === ";" && depth === 0) {
      entries.push(buf);
      buf = "";
      continue;
    }
    buf += ch;
  }
  if (buf.trim()) entries.push(buf);

  const fields = [];
  let tagValue = null;
  for (const rawEntry of entries) {
    const line = rawEntry
      .replace(/\/\*[\s\S]*?\*\//g, " ")
      .split("\n")
      .map((l) => l.replace(/\/\/.*$/, ""))
      .join(" ")
      .trim();
    if (!line) continue;
    const tagMatch = line.match(/^type\s*:\s*['"]([^'"]+)['"]$/);
    if (tagMatch) {
      tagValue = tagMatch[1];
      continue;
    }
    const fieldMatch = line.match(/^(\w+)\s*(\??)\s*:\s*([\s\S]+)$/);
    if (fieldMatch) {
      fields.push({
        name: fieldMatch[1],
        optional: fieldMatch[2] === "?",
        tsType: fieldMatch[3].replace(/\s+/g, " ").trim(),
      });
    }
  }
  if (!tagValue) throw new Error(`Interface ${name} has no literal type tag`);
  return { name, tag: tagValue, fields };
}

const commands = unionMembers.map(extractInterface);

// ---- 3. Field type mapping ---------------------------------------------

function swiftType(tsType) {
  if (/^['"].*['"]/.test(tsType.split("|")[0].trim())) return "String";
  const t = tsType.trim();
  if (t.startsWith("Array<")) return "[[String: Any]]";
  if (t === "string") return "String";
  // All current `number` fields in PluginCommand are integer (index, delta
  // ticks). Using Int keeps call sites clean — Swift will not implicitly
  // widen Int→Double.
  if (t === "number") return "Int";
  if (t === "boolean") return "Bool";
  if (t.startsWith("{")) return "[String: Any]";
  return "String";
}

function kotlinType(tsType) {
  if (/^['"].*['"]/.test(tsType.split("|")[0].trim())) return "String";
  const t = tsType.trim();
  if (t.startsWith("Array<")) return "List<Map<String, Any?>>";
  if (t === "string") return "String";
  if (t === "number") return "Int";
  if (t === "boolean") return "Boolean";
  if (t.startsWith("{")) return "Map<String, Any?>";
  return "String";
}

function tsType(raw) {
  // Keep the TS type verbatim; the output file imports the source types.
  return raw;
}

function camelCaseTag(tag) {
  return tag.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
}

// Case name is derived from the wire tag so `.respond` / `.sendPrompt` /
// `.sessionCommand` mirror the on-wire discriminant exactly.
function caseName(_interfaceName, tag) {
  return camelCaseTag(tag);
}

// ---- 4. Swift emission --------------------------------------------------

function emitSwift() {
  const lines = [
    "// AUTO-GENERATED by scripts/generate-command-builders.mjs — DO NOT EDIT.",
    "// Source of truth: shared/src/protocol.ts (PluginCommand union).",
    "//",
    "// Purpose: compile-time-safe construction of commands the Swift daemon",
    "// sends to session bridges. Prevents field-name typos (e.g. `response`",
    "// vs `value`) because each variant is a distinct enum case.",
    "",
    "import Foundation",
    "",
    "public enum AgentCommand: Equatable {",
  ];

  for (const cmd of commands) {
    const params = cmd.fields.map((f) => {
      const t = swiftType(f.tsType);
      const optional = f.optional ? "?" : "";
      return `${f.name}: ${t}${optional}`;
    });
    if (params.length === 0) {
      lines.push(`    case ${caseName(cmd.name, cmd.tag)}`);
    } else {
      lines.push(`    case ${caseName(cmd.name, cmd.tag)}(${params.join(", ")})`);
    }
  }

  lines.push("");
  lines.push("    /// JSON-serializable dictionary representation matching protocol.ts.");
  lines.push("    public var dictionary: [String: Any] {");
  lines.push("        switch self {");
  for (const cmd of commands) {
    const c = caseName(cmd.name, cmd.tag);
    if (cmd.fields.length === 0) {
      lines.push(`        case .${c}:`);
      lines.push(`            return ["type": "${cmd.tag}"]`);
      continue;
    }
    const binds = cmd.fields.map((f) => `let ${f.name}`).join(", ");
    lines.push(`        case .${c}(${binds}):`);
    lines.push(`            var dict: [String: Any] = ["type": "${cmd.tag}"]`);
    for (const f of cmd.fields) {
      if (f.optional) {
        lines.push(`            if let ${f.name} = ${f.name} { dict["${f.name}"] = ${f.name} }`);
      } else {
        lines.push(`            dict["${f.name}"] = ${f.name}`);
      }
    }
    lines.push("            return dict");
  }
  lines.push("        }");
  lines.push("    }");

  lines.push("");
  lines.push("    /// Discriminant tag as it appears over the wire.");
  lines.push("    public var typeTag: String {");
  lines.push("        switch self {");
  for (const cmd of commands) {
    const c = caseName(cmd.name, cmd.tag);
    if (cmd.fields.length === 0) {
      lines.push(`        case .${c}: return "${cmd.tag}"`);
    } else {
      lines.push(`        case .${c}: return "${cmd.tag}"`);
    }
  }
  lines.push("        }");
  lines.push("    }");

  // Manual Equatable for cases containing [String: Any] (SessionCommand.command).
  lines.push("");
  lines.push("    public static func == (lhs: AgentCommand, rhs: AgentCommand) -> Bool {");
  lines.push("        return NSDictionary(dictionary: lhs.dictionary).isEqual(to: rhs.dictionary)");
  lines.push("    }");

  lines.push("}");
  lines.push("");

  fs.writeFileSync(path.join(outDir, "AgentCommand.swift"), lines.join("\n"));
  console.log("   → AgentCommand.swift");
}

// ---- 5. Kotlin emission -------------------------------------------------

function emitKotlin() {
  const lines = [
    "// AUTO-GENERATED by scripts/generate-command-builders.mjs — DO NOT EDIT.",
    "// Source of truth: shared/src/protocol.ts (PluginCommand union).",
    "",
    "package dev.agentdeck.generated",
    "",
    "import kotlinx.serialization.json.Json",
    "import kotlinx.serialization.json.JsonElement",
    "import kotlinx.serialization.json.JsonObject",
    "import kotlinx.serialization.json.JsonPrimitive",
    "import kotlinx.serialization.json.JsonNull",
    "",
    "sealed class AgentCommand {",
    "    abstract val typeTag: String",
    "    abstract fun toJson(): String",
    "",
  ];

  for (const cmd of commands) {
    const c = caseName(cmd.name, cmd.tag);
    const className = c.charAt(0).toUpperCase() + c.slice(1);
    const params = cmd.fields.map((f) => {
      const t = kotlinType(f.tsType);
      const nullable = f.optional ? "?" : "";
      const defaultValue = f.optional ? " = null" : "";
      return `val ${f.name}: ${t}${nullable}${defaultValue}`;
    });
    const decl =
      params.length === 0
        ? `    object ${className} : AgentCommand() {`
        : `    data class ${className}(${params.join(", ")}) : AgentCommand() {`;
    lines.push(decl);
    lines.push(`        override val typeTag: String = "${cmd.tag}"`);
    lines.push(`        override fun toJson(): String {`);
    lines.push(`            val buf = StringBuilder()`);
    lines.push(`            buf.append("{\\"type\\":\\"${cmd.tag}\\"")`);
    for (const f of cmd.fields) {
      const emit = `buf.append(",\\"${f.name}\\":").append(encode(${f.name}))`;
      if (f.optional) {
        lines.push(`            if (${f.name} != null) ${emit}`);
      } else {
        lines.push(`            ${emit}`);
      }
    }
    lines.push(`            buf.append("}")`);
    lines.push(`            return buf.toString()`);
    lines.push(`        }`);
    lines.push(`    }`);
    lines.push("");
  }

  lines.push("    companion object {");
  lines.push("        private val jsonFmt = Json { encodeDefaults = false }");
  lines.push("        internal fun encode(v: Any?): String = when (v) {");
  lines.push("            null -> \"null\"");
  lines.push("            is String -> jsonFmt.encodeToString(kotlinx.serialization.serializer<String>(), v)");
  lines.push("            is Number -> v.toString()");
  lines.push("            is Boolean -> v.toString()");
  lines.push("            is Map<*, *> -> {");
  lines.push("                val parts = v.entries.joinToString(\",\") { (k, vv) ->");
  lines.push("                    val keyJson = jsonFmt.encodeToString(kotlinx.serialization.serializer<String>(), k.toString())");
  lines.push("                    keyJson + \":\" + encode(vv)");
  lines.push("                }");
  lines.push("                \"{\" + parts + \"}\"");
  lines.push("            }");
  lines.push("            is Iterable<*> -> v.joinToString(prefix = \"[\", postfix = \"]\") { encode(it) }");
  lines.push("            else -> jsonFmt.encodeToString(kotlinx.serialization.serializer<String>(), v.toString())");
  lines.push("        }");
  lines.push("    }");
  lines.push("}");
  lines.push("");

  fs.writeFileSync(path.join(outDir, "AgentCommand.kt"), lines.join("\n"));
  console.log("   → AgentCommand.kt");
}

// ---- 6. TypeScript emission ---------------------------------------------

function emitTypeScript() {
  const lines = [
    "// AUTO-GENERATED by scripts/generate-command-builders.mjs — DO NOT EDIT.",
    "// Source of truth: shared/src/protocol.ts (PluginCommand union).",
    "//",
    "// TypeScript already has the typed discriminated union; this file provides",
    "// matching factory helpers so callers in all three languages share the same",
    "// API shape (`AgentCommand.respond(value)` / `agentCommand.respond(value)`).",
    "",
    "import type {",
  ];
  for (const cmd of commands) lines.push(`  ${cmd.name},`);
  lines.push('} from "./protocol.js";');
  lines.push("");
  lines.push("export const agentCommand = {");
  for (const cmd of commands) {
    const c = caseName(cmd.name, cmd.tag);
    if (cmd.fields.length === 0) {
      lines.push(`  ${c}(): ${cmd.name} {`);
      lines.push(`    return { type: "${cmd.tag}" };`);
      lines.push(`  },`);
      continue;
    }
    const paramList = cmd.fields
      .map((f) => {
        const optional = f.optional ? "?" : "";
        return `${f.name}${optional}: ${tsType(f.tsType)}`;
      })
      .join(", ");
    lines.push(`  ${c}(${paramList}): ${cmd.name} {`);
    lines.push(
      `    return { type: "${cmd.tag}", ${cmd.fields
        .map((f) => f.name)
        .join(", ")} };`
    );
    lines.push(`  },`);
  }
  lines.push("} as const;");
  lines.push("");

  const tsOut = path.join(projectDir, "shared/src/command-builders.ts");
  fs.writeFileSync(tsOut, lines.join("\n"));
  console.log("   → shared/src/command-builders.ts");
}

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

fs.mkdirSync(outDir, { recursive: true });
console.log(`=== Command builders (from ${commands.length} commands) ===`);
emitSwift();
emitKotlin();
emitTypeScript();
