#!/usr/bin/env node
/**
 * ArkTS Protobuf Generator
 *
 * - Loads .proto files, resolves types/services and emits ArkTS (.ets) code
 * - Options control int64 mode, JSON API generation, doc comments, packing and runtime bundling
 * - Produces per-package directories with messages/enums/services and index exports
 */
import fs from 'fs'
import path from 'path'
import protobuf from 'protobufjs'

/**
 * Parse CLI args and normalize options with safe defaults
 */
function parseArgs() {
  const args = process.argv.slice(2)
  const opts = { in: './protos', out: './packages', int64: 'bigint', packed: 'on', delimited: 'off', docs: 'on', omitDefaults: 'off', json: 'off', jsonEnum: 'names', jsonStrict: 'off', concurrent: 'off', namespaceAsFile: 'off', bundleRuntime: 'on', runtimeDir: 'protobuf-core' }
  for (let i = 0; i < args.length; i++) {
    const a = args[i]
    if (a === '--in') opts.in = args[++i]
    else if (a === '--out') opts.out = args[++i]
    else if (a === '--int64') opts.int64 = args[++i]
    else if (a === '--packed') opts.packed = args[++i]
    else if (a === '--delimited') opts.delimited = args[++i]
    else if (a === '--docs') opts.docs = args[++i]
    else if (a === '--omit-defaults') opts.omitDefaults = args[++i]
    else if (a === '--json') opts.json = args[++i]
    else if (a === '--json-strict') opts.jsonStrict = args[++i]
    else if (a === '--json-enum') opts.jsonEnum = args[++i]
    else if (a === '--concurrent') opts.concurrent = args[++i]
    else if (a === '--namespace-as-file') opts.namespaceAsFile = args[++i]
    else if (a === '--bundle-runtime') opts.bundleRuntime = args[++i]
    else if (a === '--runtime-dir') opts.runtimeDir = args[++i]
  }
  if (opts.int64 === 'long') opts.int64 = 'bigint'
  if (!['bigint','number'].includes(opts.int64)) opts.int64 = 'bigint'
  if (!['on','off'].includes(opts.packed)) opts.packed = 'on'
  if (!['on','off'].includes(opts.docs)) opts.docs = 'on'
  if (!['on','off'].includes(opts.omitDefaults)) opts.omitDefaults = 'off'
  if (!['on','off'].includes(opts.json)) opts.json = 'off'
  if (!['on','off'].includes(opts.jsonStrict)) opts.jsonStrict = 'off'
  if (!['names','numbers','accept-both'].includes(opts.jsonEnum)) opts.jsonEnum = 'names'
  if (!['off','sendable'].includes(opts.concurrent)) opts.concurrent = 'off'
  if (!['on','off'].includes(opts.namespaceAsFile)) opts.namespaceAsFile = 'off'
  if (!['on','off'].includes(opts.bundleRuntime)) opts.bundleRuntime = 'on'
  if (!opts.runtimeDir || typeof opts.runtimeDir !== 'string') opts.runtimeDir = 'protobuf-core'
  return opts
}

/**
 * Ensure directory exists (mkdir -p)
 */
function ensureDir(p) {
  if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true })
}
/**
 * Derive namespace segments from a fullName (without the trailing type name)
 */
function packageSegmentsFromObj(obj) {
  const fn = (obj.fullName || '').replace(/^\./, '')
  const parts = fn.split('.')
  return parts.length > 1 ? parts.slice(0, parts.length - 1) : ['default']
}
/**
 * Resolve output base directory for a namespace depending on layout mode
 */
function outBaseDirForSegments(opts, segs) {
  return opts.namespaceAsFile === 'on' ? path.join(opts.out, ...segs) : path.join(opts.out, segs[0] || 'default')
}
/**
 * Compute output file path (messages/enums/services) for a fully-qualified name
 */
function outPathForFull(opts, fullName, kind) {
  const parts = fullName.replace(/^\./,'').split('.')
  const segs = parts.length > 1 ? parts.slice(0, parts.length - 1) : ['default']
  const name = parts[parts.length - 1]
  const base = outBaseDirForSegments(opts, segs)
  return path.join(base, kind, name + '.ets')
}

/**
 * Convert raw comment text into ArkTS JSDoc block lines
 */
function formatDoc(comment) {
  if (!comment) return []
  const safe = String(comment).replace(/\*\//g, '*\\/')
  const lines = safe.split(/\r?\n/)
  const out = ['/**']
  for (const l of lines) out.push(` * ${l}`)
  out.push(' */')
  return out
}

/**
 * Map protobuf field type to ArkTS type name
 */
function typeFor(field, int64Mode) {
  switch (field.type) {
    case 'double':
    case 'float':
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32':
      return 'number'
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64':
      return int64Mode === 'number' ? 'number' : 'bigint'
    case 'bool':
      return 'boolean'
    case 'string':
      return 'string'
    case 'bytes':
      return 'Uint8Array'
    default:
      if (field.resolvedType) {
        return field.resolvedType.name
      }
      return 'number'
  }
}
/**
 * Sanitize identifier to avoid ArkTS reserved keywords
 */
function safeIdent(name) {
  const reserved = new Set(['export','default','class','enum','switch','case','function','let','const','var','implements','interface','package','private','protected','public','static','yield','await','delete','in','of','return'])
  return reserved.has(name) ? name + '_' : name
}
/**
 * Compute protobuf wire type for the given field
 */
function wireTypeFor(field) {
  if (field.resolvedType) {
    if (field.resolvedType instanceof protobuf.Enum) return 0
    return 2
  }
  switch (field.type) {
    case 'string':
    case 'bytes':
      return 2
    case 'float':
    case 'fixed32':
    case 'sfixed32':
      return 5
    case 'double':
    case 'fixed64':
    case 'sfixed64':
      return 1
    default:
      return 0
  }
}
/**
 * Whether a field is implicitly packable when repeated
 */
function isPackable(f) {
  if (f.resolvedType) return f.resolvedType instanceof protobuf.Enum
  switch (f.type) {
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32':
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64':
    case 'float':
    case 'double':
    case 'bool':
      return true
    default:
      return false
  }
}
/**
 * Select Reader method name for the field (honors int64 mode)
 */
function readerMethodName(f, int64Mode) {
  if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) return 'int32'
  switch (f.type) {
    case 'string': return 'string'
    case 'bytes': return 'bytes'
    case 'bool': return 'bool'
    case 'int32':
    case 'uint32': return 'int32'
    case 'sint32': return 'sint32'
    case 'fixed32': return 'fixed32'
    case 'sfixed32': return 'sfixed32'
    case 'int64':
      return int64Mode === 'number' ? 'int64Number' : 'int64'
    case 'uint64':
      return int64Mode === 'number' ? 'uint64Number' : 'uint64'
    case 'sint64': return 'sint64'
    case 'fixed64': return 'fixed64'
    case 'sfixed64': return 'sfixed64'
    case 'float': return 'float'
    case 'double': return 'double'
    default: return 'int32'
  }
}

/**
 * Get JSON type for a protobuf field (for JSON interface generation)
 * Returns the TypeScript type used in JSON interfaces
 */
function getJsonType(field, int64Mode, aliasMap) {
  if (field.resolvedType) {
    if (field.resolvedType instanceof protobuf.Enum) {
      // Enum: string (when using names mode) or number
      return 'string | number'
    } else {
      // Message: use the JSON interface type
      const full = (field.resolvedType.fullName || '').replace(/^\./, '')
      const alias = (aliasMap.get(full) && aliasMap.get(full).local) || field.resolvedType.name
      return `${alias}JSON`
    }
  }

  // Scalar types
  switch (field.type) {
    case 'string': return 'string'
    case 'bool': return 'boolean'
    case 'bytes': return 'string' // Base64 encoded
    case 'double':
    case 'float':
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32':
      return 'number'
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64':
      return int64Mode === 'number' ? 'number' : 'string'
    default:
      return 'Object'
  }
}

/**
 * Get map key type for JSON (always string)
 */
function getJsonMapKeyType(field) {
  // In JSON, all map keys must be strings
  return 'string'
}

/**
 * Generate JSON interface for a message
 * This interface defines the shape of JSON objects for toJson/fromJson
 *
 * IMPORTANT: For ArkTS compatibility, Map fields are represented as arrays of {key, value} pairs
 * instead of objects, since ArkTS doesn't allow index access like obj[key]
 */
function generateJsonInterface(className, fields, oneofs, int64Mode, aliasMap) {
  const lines = []
  lines.push(`export interface ${className}JSON {`)

  // Regular fields
  for (const f of fields) {
    const fname = f.name
    let jsonType

    if (f.map) {
      // Map fields: Use array of {key, value} pairs for ArkTS compatibility
      // Standard Protobuf JSON uses objects, but ArkTS doesn't allow obj[key] access
      const keyType = getJsonMapKeyType(f)
      const valueType = getJsonType(f, int64Mode, aliasMap)
      jsonType = `Array<{key: ${keyType}, value: ${valueType}}>`
    } else if (f.repeated) {
      // Repeated fields: Array<Type>
      const elemType = getJsonType(f, int64Mode, aliasMap)
      jsonType = `Array<${elemType}>`
    } else {
      // Singular fields
      jsonType = getJsonType(f, int64Mode, aliasMap)
    }

    // All fields are optional in JSON
    lines.push(`  ${fname}?: ${jsonType}`)
  }

  // Oneof fields - each variant appears as optional field in JSON
  // (only one will be set at a time, but JSON structure doesn't enforce this)
  for (const g of oneofs) {
    // Oneof fields are already included in regular fields
    // No additional work needed here
  }

  lines.push(`}`)
  return lines
}

/**
 * Generate CreateInput interface for create() method
 * ArkTS doesn't allow object literal types, so we must define an interface
 */
function generateCreateInterface(className, fields, oneofs, int64Mode, aliasMap) {
  const lines = []
  lines.push(`export interface ${className}CreateInput {`)

  // Regular fields - all optional with the actual message field type
  for (const f of fields) {
    const fnameSafe = safeIdent(f.name)
    const ftype = f.resolvedType
      ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name)
      : typeFor(f, int64Mode)
    const finalType = f.repeated ? `${ftype}[]` : ftype
    lines.push(`  ${fnameSafe}?: ${finalType}`)
  }

  // Oneof fields
  for (const g of oneofs) {
    lines.push(`  ${g.name}?: { case: string; value?: Object }`)
  }

  lines.push(`}`)
  return lines
}

