import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ErrorCodes, PROTOCOL_VERSION, ProtocolSchemas } from "../src/gateway/protocol/schema.js";

type JsonSchema = {
  type?: string | string[];
  properties?: Record<string, JsonSchema>;
  required?: string[];
  items?: JsonSchema;
  enum?: string[];
  patternProperties?: Record<string, JsonSchema>;
};

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const outPaths = [
  path.join(repoRoot, "apps", "macos", "Sources", "OpenClawProtocol", "GatewayModels.swift"),
  path.join(
    repoRoot,
    "apps",
    "shared",
    "OpenClawKit",
    "Sources",
    "OpenClawProtocol",
    "GatewayModels.swift",
  ),
];

const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\n// swiftlint:disable file_length\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\n\npublic enum ErrorCode: String, Codable, Sendable {\n${Object.values(
  ErrorCodes,
)
  .map((c) => `    case ${camelCase(c)} = "${c}"`)
  .join("\n")}\n}\n`;

const reserved = new Set([
  "associatedtype",
  "class",
  "deinit",
  "enum",
  "extension",
  "fileprivate",
  "func",
  "import",
  "init",
  "inout",
  "internal",
  "let",
  "open",
  "operator",
  "private",
  "precedencegroup",
  "protocol",
  "public",
  "rethrows",
  "static",
  "struct",
  "subscript",
  "typealias",
  "var",
]);

function camelCase(input: string) {
  return input
    .replace(/[^a-zA-Z0-9]+/g, " ")
    .trim()
    .toLowerCase()
    .split(/\s+/)
    .map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
    .join("");
}

function safeName(name: string) {
  const cc = camelCase(name.replace(/-/g, "_"));
  if (reserved.has(cc)) {
    return `_${cc}`;
  }
  return cc;
}

// filled later once schemas are loaded
const schemaNameByObject = new Map<object, string>();

function swiftType(schema: JsonSchema, required: boolean): string {
  const t = schema.type;
  const isOptional = !required;
  let base: string;
  const named = schemaNameByObject.get(schema as object);
  if (named) {
    base = named;
  } else if (t === "string") {
    base = "String";
  } else if (t === "integer") {
    base = "Int";
  } else if (t === "number") {
    base = "Double";
  } else if (t === "boolean") {
    base = "Bool";
  } else if (t === "array") {
    base = `[${swiftType(schema.items ?? { type: "Any" }, true)}]`;
  } else if (schema.enum) {
    base = "String";
  } else if (schema.patternProperties) {
    base = "[String: AnyCodable]";
  } else if (t === "object") {
    base = "[String: AnyCodable]";
  } else {
    base = "AnyCodable";
  }
  return isOptional ? `${base}?` : base;
}

function emitStruct(name: string, schema: JsonSchema): string {
  const props = schema.properties ?? {};
  const required = new Set(schema.required ?? []);
  const lines: string[] = [];
  if (Object.keys(props).length === 0) {
    return `public struct ${name}: Codable, Sendable {}\n`;
  }
  lines.push(`public struct ${name}: Codable, Sendable {`);
  const codingKeys: string[] = [];
  for (const [key, propSchema] of Object.entries(props)) {
    const propName = safeName(key);
    const propType = swiftType(propSchema, required.has(key));
    lines.push(`    public let ${propName}: ${propType}`);
    if (propName !== key) {
      codingKeys.push(`        case ${propName} = "${key}"`);
    } else {
      codingKeys.push(`        case ${propName}`);
    }
  }
  lines.push(
    "\n    public init(\n" +
      Object.entries(props)
        .map(([key, prop]) => {
          const propName = safeName(key);
          const req = required.has(key);
          return `        ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`;
        })
        .join(",\n") +
      ")\n" +
      "    {\n" +
      Object.entries(props)
        .map(([key]) => {
          const propName = safeName(key);
          return `        self.${propName} = ${propName}`;
        })
        .join("\n") +
      "\n    }\n\n" +
      "    private enum CodingKeys: String, CodingKey {\n" +
      codingKeys.join("\n") +
      "\n    }\n}",
  );
  lines.push("");
  return lines.join("\n");
}

function emitGatewayFrame(): string {
  const cases = ["req", "res", "event"];
  const associated: Record<string, string> = {
    req: "RequestFrame",
    res: "ResponseFrame",
    event: "EventFrame",
  };
  const caseLines = cases.map((c) => `    case ${safeName(c)}(${associated[c]})`);
  const initLines = `
    private enum CodingKeys: String, CodingKey {
        case type
    }

    public init(from decoder: Decoder) throws {
        let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
        let type = try typeContainer.decode(String.self, forKey: .type)
        switch type {
        case "req":
            self = try .req(RequestFrame(from: decoder))
        case "res":
            self = try .res(ResponseFrame(from: decoder))
        case "event":
            self = try .event(EventFrame(from: decoder))
        default:
            let container = try decoder.singleValueContainer()
            let raw = try container.decode([String: AnyCodable].self)
            self = .unknown(type: type, raw: raw)
        }
    }

    public func encode(to encoder: Encoder) throws {
        switch self {
        case let .req(v):
            try v.encode(to: encoder)
        case let .res(v):
            try v.encode(to: encoder)
        case let .event(v):
            try v.encode(to: encoder)
        case let .unknown(_, raw):
            var container = encoder.singleValueContainer()
            try container.encode(raw)
        }
    }
`;

  return [
    "public enum GatewayFrame: Codable, Sendable {",
    ...caseLines,
    "    case unknown(type: String, raw: [String: AnyCodable])",
    initLines.trimEnd(),
    "}",
    "",
  ].join("\n");
}

async function generate() {
  const definitions = Object.entries(ProtocolSchemas) as Array<[string, JsonSchema]>;

  for (const [name, schema] of definitions) {
    schemaNameByObject.set(schema as object, name);
  }

  const parts: string[] = [];
  parts.push(header);

  // Value structs
  for (const [name, schema] of definitions) {
    if (name === "GatewayFrame") {
      continue;
    }
    if (schema.type === "object") {
      parts.push(emitStruct(name, schema));
    }
  }

  // Frame enum must come after payload structs
  parts.push(emitGatewayFrame());

  const content = parts.join("\n");
  for (const outPath of outPaths) {
    await fs.mkdir(path.dirname(outPath), { recursive: true });
    await fs.writeFile(outPath, content);
    console.log(`wrote ${outPath}`);
  }
}

generate().catch((err) => {
  console.error(err);
  process.exit(1);
});
