/**
 * Cursor Protobuf Encoder
 * Implements ConnectRPC protobuf wire format encoding
 */

import * as zlib from 'zlib';
import {
  WIRE_TYPE,
  FIELD,
  COMPRESS_FLAG,
  UNIFIED_MODE,
  type WireType,
  type RoleType,
  type CursorTool,
  type CursorToolResult,
} from './cursor-protobuf-schema.js';

/**
 * Encode a varint (variable-length integer)
 */
export function encodeVarint(value: number): Uint8Array {
  const bytes: number[] = [];
  let val = value >>> 0; // Ensure unsigned
  while (val >= 0x80) {
    bytes.push((val & 0x7f) | 0x80);
    val >>>= 7;
  }
  bytes.push(val & 0x7f);
  return new Uint8Array(bytes);
}

/**
 * Encode a protobuf field (tag + value)
 */
export function encodeField(
  fieldNum: number,
  wireType: WireType,
  value: number | string | Uint8Array
): Uint8Array {
  const tag = (fieldNum << 3) | wireType;
  const tagBytes = encodeVarint(tag);

  if (wireType === WIRE_TYPE.VARINT) {
    const valueBytes = encodeVarint(value as number);
    return concatArrays(tagBytes, valueBytes);
  }

  if (wireType === WIRE_TYPE.LEN) {
    const dataBytes =
      typeof value === 'string'
        ? new TextEncoder().encode(value)
        : value instanceof Uint8Array
          ? value
          : new Uint8Array(0);

    const lengthBytes = encodeVarint(dataBytes.length);
    return concatArrays(tagBytes, lengthBytes, dataBytes);
  }

  return new Uint8Array(0);
}

/**
 * Concatenate multiple Uint8Arrays
 */
export function concatArrays(...arrays: Uint8Array[]): Uint8Array {
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}

/**
 * Encode a tool result
 */
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
  const toolCallId = toolResult.tool_call_id || '';
  const toolName = toolResult.name || '';
  const toolIndex = toolResult.index || 0;
  const rawArgs = toolResult.raw_args || '{}';

  return concatArrays(
    encodeField(FIELD.ToolResult.CALL_ID, WIRE_TYPE.LEN, toolCallId),
    encodeField(FIELD.ToolResult.NAME, WIRE_TYPE.LEN, toolName),
    encodeField(FIELD.ToolResult.INDEX, WIRE_TYPE.VARINT, toolIndex),
    encodeField(FIELD.ToolResult.RAW_ARGS, WIRE_TYPE.LEN, rawArgs)
  );
}

/**
 * Encode a conversation message
 */
export function encodeMessage(
  content: string,
  role: RoleType,
  messageId: string,
  isLast: boolean,
  hasTools: boolean,
  toolResults: CursorToolResult[]
): Uint8Array {
  return concatArrays(
    encodeField(FIELD.Message.CONTENT, WIRE_TYPE.LEN, content),
    encodeField(FIELD.Message.ROLE, WIRE_TYPE.VARINT, role),
    encodeField(FIELD.Message.ID, WIRE_TYPE.LEN, messageId),
    ...(toolResults.length > 0
      ? toolResults.map((tr) =>
          encodeField(FIELD.Message.TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
        )
      : []),
    encodeField(FIELD.Message.IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
    encodeField(
      FIELD.Message.UNIFIED_MODE,
      WIRE_TYPE.VARINT,
      hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
    ),
    ...(isLast && hasTools
      ? [encodeField(FIELD.Message.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
      : [])
  );
}

/**
 * Encode instruction text
 */
export function encodeInstruction(text: string): Uint8Array {
  return text ? encodeField(FIELD.Instruction.TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0);
}

/**
 * Encode model information
 */
export function encodeModel(modelName: string): Uint8Array {
  return concatArrays(
    encodeField(FIELD.Model.NAME, WIRE_TYPE.LEN, modelName),
    encodeField(FIELD.Model.EMPTY, WIRE_TYPE.LEN, new Uint8Array(0))
  );
}

/**
 * Encode cursor settings
 */
export function encodeCursorSetting(): Uint8Array {
  const unknown6 = concatArrays(
    encodeField(FIELD.Setting.U6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
    encodeField(FIELD.Setting.U6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0))
  );

  return concatArrays(
    encodeField(FIELD.Setting.PATH, WIRE_TYPE.LEN, 'cursor\\aisettings'),
    encodeField(FIELD.Setting.UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)),
    encodeField(FIELD.Setting.UNKNOWN_6, WIRE_TYPE.LEN, unknown6),
    encodeField(FIELD.Setting.UNKNOWN_8, WIRE_TYPE.VARINT, 1),
    encodeField(FIELD.Setting.UNKNOWN_9, WIRE_TYPE.VARINT, 1)
  );
}

/**
 * Encode metadata
 */
export function encodeMetadata(): Uint8Array {
  return concatArrays(
    encodeField(FIELD.Metadata.PLATFORM, WIRE_TYPE.LEN, process.platform || 'linux'),
    encodeField(FIELD.Metadata.ARCH, WIRE_TYPE.LEN, process.arch || 'x64'),
    encodeField(FIELD.Metadata.VERSION, WIRE_TYPE.LEN, process.version || 'v20.0.0'),
    encodeField(FIELD.Metadata.CWD, WIRE_TYPE.LEN, process.cwd() || '/'),
    encodeField(FIELD.Metadata.TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
  );
}

/**
 * Encode message ID
 */
export function encodeMessageId(messageId: string, role: RoleType, summaryId?: string): Uint8Array {
  return concatArrays(
    encodeField(FIELD.MessageId.ID, WIRE_TYPE.LEN, messageId),
    ...(summaryId ? [encodeField(FIELD.MessageId.SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
    encodeField(FIELD.MessageId.ROLE, WIRE_TYPE.VARINT, role)
  );
}

/**
 * Encode MCP tool
 */
export function encodeMcpTool(tool: CursorTool): Uint8Array {
  const toolName = tool.function?.name || tool.name || '';
  const toolDesc = tool.function?.description || tool.description || '';
  const inputSchema = tool.function?.parameters || tool.input_schema || {};

  return concatArrays(
    ...(toolName ? [encodeField(FIELD.McpTool.NAME, WIRE_TYPE.LEN, toolName)] : []),
    ...(toolDesc ? [encodeField(FIELD.McpTool.DESC, WIRE_TYPE.LEN, toolDesc)] : []),
    ...(Object.keys(inputSchema).length > 0
      ? [encodeField(FIELD.McpTool.PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
      : []),
    encodeField(FIELD.McpTool.SERVER, WIRE_TYPE.LEN, 'custom')
  );
}

/**
 * Wrap payload in ConnectRPC frame (5-byte header + payload)
 */
export function wrapConnectRPCFrame(payload: Uint8Array, compress = false): Uint8Array {
  let finalPayload = payload;
  let flags: number = COMPRESS_FLAG.NONE;

  if (compress) {
    finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload)));
    flags = COMPRESS_FLAG.GZIP;
  }

  const frame = new Uint8Array(5 + finalPayload.length);
  frame[0] = flags;
  frame[1] = (finalPayload.length >> 24) & 0xff;
  frame[2] = (finalPayload.length >> 16) & 0xff;
  frame[3] = (finalPayload.length >> 8) & 0xff;
  frame[4] = finalPayload.length & 0xff;
  frame.set(finalPayload, 5);

  return frame;
}