/**
 * Generate toJson method body for ArkTS compatibility
 * Uses dot notation instead of index access
 */
function generateToJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg) {
  const lines = []
  lines.push(`  static toJson(m: ${className}): ${className}JSON {`)
  lines.push(`    const o: ${className}JSON = {}`)

  for (const f of fields) {
    const fname = f.name
    const fnameSafe = safeIdent(fname)
    const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined

    if (f.map) {
      // Map fields: convert to array of {key, value} pairs
      lines.push(`    if (m.${fnameSafe}.size > 0) {`)
      lines.push(`      const arr: Array<{key: string, value: any}> = []`)
      lines.push(`      for (const [k, v] of m.${fnameSafe}.entries()) {`)

      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Message value
        lines.push(`        arr.push({key: String(k), value: ${alias}.toJson(v)})`)
      } else {
        // Scalar or enum value
        const valueExpr = getJsonValueExpression('v', f, int64Mode, alias, jsonEnumMode)
        lines.push(`        arr.push({key: String(k), value: ${valueExpr}})`)
      }

      lines.push(`      }`)
      lines.push(`      o.${fname} = arr`)
      lines.push(`    }`)
    } else if (f.repeated) {
      // Repeated fields
      lines.push(`    if (m.${fnameSafe}.length > 0) {`)

      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Repeated message
        lines.push(`      o.${fname} = m.${fnameSafe}.map(v => ${alias}.toJson(v))`)
      } else {
        // Repeated scalar or enum
        const mapExpr = getJsonValueExpression('v', f, int64Mode, alias, jsonEnumMode)
        lines.push(`      o.${fname} = m.${fnameSafe}.map(v => ${mapExpr})`)
      }

      lines.push(`    }`)
    } else {
      // Singular fields
      const defaultCheck = getFieldDefaultCheck(`m.${fnameSafe}`, f, int64Mode)
      lines.push(`    if (${defaultCheck}) {`)

      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Message field
        lines.push(`      o.${fname} = ${alias}.toJson(m.${fnameSafe})`)
      } else {
        // Scalar or enum field
        const valueExpr = getJsonValueExpression(`m.${fnameSafe}`, f, int64Mode, alias, jsonEnumMode)
        lines.push(`      o.${fname} = ${valueExpr}`)
      }

      lines.push(`    }`)
    }
  }

  // Handle oneof fields
  for (const g of oneofs) {
    lines.push(`    if (m.${g.name}) {`)
    lines.push(`      switch (m.${g.name}.kind) {`)
    for (const fname of g.names) {
      const f = msg.fields[fname]
      const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
      const valueExpr = getJsonValueExpression(`m.${g.name}.${fname}`, f, int64Mode, alias, jsonEnumMode)
      lines.push(`        case '${fname}': o.${fname} = ${valueExpr}; break`)
    }
    lines.push(`      }`)
    lines.push(`    }`)
  }

  lines.push(`    return o`)
  lines.push(`  }`)
  return lines
}

/**
 * Get expression to convert a value to JSON format
 */
function getJsonValueExpression(valueRef, field, int64Mode, alias, jsonEnumMode) {
  if (field.resolvedType && (field.resolvedType instanceof protobuf.Enum)) {
    // Enum
    if (jsonEnumMode === 'names') {
      return `${alias}_JSON_N2S.get(${valueRef} as number) ?? String(${valueRef} as number)`
    } else {
      return `${valueRef} as number`
    }
  }

  switch (field.type) {
    case 'bytes':
      return `encodeBase64(${valueRef} as Uint8Array)`
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64':
      return int64Mode === 'number' ? `${valueRef} as number` : `(${valueRef} as bigint).toString()`
    default:
      return valueRef
  }
}

/**
 * Get check expression for non-default value
 */
function getFieldDefaultCheck(valueRef, field, int64Mode) {
  if (field.resolvedType) {
    return `${valueRef} !== null`
  }

  const t = typeFor(field, int64Mode)
  switch (t) {
    case 'string': return `${valueRef} !== ''`
    case 'boolean': return `${valueRef} !== false`
    case 'number': return `${valueRef} !== 0`
    case 'bigint': return `${valueRef} !== 0n`
    case 'Uint8Array': return `${valueRef}.length > 0`
    default: return `${valueRef} !== null`
  }
}

/**
 * Generate fromJson method body for ArkTS compatibility
 */
function generateFromJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg) {
  const lines = []
  lines.push(`  static fromJson(j: ${className}JSON): ${className} {`)
  lines.push(`    const m = new ${className}()`)

  for (const f of fields) {
    const fname = f.name
    const fnameSafe = safeIdent(fname)
    const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined

    lines.push(`    if (j.${fname} !== undefined) {`)

    if (f.map) {
      // Map fields: convert from array of {key, value} pairs
      lines.push(`      for (const entry of j.${fname}) {`)

      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Message value
        lines.push(`        m.${fnameSafe}.set(entry.key, ${alias}.fromJson(entry.value))`)
      } else {
        // Scalar or enum value
        const valueExpr = getFromJsonValueExpression('entry.value', f, int64Mode, alias, jsonEnumMode)
        lines.push(`        m.${fnameSafe}.set(entry.key, ${valueExpr})`)
      }

      lines.push(`      }`)
    } else if (f.repeated) {
      // Repeated fields
      lines.push(`      for (const item of j.${fname}) {`)

      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Repeated message
        lines.push(`        m.${fnameSafe}.push(${alias}.fromJson(item))`)
      } else {
        // Repeated scalar or enum
        const valueExpr = getFromJsonValueExpression('item', f, int64Mode, alias, jsonEnumMode)
        lines.push(`        m.${fnameSafe}.push(${valueExpr})`)
      }

      lines.push(`      }`)
    } else {
      // Singular fields
      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        // Message field
        lines.push(`      m.${fnameSafe} = ${alias}.fromJson(j.${fname})`)
      } else {
        // Scalar or enum field
        const valueExpr = getFromJsonValueExpression(`j.${fname}`, f, int64Mode, alias, jsonEnumMode)
        lines.push(`      m.${fnameSafe} = ${valueExpr}`)
      }
    }

    lines.push(`    }`)
  }

  lines.push(`    return m`)
  lines.push(`  }`)
  return lines
}

/**
 * Get expression to convert from JSON value
 */
function getFromJsonValueExpression(valueRef, field, int64Mode, alias, jsonEnumMode) {
  if (field.resolvedType && (field.resolvedType instanceof protobuf.Enum)) {
    // Enum
    if (jsonEnumMode === 'names') {
      return `${alias}_JSON_S2N.get(String(${valueRef})) as ${alias}`
    } else if (jsonEnumMode === 'accept-both') {
      return `typeof ${valueRef} === 'number' ? ${valueRef} as ${alias} : ${alias}_JSON_S2N.get(String(${valueRef})) as ${alias}`
    } else {
      return `Number(${valueRef}) as ${alias}`
    }
  }

  switch (field.type) {
    case 'bytes':
      return `decodeBase64(String(${valueRef}))`
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64':
      return int64Mode === 'number' ? `Number(${valueRef})` : `BigInt(String(${valueRef}))`
    case 'string':
      return `String(${valueRef})`
    case 'bool':
      return `Boolean(${valueRef})`
    case 'double':
    case 'float':
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32':
      return `Number(${valueRef})`
    default:
      return valueRef
  }
}

/**
 * Emit ArkTS message class with encode/decode/verify and optional JSON methods
 */
function renderMessage(pkgDir, pkgName, msg, int64Mode, packedOn, docsOn, omitDefaultsOn, jsonOn, commentOf, jsonEnumMode, jsonStrict, opts) {
  const className = msg.name
  const fields = msg.fieldsArray
  const oneofs = msg.oneofs ? Object.entries(msg.oneofs).map(([name, oo]) => ({ name, names: oo.oneof })) : []
  const oneofsWithComments = (msg.oneofsArray || []).map(o => ({ name: o.name, comment: o.comment }))
  const currMsgDir = path.join(pkgDir, 'messages')
  const rtRelRaw = path.relative(currMsgDir, path.join(opts.out, opts.runtimeDir)).replace(/\\/g,'/')
  const rtRel = rtRelRaw.startsWith('.') ? rtRelRaw : './' + rtRelRaw
  const isTimestamp = msg.fullName === '.google.protobuf.Timestamp'
  const isDuration = msg.fullName === '.google.protobuf.Duration'
  const isAny = msg.fullName === '.google.protobuf.Any'
  
  const utilImports = new Set()
  // Add required utility imports based on field types
  for (const f of fields) {
      if (f.type === 'bytes') {
          utilImports.add('encodeBase64')
          utilImports.add('decodeBase64')
      }
  }

  if (jsonOn) {
      if (isTimestamp) { utilImports.add('timestampToJson'); utilImports.add('timestampFromJson') }
      if (isDuration) { utilImports.add('durationToJson'); utilImports.add('durationFromJson') }
      if (isAny) { utilImports.add('encodeBase64'); utilImports.add('decodeBase64') }
      if (msg.fullName === '.google.protobuf.BytesValue') { utilImports.add('encodeBase64'); utilImports.add('decodeBase64') }
  }
  
  const baseImports = ["Message", "Visitor", "Reader", "Writer", "BinaryEncodingVisitor"]
  if (jsonOn) baseImports.push("JsonEncodingVisitor")
  
  const imports = []
  if (utilImports.size > 0) {
      imports.push(`import { ${baseImports.join(', ')}, ${Array.from(utilImports).join(', ')} } from '${rtRel}'`)
  } else {
      imports.push(`import { ${baseImports.join(', ')} } from '${rtRel}'`)
  }
  const aliasMap = new Map()
  const usedNames = new Map() // name -> fullName
  const depList = []
  for (const f of fields) {
    if (!f.resolvedType) continue
    const full = (f.resolvedType.fullName || '').replace(/^\./, '')
    // skip self-import when a field references the enclosing message type
    const selfFull = (msg.fullName || '').replace(/^\./, '')
    if (full === selfFull) continue
    if (aliasMap.has(full)) continue
    const isEnum = f.resolvedType instanceof protobuf.Enum
    const depPkg = full.split('.')[0] || 'default'
    const relRaw = path.relative(currMsgDir, outPathForFull(opts, full, isEnum ? 'enums' : 'messages')).replace(/\\/g,'/')
    const relNoExt = relRaw.replace(/\.ets$/,'')
    const rel = relNoExt.startsWith('.') ? relNoExt : './' + relNoExt
    let local = f.resolvedType.name
    if (usedNames.has(local) && usedNames.get(local) !== full) local = `${local}_${depPkg}`
    usedNames.set(local, full)
    aliasMap.set(full, { local, base: f.resolvedType.name, rel, isEnum })
    depList.push({ local, base: f.resolvedType.name, rel, isEnum })
  }
  const lines = []
  lines.push(...imports)
  if (depList.length) {
    depList.sort((a,b)=> a.local.localeCompare(b.local) || a.rel.localeCompare(b.rel))
    for (const d of depList) {
      if (jsonOn) {
        if (d.local === d.base) {
          if (d.isEnum) {
            lines.push(`import { ${d.base} } from '${d.rel}'`)
          } else {
            lines.push(`import { ${d.base} } from '${d.rel}'`)
            lines.push(`import { ${d.base}JSON } from '${d.rel}'`)
          }
        } else {
          if (d.isEnum) {
            lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
          } else {
            lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
            lines.push(`import { ${d.base}JSON as ${d.local}JSON } from '${d.rel}'`)
          }
        }
      } else {
        if (d.local === d.base) lines.push(`import { ${d.base} } from '${d.rel}'`)
        else lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
      }
    }
  }
  if (jsonOn && jsonEnumMode === 'names') {
    const emitted = new Set()
    for (const f of fields) {
      if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) {
        const full = (f.resolvedType.fullName || '').replace(/^\./,'')
        const alias = (aliasMap.get(full) && aliasMap.get(full).local) || f.resolvedType.name
        if (emitted.has(alias)) continue
        emitted.add(alias)
        const values = Object.entries(f.resolvedType.values)
        const n2sPairs = values.map(([k,v])=> `  [${v}, ${JSON.stringify(k)}]`).join(',\n')
        const s2nPairs = values.map(([k,v])=> `  [${JSON.stringify(k)}, ${v}]`).join(',\n')
        lines.push(`const ${alias}_JSON_N2S = new Map<number, string>([\n${n2sPairs}\n])`)
        lines.push(`const ${alias}_JSON_S2N = new Map<string, number>([\n${s2nPairs}\n])`)
      }
    }
  }
  const fullName = (msg.fullName || '').replace(/^\./,'')
  if (docsOn) {
    const mc = commentOf ? commentOf(fullName) : (msg.comment || (msg.commentRange ? msg.commentRange.leadingComments : ''))
    if (mc) lines.push(...formatDoc(mc))
  }
  if (opts.concurrent === 'sendable') lines.push('@Sendable')

  // Generate JSON interface if JSON is enabled
  if (jsonOn) {
    const jsonInterfaceLines = generateJsonInterface(className, fields, oneofs, int64Mode, aliasMap)
    lines.push(...jsonInterfaceLines)
    lines.push('')
  }

  // Generate CreateInput interface for create() method to avoid object literal types
  const createInterfaceLines = generateCreateInterface(className, fields, oneofs, int64Mode, aliasMap)
  lines.push(...createInterfaceLines)
  lines.push('')

  // No longer generate *Init interface - use Partial<T> instead
  lines.push(`export class ${className} extends Message {`)
  // Add $typeName for reflection
  lines.push(`  readonly $typeName: string = '${fullName}'`)
  lines.push(`  static readonly typeName: string = '${fullName}'`)
  lines.push('')
  const localType = (ff) => ff.resolvedType ? (aliasMap.get((ff.resolvedType.fullName||'').replace(/^\./,''))?.local || ff.resolvedType.name) : typeFor(ff, int64Mode)
  // Helper to get default value for field initialization
  const getFieldDefault = (f) => {
    if (f.map) return `new Map()`
    if (f.repeated) return `[]`
    const t = localType(f)
    if (f.resolvedType) return 'null'  // Messages/enums default to null
    switch (t) {
      case 'string': return `''`
      case 'boolean': return 'false'
      case 'number': return '0'
      case 'bigint': return '0n'
      case 'Uint8Array': return 'new Uint8Array(0)'
      default: return 'null'
    }
  }
  for (const f of fields) {
    const t = localType(f)
    const base = f.map ? `Map<${mapKeyType(f, int64Mode)}, ${localType(f)}>` : (f.repeated ? `Array<${t}>` : (f.resolvedType ? `${t} | null` : t))
    const fnameSafe = safeIdent(f.name)
    if (docsOn) {
      const fc = commentOf ? commentOf(`${fullName}#${f.name}`) : (f.comment || (msg.comments && msg.comments[f.name]))
      if (fc) lines.push(...formatDoc(fc).map(s => '  ' + s))
    }
    lines.push(`  ${fnameSafe}: ${base} = ${getFieldDefault(f)}`)
  }
  for (const g of oneofs) {
    const union = g.names.map(n => { const f = msg.fields[n]; return `{ kind: '${n}', ${n}: ${localType(f)} }` }).join(' | ')
    if (docsOn) {
      const oc = commentOf ? commentOf(`${fullName}#oneof:${g.name}`) : (oneofsWithComments.find(x => x.name === g.name)?.comment)
      if (oc) lines.push(...formatDoc(oc).map(s => '  ' + s))
    }
    lines.push(`  ${g.name}: (${union}) | undefined = undefined`)
  }
  lines.push('')
  // Simple constructor that calls super()
  lines.push(`  constructor() {`)
  lines.push(`    super()`)
  lines.push('  }')
  lines.push('')
  lines.push(`  clone(): ${className} {`)
  lines.push(`    const clone = new ${className}()`)
  lines.push(`    clone.mergeFrom(this)`)
  lines.push(`    return clone`)
  lines.push(`  }`)
  lines.push('')
  // static create() method
  // Use the generated CreateInput interface instead of inline object literal
  const createParamType = (fields.length > 0 || oneofs.length > 0) ? `${className}CreateInput` : 'Record<string, never>'

  lines.push(`  static create(init?: ${createParamType}): ${className} {`)
  lines.push(`    const msg = new ${className}()`)
  lines.push(`    if (init) {`)
  for (const f of fields) {
    const fnameSafe = safeIdent(f.name)
    if (f.map || f.repeated) {
      lines.push(`      if (init.${fnameSafe} !== undefined) msg.${fnameSafe} = init.${fnameSafe}`)
    } else if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
      const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
      lines.push(`      if (init.${fnameSafe} !== undefined) {`)
      lines.push(`        msg.${fnameSafe} = init.${fnameSafe} instanceof ${alias}`)
      lines.push(`          ? init.${fnameSafe}`)
      lines.push(`          : ${alias}.create(init.${fnameSafe})`)
      lines.push(`      }`)
    } else {
      lines.push(`      if (init.${fnameSafe} !== undefined) msg.${fnameSafe} = init.${fnameSafe}`)
    }
  }
  for (const g of oneofs) {
    lines.push(`      if (init.${g.name} !== undefined) msg.${g.name} = init.${g.name}`)
  }
  lines.push(`    }`)
  lines.push(`    return msg`)
  lines.push(`  }`)
  lines.push('')
  // Helper to get visitor method name
  const getVisitorMethod = (f) => {
    const prefix = f.map ? 'visitMap' : (f.repeated ? 'visitRepeated' : 'visit')
    if (f.map) {
      const kt = mapKeyType(f, int64Mode)
      const keyPart = kt === 'string' ? 'String' : (kt === 'number' ? 'Int32' : 'Int64')
      if (f.resolvedType) {
        if (f.resolvedType instanceof protobuf.Enum) {
          return `${prefix}${keyPart}Int32`
        } else {
          return `${prefix}${keyPart}Message`
        }
      }
      const vt = typeFor(f, int64Mode)
      const valPart = vt === 'string' ? 'String' : (vt === 'number' ? 'Int32' : (vt === 'bigint' ? 'Int64' : (vt === 'boolean' ? 'Bool' : (vt === 'Uint8Array' ? 'Bytes' : 'Int32'))))
      return `${prefix}${keyPart}${valPart}`
    }
    if (f.resolvedType) {
      if (f.resolvedType instanceof protobuf.Enum) {
        return `${prefix}Int32`
      } else {
        return `${prefix}Message`
      }
    }
    const t = typeFor(f, int64Mode)
    switch (t) {
      case 'string': return `${prefix}String`
      case 'number': {
        if (['int32','uint32','sint32','fixed32','sfixed32'].includes(f.type)) return `${prefix}Int32`
        if (['float'].includes(f.type)) return `${prefix}Float`
        if (['double'].includes(f.type)) return `${prefix}Double`
        return `${prefix}Int32`
      }
      case 'bigint': return `${prefix}Int64`
      case 'boolean': return `${prefix}Bool`
      case 'Uint8Array': return `${prefix}Bytes`
      default: return `${prefix}Int32`
    }
  }
  // traverse() method - the core Visitor pattern implementation
  lines.push(`  traverse(v: Visitor): void {`)
  for (const f of fields) {
    const fnameSafe = safeIdent(f.name)
    const fieldNum = f.id
    const visitorMethod = getVisitorMethod(f)
    if (f.map) {
      lines.push(`    if (this.${fnameSafe}.size > 0) {`)
      lines.push(`      v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
      lines.push(`    }`)
    } else if (f.repeated) {
      lines.push(`    if (this.${fnameSafe}.length > 0) {`)
      lines.push(`      v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
      lines.push(`    }`)
    } else {
      const defVal = getFieldDefault(f)
      let condition
      if (f.resolvedType) {
        condition = `this.${fnameSafe} !== null`
      } else {
        const t = typeFor(f, int64Mode)
        switch (t) {
          case 'string': condition = `this.${fnameSafe} !== ''`; break
          case 'number': condition = `this.${fnameSafe} !== 0`; break
          case 'bigint': condition = `this.${fnameSafe} !== 0n`; break
          case 'boolean': condition = `this.${fnameSafe} !== false`; break
          case 'Uint8Array': condition = `this.${fnameSafe}.length > 0`; break
          default: condition = `this.${fnameSafe} !== null`
        }
      }
      lines.push(`    if (${condition}) {`)
      lines.push(`      v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
      lines.push(`    }`)
    }
  }
  // Handle oneofs
  for (const g of oneofs) {
    lines.push(`    if (this.${g.name} !== undefined) {`)
    lines.push(`      switch (this.${g.name}.kind) {`)
    for (const n of g.names) {
      const f = msg.fields[n]
      const visitorMethod = getVisitorMethod(f)
      lines.push(`        case '${n}':`)
      lines.push(`          v.${visitorMethod}(this.${g.name}.${n}, ${f.id})`)
      lines.push(`          break`)
    }
    lines.push(`      }`)
    lines.push(`    }`)
  }
  lines.push(`  }`)
  lines.push('')
  // decodeFrom() method - reads from Reader and updates this message
  lines.push(`  decodeFrom(r: Reader, len?: number): void {`)
  lines.push(`    const endPos = len === undefined ? r.len : r.pos + len`)
  lines.push('')
  lines.push(`    while (r.pos < endPos) {`)
  lines.push(`      const tag = r.uint32()`)
  lines.push(`      const fieldNum = tag >>> 3`)
  lines.push('')
  lines.push(`      switch (fieldNum) {`)
  for (const f of fields) {
    const no = f.id
    const fnameSafe = safeIdent(f.name)
    lines.push(`        case ${no}: {`)
    if (f.map) {
      const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
      const kt = mapKeyType(f, int64Mode)
      const vt = localType(f)
      lines.push(`          {`)
      lines.push(`            const l = r.uint32()`)
      lines.push(`            const end2 = r.pos + l`)
      lines.push(`            let k: ${kt}`)
      lines.push(`            let v: ${vt}`)
      lines.push(`            while (r.pos < end2) {`)
      lines.push(`              const tt = r.uint32()`)
      lines.push(`              switch (tt >>> 3) {`)
      lines.push(`                case 1: {`)
      switch (f.keyType) {
        case 'string': lines.push(`                  k = r.string()`); break
        case 'bool': lines.push(`                  k = r.bool()`); break
        case 'int32':
        case 'uint32': lines.push(`                  k = r.int32()`); break
        case 'sint32': lines.push(`                  k = r.sint32()`); break
        case 'fixed32': lines.push(`                  k = r.fixed32()`); break
        case 'sfixed32': lines.push(`                  k = r.sfixed32()`); break
        case 'int64':
        case 'uint64': lines.push(int64Mode === 'number' ? `                  k = r.int64Number()` : `                  k = r.int64()`); break
        case 'sint64': lines.push(int64Mode === 'number' ? `                  k = Number(r.sint64())` : `                  k = r.sint64()`); break
        case 'fixed64': lines.push(int64Mode === 'number' ? `                  k = Number(r.fixed64())` : `                  k = r.fixed64()`); break
        case 'sfixed64': lines.push(int64Mode === 'number' ? `                  k = Number(r.sfixed64())` : `                  k = r.sfixed64()`); break
        default: lines.push(`                  k = r.int32()`); break
      }
      lines.push(`                  break`)
      lines.push(`                }`)
      lines.push(`                case 2: {`)
      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        lines.push(`                  const msgLen = r.uint32()`)
        lines.push(`                  v = new ${alias}()`)
        lines.push(`                  v.decodeFrom(r, msgLen)`)
      } else {
        switch (f.type) {
          case 'string': lines.push(`                  v = r.string()`); break
          case 'bytes': lines.push(`                  v = r.bytes()`); break
          case 'bool': lines.push(`                  v = r.bool()`); break
          case 'int32':
          case 'uint32': lines.push(`                  v = r.int32()`); break
          case 'sint32': lines.push(`                  v = r.sint32()`); break
          case 'fixed32': lines.push(`                  v = r.fixed32()`); break
          case 'sfixed32': lines.push(`                  v = r.sfixed32()`); break
          case 'int64': lines.push(int64Mode === 'number' ? `                  v = r.int64Number()` : `                  v = r.int64()`); break
          case 'uint64': lines.push(int64Mode === 'number' ? `                  v = r.uint64Number()` : `                  v = r.uint64()`); break
          case 'sint64': lines.push(int64Mode === 'number' ? `                  v = Number(r.sint64())` : `                  v = r.sint64()`); break
          case 'fixed64': lines.push(int64Mode === 'number' ? `                  v = Number(r.fixed64())` : `                  v = r.fixed64()`); break
          case 'sfixed64': lines.push(int64Mode === 'number' ? `                  v = Number(r.sfixed64())` : `                  v = r.sfixed64()`); break
          case 'float': lines.push(`                  v = r.float()`); break
          case 'double': lines.push(`                  v = r.double()`); break
          default: lines.push(`                  v = r.int32()`); break
        }
      }
      lines.push(`                  break`)
      lines.push(`                }`)
      lines.push(`                default: r.skipType(tt & 7)`)
      lines.push(`              }`)
      lines.push(`            }`)
      lines.push(`            this.${fnameSafe}.set(k as ${kt}, v as ${vt})`)
      lines.push(`          }`)
    } else if (f.repeated) {
      if (isPackable(f)) {
        const rm = readerMethodName(f, int64Mode)
        lines.push(`          if ((tag & 7) === 2) {`)
        lines.push(`            const l = r.uint32()`)
        lines.push(`            const end2 = r.pos + l`)
        lines.push(`            while (r.pos < end2) {`)
        if (['sint64','fixed64','sfixed64'].includes(f.type)) {
          lines.push(int64Mode === 'number'
            ? `              this.${fnameSafe}.push(Number(r.${f.type}()))`
            : `              this.${fnameSafe}.push(r.${f.type}())`)
        } else {
          lines.push(`              this.${fnameSafe}.push(r.${rm}())`)
        }
        lines.push(`            }`)
        lines.push(`          } else {`)
      }
      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
        lines.push(isPackable(f) ? `            const msgLen = r.uint32()` : `          const msgLen = r.uint32()`)
        lines.push(isPackable(f) ? `            const msg = new ${alias}()` : `          const msg = new ${alias}()`)
        lines.push(isPackable(f) ? `            msg.decodeFrom(r, msgLen)` : `          msg.decodeFrom(r, msgLen)`)
        lines.push(isPackable(f) ? `            this.${fnameSafe}.push(msg)` : `          this.${fnameSafe}.push(msg)`)
      } else {
        const rm = readerMethodName(f, int64Mode)
        if (['sint64','fixed64','sfixed64'].includes(f.type)) {
          lines.push(int64Mode === 'number'
            ? (isPackable(f) ? `            this.${fnameSafe}.push(Number(r.${f.type}()))` : `          this.${fnameSafe}.push(Number(r.${f.type}()))`)
            : (isPackable(f) ? `            this.${fnameSafe}.push(r.${f.type}())` : `          this.${fnameSafe}.push(r.${f.type}())`))
        } else {
          lines.push(isPackable(f) ? `            this.${fnameSafe}.push(r.${rm}())` : `          this.${fnameSafe}.push(r.${rm}())`)
        }
      }
      if (isPackable(f)) lines.push(`          }`)
    } else {
      if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
        const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
        lines.push(`          {`)
        lines.push(`            const msgLen = r.uint32()`)
        lines.push(`            this.${fnameSafe} = new ${alias}()`)
        lines.push(`            this.${fnameSafe}.decodeFrom(r, msgLen)`)
        lines.push(`          }`)
      } else {
        const rm = readerMethodName(f, int64Mode)
        if (['sint64','fixed64','sfixed64'].includes(f.type)) {
          lines.push(int64Mode === 'number'
            ? `          this.${fnameSafe} = Number(r.${f.type}())`
            : `          this.${fnameSafe} = r.${f.type}()`)
        } else {
          lines.push(`          this.${fnameSafe} = r.${rm}()`)
        }
      }
    }
    if (f.partOf) {
      lines.push(`          this.${f.partOf.name} = { kind: '${f.name}', ${f.name}: this.${fnameSafe} }`)
    }
    lines.push(`          break`)
    lines.push(`        }`)
  }
  lines.push(`        default: {`)
  lines.push(`          const wireType = tag & 7`)
  lines.push(`          const start = r.pos`)
  lines.push(`          r.skipType(wireType)`)
  lines.push(`          this.unknownFields.append(r.view(start, r.pos))`)
  lines.push(`        }`)
  lines.push(`      }`)
  lines.push(`    }`)
  lines.push(`  }`)
  lines.push('')
  lines.push(`  static encode(m: ${className}, w: Writer): void {`)
  lines.push(`    const v = new BinaryEncodingVisitor(w)`)
  lines.push(`    m.traverse(v)`)
  lines.push(`  }`)
  lines.push('')
  lines.push(`  static decode(r: Reader): ${className} {`)
  lines.push(`    const m = new ${className}()`)
  lines.push(`    m.decodeFrom(r)`)
  lines.push(`    return m`)
  lines.push(`  }`)
  lines.push('')
  // static fromBinary() - typed wrapper
  lines.push(`  static fromBinary(data: Uint8Array): ${className} {`)
  lines.push(`    try {`)
  lines.push(`      return new ${className}().fromBinary(data) as ${className}`)
  lines.push(`    } catch (e) {`)
  lines.push(`      throw new Error(\`Failed to parse ${className} from binary: \${e}\`)`)
  lines.push(`    }`)
  lines.push(`  }`)
  lines.push('')
  // validate() - instance method for field validation
  lines.push(`  validate(): string | null {`)
  // Only validate nested messages (not scalar fields)
  for (const f of fields) {
    if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
      const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
      const fnameSafe = safeIdent(f.name)
      if (f.repeated) {
        lines.push(`    for (const item of this.${fnameSafe}) {`)
        lines.push(`      const err = item.validate()`)
        lines.push(`      if (err !== null) return '${f.name}: ' + err`)
        lines.push(`    }`)
      } else if (f.map) {
        lines.push(`    for (const [k, v] of this.${fnameSafe}.entries()) {`)
        lines.push(`      const err = v.validate()`)
        lines.push(`      if (err !== null) return '${f.name}[' + String(k) + ']: ' + err`)
        lines.push(`    }`)
      } else {
        lines.push(`    if (this.${fnameSafe} !== null) {`)
        lines.push(`      const err = this.${fnameSafe}.validate()`)
        lines.push(`      if (err !== null) return '${f.name}: ' + err`)
        lines.push(`    }`)
      }
    }
  }
  lines.push(`    return null`)
  lines.push(`  }`)
  lines.push('')
  // clear() - reset all fields to default values
  lines.push(`  clear(): ${className} {`)
  for (const f of fields) {
    const fnameSafe = safeIdent(f.name)
    lines.push(`    this.${fnameSafe} = ${getFieldDefault(f)}`)
  }
  for (const g of oneofs) {
    lines.push(`    this.${g.name} = undefined`)
  }
  lines.push(`    this.unknownFields.clear()`)
  lines.push(`    return this`)
  lines.push(`  }`)
  lines.push('')
  // isEmpty() - check if all fields are at default values
  lines.push(`  isEmpty(): boolean {`)
  const checks = []
  for (const f of fields) {
    const fnameSafe = safeIdent(f.name)
    if (f.map) {
      checks.push(`this.${fnameSafe}.size === 0`)
    } else if (f.repeated) {
      checks.push(`this.${fnameSafe}.length === 0`)
    } else if (f.resolvedType) {
      checks.push(`this.${fnameSafe} === null`)
    } else {
      const t = typeFor(f, int64Mode)
      switch (t) {
        case 'string': checks.push(`this.${fnameSafe} === ''`); break
        case 'number': checks.push(`this.${fnameSafe} === 0`); break
        case 'bigint': checks.push(`this.${fnameSafe} === 0n`); break
        case 'boolean': checks.push(`this.${fnameSafe} === false`); break
        case 'Uint8Array': checks.push(`this.${fnameSafe}.length === 0`); break
        default: checks.push(`this.${fnameSafe} === null`)
      }
    }
  }
  for (const g of oneofs) {
    checks.push(`this.${g.name} === undefined`)
  }
  checks.push(`this.unknownFields.isEmpty()`)
  if (checks.length <= 3) {
    lines.push(`    return ${checks.join(' && ')}`)
  } else {
    lines.push(`    return ${checks[0]}`)
    for (let i = 1; i < checks.length; i++) {
      lines.push(`           && ${checks[i]}`)
    }
  }
  lines.push(`  }`)
  if (jsonOn) {
    const fullNoDot = (msg.fullName||'').replace(/^\./,'')
    const isTimestamp = (fullNoDot === 'google.protobuf.Timestamp')
    const isDuration = (fullNoDot === 'google.protobuf.Duration')
    const isAny = (fullNoDot === 'google.protobuf.Any')
    const isStruct = (fullNoDot === 'google.protobuf.Struct')
    const isValue = (fullNoDot === 'google.protobuf.Value')
    const isListValue = (fullNoDot === 'google.protobuf.ListValue')
    const isFieldMask = (fullNoDot === 'google.protobuf.FieldMask')
    const isStringValue = (fullNoDot === 'google.protobuf.StringValue')
    const isBoolValue = (fullNoDot === 'google.protobuf.BoolValue')
    const isBytesValue = (fullNoDot === 'google.protobuf.BytesValue')
    const isDoubleValue = (fullNoDot === 'google.protobuf.DoubleValue')
    const isFloatValue = (fullNoDot === 'google.protobuf.FloatValue')
    const isInt32Value = (fullNoDot === 'google.protobuf.Int32Value')
    const isUInt32Value = (fullNoDot === 'google.protobuf.UInt32Value')
    const isInt64Value = (fullNoDot === 'google.protobuf.Int64Value')
    const isUInt64Value = (fullNoDot === 'google.protobuf.UInt64Value')
    const structAlias = (aliasMap.get('google.protobuf.Struct') && aliasMap.get('google.protobuf.Struct').local) || 'Struct'
    const listAlias = (aliasMap.get('google.protobuf.ListValue') && aliasMap.get('google.protobuf.ListValue').local) || 'ListValue'
    const valueAlias = (aliasMap.get('google.protobuf.Value') && aliasMap.get('google.protobuf.Value').local) || 'Value'
    if (isTimestamp) {
      lines.push('  static toJson(m: ' + className + '): string {')
      lines.push('    return timestampToJson(m.seconds as bigint, (m.nanos as number) ?? 0)')
      lines.push('  }')
      lines.push('  static fromJson(j: any): ' + className + ' {')
      lines.push('    const t = timestampFromJson(String(j))')
      lines.push('    return new ' + className + '({ seconds: t.seconds, nanos: t.nanos })')
      lines.push('  }')
    } else if (isDuration) {
      lines.push('  static toJson(m: ' + className + '): string {')
      lines.push('    return durationToJson(m.seconds as bigint, (m.nanos as number) ?? 0)')
      lines.push('  }')
      lines.push('  static fromJson(j: any): ' + className + ' {')
      lines.push('    const t = durationFromJson(String(j))')
      lines.push('    return new ' + className + '({ seconds: t.seconds, nanos: t.nanos })')
      lines.push('  }')
    } else if (isAny) {
      lines.push('  static toJson(m: ' + className + '): {typeUrl: string, value: string} {')
      lines.push('    const av = m as { type_url: string, value: Uint8Array }')
      lines.push('    return { typeUrl: av.type_url, value: encodeBase64(av.value) }')
      lines.push('  }')
      lines.push('  static fromJson(j: {typeUrl: string, value: string}): ' + className + ' {')
      lines.push('    return new ' + className + '({ type_url: j.typeUrl, value: decodeBase64(j.value) })')
      lines.push('  }')
    } else if (isStruct) {
      lines.push('  static toJson(m: ' + className + '): Record<string, Object> {')
      lines.push('    const o: Record<string, Object> = {}')
      lines.push('    const mp = (m as { fields?: Map<string, ' + valueAlias + '> }).fields')
      lines.push('    if (mp) { for (const [k,v] of mp.entries()) { o[k] = ' + valueAlias + '.toJson(v) } }')
      lines.push('    return o')
      lines.push('  }')
      lines.push('  static fromJson(j: Record<string, Object>): ' + className + ' {')
      lines.push('    const fields: Map<string, ' + valueAlias + '> = new Map()')
      lines.push('    if (j) for (const k of Object.keys(j)) fields.set(k, ' + valueAlias + '.fromJson(j[k]))')
      lines.push('    return new ' + className + '({ fields: fields })')
      lines.push('  }')
    } else if (isValue) {
      lines.push('  static toJson(m: ' + className + '): any {')
      lines.push('    const vv = m as Object')
      lines.push('    if (vv.null_value !== undefined) return null')
      lines.push('    if (vv.number_value !== undefined) return vv.number_value')
      lines.push('    if (vv.string_value !== undefined) return vv.string_value')
      lines.push('    if (vv.bool_value !== undefined) return vv.bool_value')
      lines.push('    if (vv.struct_value !== undefined) return ' + structAlias + '.toJson(vv.struct_value)')
      lines.push('    if (vv.list_value !== undefined) return ' + listAlias + '.toJson(vv.list_value)')
      lines.push('    return null')
      lines.push('  }')
      lines.push('  static fromJson(j: any): ' + className + ' {')
      lines.push('    if (j === null) return new ' + className + '({ null_value: 0 })')
      lines.push('    if (typeof j === "number") return new ' + className + '({ number_value: j })')
      lines.push('    if (typeof j === "string") return new ' + className + '({ string_value: j })')
      lines.push('    if (typeof j === "boolean") return new ' + className + '({ bool_value: j })')
      lines.push('    if (Array.isArray(j)) return new ' + className + '({ list_value: ' + listAlias + '.fromJson(j) })')
      lines.push('    return new ' + className + '({ struct_value: ' + structAlias + '.fromJson(j) })')
      lines.push('  }')
    } else if (isListValue) {
      lines.push('  static toJson(m: ' + className + '): Array<Object> {')
      lines.push('    const arr: Array<Object> = []')
      lines.push('    const lv = (m as { values?: Array<' + valueAlias + '> }).values')
      lines.push('    if (lv) for (const v of lv) arr.push(' + valueAlias + '.toJson(v))')
      lines.push('    return arr')
      lines.push('  }')
      lines.push('  static fromJson(j: Array<Object>): ' + className + ' {')
      lines.push('    const values: Array<' + valueAlias + '> = []')
      lines.push('    if (Array.isArray(j)) for (const v of j) values.push(' + valueAlias + '.fromJson(v))')
      lines.push('    return new ' + className + '({ values: values })')
      lines.push('  }')
    } else if (isFieldMask) {
      lines.push('  static toJson(m: ' + className + '): string {')
      lines.push('    const fm = (m as { paths?: Array<string> }).paths')
      lines.push('    const paths = fm ?? []')
      lines.push('    return paths.join(",")')
      lines.push('  }')
      lines.push('  static fromJson(j: string): ' + className + ' {')
      lines.push('    const s = String(j)')
      lines.push('    const arr = s.length ? s.split(",").map(x=> x.trim()).filter(x=> x.length>0) : []')
      lines.push('    return new ' + className + '({ paths: arr })')
      lines.push('  }')
    } else if (isStringValue) {
      lines.push('  static toJson(m: ' + className + '): string {')
      lines.push('    return (m as { value?: string }).value ?? ""')
      lines.push('  }')
      lines.push('  static fromJson(j: string): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: String(j) })')
      lines.push('  }')
    } else if (isBoolValue) {
      lines.push('  static toJson(m: ' + className + '): boolean {')
      lines.push('    return (m as { value?: boolean }).value ?? false')
      lines.push('  }')
      lines.push('  static fromJson(j: boolean): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: Boolean(j) })')
      lines.push('  }')
    } else if (isBytesValue) {
      lines.push('  static toJson(m: ' + className + '): string {')
      lines.push('    return encodeBase64((m as { value?: Uint8Array }).value ?? new Uint8Array(0))')
      lines.push('  }')
      lines.push('  static fromJson(j: string): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: decodeBase64(String(j)) })')
      lines.push('  }')
    } else if (isDoubleValue || isFloatValue) {
      lines.push('  static toJson(m: ' + className + '): number {')
      lines.push('    return Number((m as { value?: number }).value ?? 0)')
      lines.push('  }')
      lines.push('  static fromJson(j: number): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: Number(j) })')
      lines.push('  }')
    } else if (isInt32Value || isUInt32Value) {
      lines.push('  static toJson(m: ' + className + '): number {')
      lines.push('    return Number((m as { value?: number }).value ?? 0)')
      lines.push('  }')
      lines.push('  static fromJson(j: number): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: Number(j) })')
      lines.push('  }')
    } else if (isInt64Value || isUInt64Value) {
      lines.push('  static toJson(m: ' + className + '): ' + (int64Mode === 'number' ? 'number' : 'string') + ' {')
      lines.push('    return ' + (int64Mode === 'number' ? 'Number((m as { value?: number }).value ?? 0)' : '(m as { value?: bigint }).value?.toString() ?? "0"') )
      lines.push('  }')
      lines.push('  static fromJson(j: ' + (int64Mode === 'number' ? 'number' : 'string') + '): ' + className + ' {')
      lines.push('    return new ' + className + '({ value: ' + (int64Mode === 'number' ? 'Number(j)' : 'BigInt(String(j))') + ' })')
      lines.push('  }')
    } else {
      // Generate ArkTS-compatible toJson and fromJson methods
      const toJsonLines = generateToJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg)
      lines.push(...toJsonLines)
      lines.push('')
      const fromJsonLines = generateFromJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg)
      lines.push(...fromJsonLines)
    }
  }
  lines.push('}\n')
  return lines.join('\n')
}

/**
 * Build an expression to detect default scalar/enum values for omit-defaults
 */
function defaultCheckExpr(f, int64Mode, expr) {
  if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) return null
  switch (f.type) {
    case 'string': return `${expr} === ''`
    case 'bytes': return `${expr}.length === 0`
    case 'bool': return `${expr} === false`
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32':
    case 'float':
    case 'double': return `${expr} === 0`
    case 'int64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64': return int64Mode === 'number' ? `${expr} === 0` : `${expr} === 0n`
    case 'uint64': return int64Mode === 'number' ? `${expr} === 0` : `${expr} === 0n`
    default:
      if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) return `${expr} === 0`
      return null
  }
}

/**
 * Generate Writer calls to serialize a single field value
 */
function renderWriteField(f, int64Mode, expr, wvar) {
  const out = []
  switch (f.type) {
    case 'string':
      out.push(`${wvar}.string(${expr})`)
      break
    case 'bytes':
      out.push(`${wvar}.bytes(${expr})`)
      break
    case 'bool':
      out.push(`${wvar}.bool(${expr})`)
      break
    case 'int32':
    case 'uint32':
      out.push(`${wvar}.int32(${expr} as number)`)
      break
    case 'sint32':
      out.push(`${wvar}.sint32(${expr} as number)`)
      break
    case 'fixed32':
      out.push(`${wvar}.fixed32(${expr} as number)`)
      break
    case 'sfixed32':
      out.push(`${wvar}.sfixed32(${expr} as number)`)
      break
    case 'int64':
    case 'uint64':
      if (int64Mode === 'number') out.push(`${wvar}.int64Number(${expr} as number)`)
      else out.push(`${wvar}.int64(${expr} as bigint)`)
      break
    case 'sint64':
      out.push(`${wvar}.sint64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
      break
    case 'fixed64':
      out.push(`${wvar}.fixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
      break
    case 'sfixed64':
      out.push(`${wvar}.sfixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
      break
    case 'float':
      out.push(`${wvar}.float(${expr} as number)`)
      break
    case 'double':
      out.push(`${wvar}.double(${expr} as number)`)
      break
    default:
      if (f.resolvedType) {
        if (f.resolvedType instanceof protobuf.Enum) out.push(`${wvar}.int32(${expr} as number)`)
        else out.push(`{ const cw = ${wvar}.fork(); ${f.resolvedType.name}.encode(${expr}, cw); ${wvar}.ldelim(cw) }`)
      } else out.push(`${wvar}.int32(${expr} as number)`)
  }
  return out
}

/**
 * Generate Reader calls to deserialize into target variable
 */
function renderReadField(f, int64Mode, targetExpr, aliasName) {
  const out = []
  const target = targetExpr
  if (f.repeated) out.push(`${target} = ${target} ?? []`)
  switch (f.type) {
    case 'string':
      out.push(f.repeated ? `${target}.push(r.string())` : `${target} = r.string()`)
      break
    case 'bytes':
      out.push(f.repeated ? `${target}.push(r.bytes())` : `${target} = r.bytes()`)
      break
    case 'bool':
      out.push(f.repeated ? `${target}.push(r.bool())` : `${target} = r.bool()`)
      break
    case 'int32':
    case 'uint32':
      out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
      break
    case 'sint32':
      out.push(f.repeated ? `${target}.push(r.sint32())` : `${target} = r.sint32()`)
      break
    case 'fixed32':
      out.push(f.repeated ? `${target}.push(r.fixed32())` : `${target} = r.fixed32()`)
      break
    case 'sfixed32':
      out.push(f.repeated ? `${target}.push(r.sfixed32())` : `${target} = r.sfixed32()`)
      break
    case 'int64':
      if (int64Mode === 'number') out.push(f.repeated ? `${target}.push(r.int64Number())` : `${target} = r.int64Number()`)
      else out.push(f.repeated ? `${target}.push(r.int64())` : `${target} = r.int64()`)
      break
    case 'uint64':
      if (int64Mode === 'number') out.push(f.repeated ? `${target}.push(r.uint64Number())` : `${target} = r.uint64Number()`)
      else out.push(f.repeated ? `${target}.push(r.uint64())` : `${target} = r.uint64()`)
      break
    case 'sint64':
      out.push(int64Mode === 'number'
        ? (f.repeated ? `${target}.push(Number(r.sint64()))` : `${target} = Number(r.sint64())`)
        : (f.repeated ? `${target}.push(r.sint64())` : `${target} = r.sint64()`))
      break
    case 'fixed64':
      out.push(int64Mode === 'number'
        ? (f.repeated ? `${target}.push(Number(r.fixed64()))` : `${target} = Number(r.fixed64())`)
        : (f.repeated ? `${target}.push(r.fixed64())` : `${target} = r.fixed64()`))
      break
    case 'sfixed64':
      out.push(int64Mode === 'number'
        ? (f.repeated ? `${target}.push(Number(r.sfixed64()))` : `${target} = Number(r.sfixed64())`)
        : (f.repeated ? `${target}.push(r.sfixed64())` : `${target} = r.sfixed64()`))
      break
    case 'float':
      out.push(f.repeated ? `${target}.push(r.float())` : `${target} = r.float()`)
      break
    case 'double':
      out.push(f.repeated ? `${target}.push(r.double())` : `${target} = r.double()`)
      break
    default:
      if (f.resolvedType) {
        if (f.resolvedType instanceof protobuf.Enum) out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
        else {
          const nm = aliasName || f.resolvedType.name
          out.push(f.repeated ? `${target}.push(${nm}.decode(r, r.uint32()))` : `${target} = ${nm}.decode(r, r.uint32())`)
        }
      } else out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
  }
  return out
}
/**
 * ArkTS type for map key (depends on int64 mode)
 */
function mapKeyType(f, int64Mode) {
  const kt = f.keyType
  switch (kt) {
    case 'string': return 'string'
    case 'bool': return 'boolean'
    case 'int32':
    case 'uint32':
    case 'sint32':
    case 'fixed32':
    case 'sfixed32': return 'number'
    case 'int64':
    case 'uint64':
    case 'sint64':
    case 'fixed64':
    case 'sfixed64': return int64Mode === 'number' ? 'number' : 'bigint'
    default: return 'string'
  }
}
/**
 * Generate map-entry key write sequence
 */
function renderWriteMapKey(f, int64Mode, expr) {
  const out = []
  const wt = (function(){
    switch (f.keyType) {
      case 'string':
        return 2
      case 'float':
      case 'fixed32':
      case 'sfixed32':
        return 5
      case 'double':
      case 'fixed64':
      case 'sfixed64':
        return 1
      default:
        return 0
    }
  })()
  out.push(`cw.uint32((1 << 3) | ${wt})`)
  switch (f.keyType) {
    case 'string': out.push(`cw.string(${expr})`); break
    case 'bool': out.push(`cw.bool(${expr})`); break
    case 'int32':
    case 'uint32': out.push(`cw.int32(${expr} as number)`); break
    case 'sint32': out.push(`cw.sint32(${expr} as number)`); break
    case 'fixed32': out.push(`cw.fixed32(${expr} as number)`); break
    case 'sfixed32': out.push(`cw.sfixed32(${expr} as number)`); break
    case 'int64':
    case 'uint64': out.push(int64Mode === 'number' ? `cw.int64Number(${expr} as number)` : `cw.int64(${expr} as bigint)`); break
    case 'sint64': out.push(`cw.sint64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
    case 'fixed64': out.push(`cw.fixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
    case 'sfixed64': out.push(`cw.sfixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
    default: out.push(`cw.int32(${expr} as number)`)
  }
  return out
}
/**
 * Generate map-entry value write sequence
 */
function renderWriteMapValue(f, int64Mode, expr) {
  const out = []
  const vt = f.type
  if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) {
    out.push('cw.uint32((2 << 3) | 0)')
    out.push(`cw.int32(${expr} as number)`)
  } else {
    out.push('cw.uint32((2 << 3) | ' + (f.resolvedType ? 2 : wireTypeFor(f)) + ')')
    out.push(...renderWriteField(f, int64Mode, expr, 'cw'))
  }
  return out
}
/**
 * Generate map-entry read loop for a map field
 */
function renderReadMapField(f, int64Mode, varName, aliasName) {
  const out = []
  out.push(`${varName} = ${varName} ?? new Map()`) 
  out.push('const l = r.uint32()')
  out.push('const end2 = r.pos + l')
  const vType = (function(){
    if (f.resolvedType) {
      if (f.resolvedType instanceof protobuf.Enum) return 'number'
      return aliasName || f.resolvedType.name
    }
    return typeFor(f, int64Mode)
  })()
  out.push(`let k: ${mapKeyType(f, int64Mode)}; let v: ${vType}`)
  out.push('while (r.pos < end2) {')
  out.push('  const tt = r.uint32()')
  out.push('  switch (tt >>> 3) {')
  out.push('    case 1: {')
  switch (f.keyType) {
    case 'string': out.push('      k = r.string(); break'); break
    case 'bool': out.push('      k = r.bool(); break'); break
    case 'int32':
    case 'uint32': out.push('      k = r.int32(); break'); break
    case 'sint32': out.push('      k = r.sint32(); break'); break
    case 'fixed32': out.push('      k = r.fixed32(); break'); break
    case 'sfixed32': out.push('      k = r.sfixed32(); break'); break
    case 'int64':
    case 'uint64': out.push(int64Mode === 'number' ? '      k = r.int64Number(); break' : '      k = r.int64(); break'); break
    case 'sint64': out.push(int64Mode === 'number' ? '      k = Number(r.sint64()); break' : '      k = r.sint64(); break'); break
    case 'fixed64': out.push(int64Mode === 'number' ? '      k = Number(r.fixed64()); break' : '      k = r.fixed64(); break'); break
    case 'sfixed64': out.push(int64Mode === 'number' ? '      k = Number(r.sfixed64()); break' : '      k = r.sfixed64(); break'); break
    default: out.push('      k = r.int32(); break'); break
  }
  out.push('      break')
  out.push('    }')
  out.push('    case 2: {')
  switch (f.type) {
    case 'string': out.push('      v = r.string(); break'); break
    case 'bytes': out.push('      v = r.bytes(); break'); break
    case 'bool': out.push('      v = r.bool(); break'); break
    case 'int32':
    case 'uint32': out.push('      v = r.int32(); break'); break
    case 'sint32': out.push('      v = r.sint32(); break'); break
    case 'fixed32': out.push('      v = r.fixed32(); break'); break
    case 'sfixed32': out.push('      v = r.sfixed32(); break'); break
    case 'int64': out.push(int64Mode === 'number' ? '      v = r.int64Number(); break' : '      v = r.int64(); break'); break
    case 'uint64': out.push(int64Mode === 'number' ? '      v = r.uint64Number(); break' : '      v = r.uint64(); break'); break
    case 'sint64': out.push(int64Mode === 'number' ? '      v = Number(r.sint64()); break' : '      v = r.sint64(); break'); break
    case 'fixed64': out.push(int64Mode === 'number' ? '      v = Number(r.fixed64()); break' : '      v = r.fixed64(); break'); break
    case 'sfixed64': out.push(int64Mode === 'number' ? '      v = Number(r.sfixed64()); break' : '      v = r.sfixed64(); break'); break
    case 'float': out.push('      v = r.float(); break'); break
    case 'double': out.push('      v = r.double(); break'); break
    default: if (f.resolvedType) { out.push(`      v = ${(aliasName || f.resolvedType.name)}.decode(r, r.uint32()); break`) } else { out.push('      v = r.int32(); break') } break
  }
  out.push('      break')
  out.push('    }')
  out.push('    default: r.skipType(tt & 7)')
  out.push('  }')
  out.push('}')
  out.push(`${varName}.set(k as ${mapKeyType(f, int64Mode)}, v as ${vType})`)
  return out
}

/**
 * Determine top-level package name from a reflection object
 */
function pkgFromObj(obj) {
  const fn = (obj.fullName || '').replace(/^\./, '')
  const seg = fn.split('.')[0]
  return seg || 'default'
}
/**
 * DFS walk protobuf reflection tree and invoke callback for types/enums/services
 */
function walk(node, cb) {
  if (!node) return
  if (node instanceof protobuf.Type || node instanceof protobuf.Enum || node instanceof protobuf.Service) cb(node)
  if (node.nestedArray) for (const n of node.nestedArray) walk(n, cb)
}
/**
 * Main entry: preprocess protos, load, emit code, build indexes
 */
async function main() {
  const opts = parseArgs()
  ensureDir(opts.out)
  function collectProtoFiles(dir) {
    const out = []
    for (const entry of fs.readdirSync(dir)) {
      const p = path.join(dir, entry)
      const st = fs.statSync(p)
      if (st.isDirectory()) out.push(...collectProtoFiles(p))
      else if (entry.endsWith('.proto')) out.push(p)
    }
    return out
  }
  const files = collectProtoFiles(opts.in)
  const tmpDir = path.join(process.cwd(), '.proto-preprocessed')
  if (fs.existsSync(tmpDir)) {
    fs.rmSync(tmpDir, { recursive: true, force: true })
  }
  fs.mkdirSync(tmpDir, { recursive: true })
  function preprocessProto(src) {
    return src.replace(/=\s*(-?)\s*(0[xX][0-9a-fA-F]+)/g, (_, sign, hex) => {
      const v = parseInt(hex, 16)
      return `= ${sign === '-' ? -v : v}`
    })
  }
  function mirrorWrite(srcPath) {
    const rel = path.relative(opts.in, srcPath)
    const outPath = path.join(tmpDir, rel)
    fs.mkdirSync(path.dirname(outPath), { recursive: true })
    const src = fs.readFileSync(srcPath, 'utf-8')
    fs.writeFileSync(outPath, preprocessProto(src))
    return outPath
  }
  const preFiles = files.map(mirrorWrite)
  if (opts.bundleRuntime === 'on') {
    const rtSrc = path.join(process.cwd(), 'runtime', 'arkpb')
    const rtDst = path.join(opts.out, opts.runtimeDir)
    ensureDir(rtDst)
    // Copy all runtime files
    const runtimeFiles = [
      'index.ets',
      'Message.ets',
      'Visitor.ets',
      'UnknownFields.ets',
      'BinaryEncodingVisitor.ets',
      'JsonEncodingVisitor.ets',
      'MessageRegistry.ets',
      'MessageUtils.ets',
      'Reader.ets',
      'Writer.ets',
      'RpcTransport.ets',
      'util.ets'
    ]
    for (const f of runtimeFiles) {
      const s = path.join(rtSrc, f)
      if (fs.existsSync(s)) fs.copyFileSync(s, path.join(rtDst, f))
    }
  }
  function extractComments(src) {
    const lines = src.split(/\r?\n/)
    const pkgMatch = src.match(/\bpackage\s+([A-Za-z0-9_.]+)\s*;/)
    const pkg = pkgMatch ? pkgMatch[1] : 'default'
    const map = new Map()
    let pending = []
    function flushPending() {
      const txt = pending.join('\n').trim()
      pending = []
      return txt.length ? txt : undefined
    }
    let stack = []
    function full(n) { return pkg + '.' + n }
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      const trimmed = line.trim()
      if (trimmed.startsWith('/*')) {
        let block = trimmed
        while (!block.includes('*/') && i + 1 < lines.length) { i++; block += '\n' + lines[i] }
        const body = block.replace(/^\/*\*?/,'').replace(/\*\/\s*$/,'')
        pending.push(body)
        continue
      }
      if (trimmed.startsWith('//')) { pending.push(trimmed.replace(/^\/\//,'')); continue }
      const msgMatch = trimmed.match(/^message\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
      const enumMatch = trimmed.match(/^enum\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
      const svcMatch = trimmed.match(/^service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
      const oneofMatch = trimmed.match(/^oneof\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
      if (msgMatch) { const name = msgMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'message', name }) ; continue }
      if (enumMatch) { const name = enumMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'enum', name }) ; continue }
      if (svcMatch) { const name = svcMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'service', name }) ; continue }
      if (oneofMatch) { const name = oneofMatch[1]; const c = flushPending(); if (c && stack[stack.length-1]?.kind==='message') map.set(full(stack[stack.length-1].name)+`#oneof:${name}`, c); stack.push({ kind: 'oneof', name }); continue }
      if (trimmed.startsWith('}')) { stack.pop(); continue }
      if (stack.length) {
        const top = stack[stack.length-1]
        if (top.kind === 'message') {
          const fieldInline = trimmed.match(/^(?:repeated\s+|optional\s+)?(?:map<[^>]+>|[A-Za-z_][A-Za-z0-9_.]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\d+.*?;/)
          if (fieldInline) {
            const name = fieldInline[1]
            let trailing = undefined
            const idx = line.indexOf('//')
            if (idx >= 0) trailing = line.slice(idx + 2).trim()
            const bidx = line.indexOf('/*')
            if (bidx >= 0) {
              let block = line.slice(bidx + 2)
              if (block.includes('*/')) {
                block = block.split('*/')[0]
              } else {
                let j = i + 1
                while (j < lines.length) {
                  const ln = lines[j]
                  const pos = ln.indexOf('*/')
                  if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
                  block += '\n' + ln
                  j++
                }
              }
              trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
            }
            const c = trailing || flushPending()
            if (c) map.set(full(top.name)+`#${name}`, c)
          }
        } else if (top.kind === 'enum') {
          const valInline = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[-]?(?:0[xX][0-9a-fA-F]+|\d+)\s*;/)
          if (valInline) {
            const name = valInline[1]
            let trailing = undefined
            const idx = line.indexOf('//')
            if (idx >= 0) trailing = line.slice(idx + 2).trim()
            const bidx = line.indexOf('/*')
            if (bidx >= 0) {
              let block = line.slice(bidx + 2)
              if (block.includes('*/')) {
                block = block.split('*/')[0]
              } else {
                let j = i + 1
                while (j < lines.length) {
                  const ln = lines[j]
                  const pos = ln.indexOf('*/')
                  if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
                  block += '\n' + ln
                  j++
                }
              }
              trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
            }
            const c = trailing || flushPending()
            if (c) map.set(full(top.name)+`#${name}`, c)
          }
        } else if (top.kind === 'service') {
          const rpcInline = trimmed.match(/^rpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/)
          if (rpcInline) {
            const name = rpcInline[1]
            let trailing = undefined
            const idx = line.indexOf('//')
            if (idx >= 0) trailing = line.slice(idx + 2).trim()
            const bidx = line.indexOf('/*')
            if (bidx >= 0) {
              let block = line.slice(bidx + 2)
              if (block.includes('*/')) {
                block = block.split('*/')[0]
              } else {
                let j = i + 1
                while (j < lines.length) {
                  const ln = lines[j]
                  const pos = ln.indexOf('*/')
                  if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
                  block += '\n' + ln
                  j++
                }
              }
              trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
            }
            const c = trailing || flushPending()
            if (c) map.set(full(top.name)+`#method:${name}`, c)
          }
        }
      }
    }
    return map
  }
  const globalComments = new Map()
  for (const f of preFiles) {
    const srcText0 = fs.readFileSync(f, 'utf-8')
    const cm0 = extractComments(srcText0)
    for (const [k,v] of cm0.entries()) if (!globalComments.has(k)) globalComments.set(k, v)
  }
  const commentOfGlobal = (key) => globalComments.get(key)
  const pkgDirs = new Set()
  const pkgIndex = new Map()
  for (const f of preFiles) {
    const root = await protobuf.load(f)
    root.resolveAll()
    walk(root, (obj) => {
      const pkgName = pkgFromObj(obj)
      const segs = packageSegmentsFromObj(obj)
      const fullPkgName = segs.join('.')
      const pkgDir = outBaseDirForSegments(opts, segs)
      ensureDir(pkgDir)
      ensureDir(path.join(pkgDir, 'messages'))
      ensureDir(path.join(pkgDir, 'enums'))
      ensureDir(path.join(pkgDir, 'services'))
      if (!pkgDirs.has(fullPkgName)) {
        pkgDirs.add(fullPkgName)
        pkgIndex.set(fullPkgName, { messages: new Set(), enums: new Set(), services: new Set() })
      } else {
        if (!pkgIndex.has(fullPkgName)) {
          pkgIndex.set(fullPkgName, { messages: new Set(), enums: new Set(), services: new Set() })
        }
      }
      if (obj instanceof protobuf.Type) {
        const code = renderMessage(pkgDir, pkgName, obj, opts.int64, opts.packed === 'on', opts.docs === 'on', opts.omitDefaults === 'on', opts.json === 'on', commentOfGlobal, opts.jsonEnum, opts.jsonStrict, opts)
        fs.writeFileSync(path.join(pkgDir, 'messages', obj.name + '.ets'), code)
        pkgIndex.get(fullPkgName).messages.add(obj.name)
      } else if (obj instanceof protobuf.Enum) {
        const enumLines = []
        if (opts.docs === 'on') enumLines.push(...formatDoc(commentOfGlobal((obj.fullName||'').replace(/^\./,'')) || obj.comment))
        enumLines.push(`export enum ${obj.name} {`)
        const commentsMap = (obj.comments || {})
        const entries = Object.entries(obj.values)
        for (let i = 0; i < entries.length; i++) {
          const [k,v] = entries[i]
          const vc = commentOfGlobal(`${(obj.fullName||'').replace(/^\./,'')}#${k}`) || commentsMap[k]
          if (opts.docs === 'on' && vc) enumLines.push(...formatDoc(vc).map(s => '  ' + s))
          const comma = i < entries.length - 1 ? ',' : ''
          enumLines.push(`  ${k} = ${v}${comma}`)
        }
        enumLines.push('}')
        enumLines.push('')
        fs.writeFileSync(path.join(pkgDir, 'enums', obj.name + '.ets'), enumLines.join('\n'))
        pkgIndex.get(fullPkgName).enums.add(obj.name)
      } else if (obj instanceof protobuf.Service) {
        const svcName = obj.name
        const methods = Object.entries(obj.methods).map(([n,m]) => ({ n, m }))
        const used = new Map()
        const imports = new Map()
        const currSvcDir = path.join(pkgDir, 'services')
        for (const { n, m } of methods) {
          const reqT = m.resolvedRequestType
          const resT = m.resolvedResponseType
          for (const T of [reqT, resT]) {
            const full = (T.fullName || '').replace(/^\./,'')
            const depPkg = full.split('.')[0] || 'default'
            const relRaw = path.relative(currSvcDir, outPathForFull(opts, full, 'messages')).replace(/\\/g,'/')
            const relNoExt = relRaw.replace(/\.ets$/,'')
            const rel = relNoExt.startsWith('.') ? relNoExt : './' + relNoExt
            let local = T.name
            if (used.has(local) && used.get(local) !== full) local = `${local}_${depPkg}`
            used.set(local, full)
            imports.set(full, { base: T.name, local, rel })
          }
        }
        const rtRelRawSvc = path.relative(currSvcDir, path.join(opts.out, opts.runtimeDir)).replace(/\\/g,'/')
        const rtRelSvc = rtRelRawSvc.startsWith('.') ? rtRelRawSvc : './' + rtRelRawSvc
        const importLines = ["import { RpcTransport, Writer, Reader } from '" + rtRelSvc + "'"]
        const sorted = Array.from(imports.values()).sort((a,b)=> a.local.localeCompare(b.local) || a.rel.localeCompare(b.rel))
        for (const d of sorted) {
          if (d.local === d.base) importLines.push(`import { ${d.base} } from '${d.rel}'`)
          else importLines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
        }
        const codeLines = []
        codeLines.push(...importLines)
        if (opts.docs === 'on') codeLines.push(...formatDoc(commentOfGlobal((obj.fullName||'').replace(/^\./,'')) || obj.comment))
        if (opts.concurrent === 'sendable') codeLines.push('@Sendable')
        codeLines.push(`export class ${svcName}Client {`)
        codeLines.push('  private t: RpcTransport')
        codeLines.push('  constructor(t: RpcTransport) { this.t = t }')
        for (const { n, m } of methods) {
          const reqT = m.resolvedRequestType
          const resT = m.resolvedResponseType
          const reqFull = (reqT.fullName || '').replace(/^\./,'')
          const resFull = (resT.fullName || '').replace(/^\./,'')
          const reqLocal = imports.get(reqFull).local
          const resLocal = imports.get(resFull).local
          if (opts.docs === 'on') {
            const mc = commentOfGlobal(`${(obj.fullName||'').replace(/^\./,'')}#method:${n}`) || m.comment
            const doc = mc ? `${mc}\n@rpc /${pkgName}.${svcName}/${n}\n@param req ${reqLocal}\n@returns ${resLocal}` : `@rpc /${pkgName}.${svcName}/${n}\n@param req ${reqLocal}\n@returns ${resLocal}`
            codeLines.push(...formatDoc(doc).map(s => '  ' + s))
          }
          codeLines.push(`  async ${n}(req: ${reqLocal}): Promise<${resLocal}> {`)
          codeLines.push('    const w = new Writer()')
          codeLines.push(`    ${reqLocal}.encode(req, w)`) 
          codeLines.push(`    const res = await this.t.unary('/${pkgName}.${svcName}/${n}', w.finish())`)
          codeLines.push('    return ' + `${resLocal}.decode(new Reader(res))`)
          codeLines.push('  }')
        }
        codeLines.push('}')
        fs.writeFileSync(path.join(pkgDir, 'services', svcName + '.ets'), codeLines.join('\n'))
        pkgIndex.get(fullPkgName).services.add(svcName)
      }
    })
  }
    function mergeIndex(idxPath, lines) {
      // Always overwrite index.ets to avoid stale exports or conflicts
      const content = lines.join('\n') + '\n'
      fs.writeFileSync(idxPath, content)
    }
    for (const fullPkgName of pkgDirs) {
      const segs = fullPkgName.split('.')
      const pkgDir = outBaseDirForSegments(opts, segs)
      const index = pkgIndex.get(fullPkgName)
      const enumsLines = []
      const messagesLines = []
      const servicesLines = []
      if (index.enums.size) {
        for (const e of Array.from(index.enums).sort()) enumsLines.push(`export { ${e} } from './${e}'`)
        ensureDir(path.join(pkgDir, 'enums'))
        mergeIndex(path.join(pkgDir, 'enums', 'index.ets'), enumsLines)
      }
      if (index.messages.size) {
        for (const m of Array.from(index.messages).sort()) {
          messagesLines.push(`export { ${m} } from './${m}'`)
          // Also export the CreateInput interface which is always generated
          messagesLines.push(`export { ${m}CreateInput } from './${m}'`)
          
          if (opts.json === 'on') {
             messagesLines.push(`export { ${m}JSON } from './${m}'`)
          }
        }
        ensureDir(path.join(pkgDir, 'messages'))
        mergeIndex(path.join(pkgDir, 'messages', 'index.ets'), messagesLines)
      }
      if (index.services.size) {
        for (const s of Array.from(index.services).sort()) servicesLines.push(`export { ${s}Client } from './${s}'`)
        ensureDir(path.join(pkgDir, 'services'))
        mergeIndex(path.join(pkgDir, 'services', 'index.ets'), servicesLines)
      }
      
      const lines = []
      if (index.enums.size) lines.push("export * from './enums'")
      if (index.messages.size) lines.push("export * from './messages'")
      if (index.services.size) lines.push("export * from './services'")
      mergeIndex(path.join(pkgDir, 'index.ets'), lines)
    }
    if (opts.namespaceAsFile === 'on') {
      function buildIndexesRecursive(dir) {
        const entries = fs.readdirSync(dir, { withFileTypes: true })
        const subdirs = entries.filter(e => e.isDirectory()).map(e => e.name)
        let lines = []
        for (const sd of subdirs) {
          const childIdx = path.join(dir, sd, 'index.ets')
          if (fs.existsSync(childIdx)) lines.push(`export * from './${sd}'`)
          buildIndexesRecursive(path.join(dir, sd))
        }
        if (lines.length) {
          const idxPath = path.join(dir, 'index.ets')
          mergeIndex(idxPath, lines)
        }
      }
      buildIndexesRecursive(opts.out)
    }
}

main().catch(e => { console.error(e); process.exit(1) })