import { BadRequestException, Injectable } from "@nestjs/common";
import { createRequire } from "module";
import { Observable, Subject } from "rxjs";
import { PassThrough } from "stream";

type ResolvedUpstream = { url: string; host: string; hostname: string };
type MessageEventLike = { data: unknown; type?: string };

@Injectable()
export class AmfProxyService {
  private readonly defaultUpstreamUrl = process.env.AMF_PROXY_UPSTREAM_URL || "https://play.ninjasage.id/amf";
  private readonly allowedHosts = String(process.env.AMF_PROXY_ALLOWED_HOSTS || "play.ninjasage.id")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);

  private readonly defaultHeaders: Record<string, string> = {
    Referer: process.env.AMF_PROXY_REFERER || "app:/NinjaSage.swf",
    Accept:
      process.env.AMF_PROXY_ACCEPT ||
      "text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, text/css, image/png, image/jpeg, image/gif;q=0.8, application/x-shockwave-flash, video/mp4;q=0.9, flv-application/octet-stream;q=0.8, video/x-flv;q=0.7, audio/mp4, application/futuresplash, */*;q=0.5, application/x-mpegURL",
    "x-flash-version": process.env.AMF_PROXY_FLASH_VERSION || "51,1,3,10",
    "x-air-appid": "pta5yAteNUXGZU30lxDv+BRLP9xn3HZsknJnVEyQZU4=",
    "User-Agent":
      process.env.AMF_PROXY_USER_AGENT ||
      "Mozilla/5.0 (Windows; U; en) AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/51.1",
    "Content-Type": "application/x-amf",
  };

  private readonly passthruHeaders = String(process.env.AMF_PROXY_PASSTHRU_HEADERS || "cookie,x-ninjasage-cookie")
    .split(",")
    .map((s) => s.trim().toLowerCase())
    .filter(Boolean);

  private readonly timeoutMs = Math.max(1_000, Number.parseInt(process.env.AMF_PROXY_TIMEOUT_MS || "30000", 10) || 30_000);
  private readonly leaderboardTimeoutMs = Math.max(
    1_000,
    Number.parseInt(process.env.AMF_PROXY_LEADERBOARD_TIMEOUT_MS || "8000", 10) || 8_000
  );
  private readonly maxBytes = Math.max(0, Number.parseInt(process.env.AMF_PROXY_MAX_BYTES || String(10 * 1024 * 1024), 10) || 0);

  private readonly require = createRequire(import.meta.url);
  private readonly leaderboardStreams = new Map<
    string,
    { subject: Subject<MessageEventLike>; refCount: number; timer: NodeJS.Timeout | null }
  >();

  resolveUpstream(urlOverride: string | undefined): ResolvedUpstream {
    const urlStr = String(urlOverride || this.defaultUpstreamUrl || "").trim();
    let u: URL;
    try {
      u = new URL(urlStr);
    } catch {
      throw new BadRequestException("bad_url");
    }
    if (!this.allowedHosts.includes(u.hostname)) {
      throw new BadRequestException("host_not_allowed");
    }
    return { url: u.toString(), host: u.host, hostname: u.hostname };
  }

  buildUpstreamHeaders(reqHeaders: Record<string, unknown>, upstreamHost: string): Record<string, string> {
    const headers: Record<string, string> = { ...this.defaultHeaders, Host: upstreamHost };
    for (const name of this.passthruHeaders) {
      const v = reqHeaders[name];
      if (typeof v === "string" && v.trim() !== "") {
        headers[name] = v;
      }
    }
    return headers;
  }

  async fetchUpstream(
    url: string,
    headers: Record<string, string>,
    body: Uint8Array,
    signal?: AbortSignal,
    timeoutMsOverride?: number
  ): Promise<Response> {
    const controller = new AbortController();
    const timeoutMs = Number.isFinite(timeoutMsOverride as any) ? Math.floor(Number(timeoutMsOverride)) : this.timeoutMs;
    const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
    const signals: AbortSignal[] = [controller.signal];
    if (signal) signals.push(signal);
    const anyFn = (AbortSignal as any).any as undefined | ((sigs: AbortSignal[]) => AbortSignal);
    const externalAbortHandler = signal && !anyFn ? () => controller.abort() : null;
    if (signal && externalAbortHandler) {
      if (signal.aborted) controller.abort();
      else signal.addEventListener("abort", externalAbortHandler, { once: true });
    }
    const combined = anyFn ? anyFn(signals) : controller.signal;
    try {
      return await fetch(url, {
        method: "POST",
        headers,
        body: body as any,
        signal: combined,
      });
    } finally {
      if (timer) clearTimeout(timer);
      if (signal && externalAbortHandler) signal.removeEventListener("abort", externalAbortHandler);
    }
  }

  getMaxBytes(): number {
    return this.maxBytes;
  }

  async fetchLeaderboard(params: { charId: number; sessionKey: string; category: string; url?: string }): Promise<any> {
    const { charId, sessionKey, category } = params;
    const resolved = this.resolveUpstream(params.url);

    const headers = this.buildUpstreamHeaders({}, resolved.host);
    const body = this.buildLeaderboardGetDataRequest({ charId, sessionKey, category });
    const upstream = await this.fetchUpstream(resolved.url, headers, body, undefined, this.leaderboardTimeoutMs);
    const respBuf = Buffer.from(await upstream.arrayBuffer());

    if (!upstream.ok) {
      return { status: 0, error: "upstream_http_error", http_status: upstream.status };
    }

    try {
      const envelope = this.decodeRemotingEnvelope(respBuf);
      return envelope.bodies[0]?.value ?? null;
    } catch (e: any) {
      return { status: 0, error: "decode_error", message: String(e?.message || e) };
    }
  }

  async fetchSystemLoginGetAllCharacters(params: { userId: string | number; sessionKey: string; url?: string }): Promise<any> {
    const { userId, sessionKey } = params;
    const resolved = this.resolveUpstream(params.url);

    const headers = this.buildUpstreamHeaders({}, resolved.host);
    const body = this.buildRemotingRequest({
      target: "SystemLogin.getAllCharacters",
      args: [userId, sessionKey],
    });

    const upstream = await this.fetchUpstream(resolved.url, headers, body, undefined, this.timeoutMs);
    const respBuf = Buffer.from(await upstream.arrayBuffer());

    if (!upstream.ok) {
      return { status: 0, error: "upstream_http_error", http_status: upstream.status };
    }

    try {
      const envelope = this.decodeRemotingEnvelope(respBuf);
      return { status: 1, decoded: envelope };
    } catch (e: any) {
      return { status: 0, error: "decode_error", message: String(e?.message || e) };
    }
  }

  private generateRandomToken(length: number): string {
    const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    let result = "";
    for (let i = 0; i < length; i++) {
      result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
  }

  private generateBigConstant(): string {
    // Generates a string of 30-40 random digits
    let result = "";
    const length = 35 + Math.floor(Math.random() * 5); // 35-40 digits
    for (let i = 0; i < length; i++) {
      result += Math.floor(Math.random() * 10).toString();
    }
    return result;
  }

  async fetchSystemLoginCheckVersion(url?: string): Promise<{ token: string; timestamp: number }> {
    const resolved = this.resolveUpstream(url);
    const headers = this.buildUpstreamHeaders({}, resolved.host);
    const body = this.buildRemotingRequest({
      target: "SystemLogin.checkVersion",
      args: ["Public 0.53.1"],
    });

    const upstream = await this.fetchUpstream(resolved.url, headers, body, undefined, this.timeoutMs);
    const respBuf = Buffer.from(await upstream.arrayBuffer());

    if (!upstream.ok) {
      throw new Error(`upstream_http_error_${upstream.status}`);
    }

    let token = "";
    let timestamp = 0;

    // Search for token marker: 05 5F 5F 06 (Double marker, string marker?)
    // Based on python script logic
    const tokenMarker = Buffer.from([0x05, 0x5f, 0x5f, 0x06]);
    const tmIdx = respBuf.indexOf(tokenMarker);
    if (tmIdx !== -1) {
        const lenByte = respBuf[tmIdx + 4];
        const tokenLen = lenByte >> 1;
        token = respBuf.subarray(tmIdx + 5, tmIdx + 5 + tokenLen).toString("utf8");
    }

    // Search for timestamp marker: 03 5F 05
    const tsMarker = Buffer.from([0x03, 0x5f, 0x05]);
    const tsIdx = respBuf.indexOf(tsMarker);
    if (tsIdx !== -1) {
        timestamp = respBuf.readDoubleBE(tsIdx + 3);
    }

    console.log(`[CheckVersion] Token: ${token}, Timestamp: ${timestamp}`);

    return { 
        token: token || "g58hFak2chzNeteQ", 
        timestamp: timestamp || Math.floor(Date.now() / 1000) 
    };
  }

  async fetchSystemLoginLoginUser(params: { 
    username: string; 
    passwordHash: string; 
    url?: string;
    // Optional overrides for replay/captured credentials
    timestamp?: number;
    token?: string;
    signature?: string;
  }): Promise<any> {
    const { username, passwordHash } = params;
    
    // Updated constants from captured AMF (elena26)
    let token = params.token || "yvfgyKDAjm4wJ0Pk"; 
    let timestamp = params.timestamp || Math.floor(Date.now() / 1000);

    // If we want to dynamically check version, we can uncomment this, 
    // but for now we use the latest known working constants.
    /*
    try {
        const ver = await this.fetchSystemLoginCheckVersion(params.url);
        if (ver.token) token = ver.token;
        if (ver.timestamp) timestamp = ver.timestamp;
    } catch (e) {
        console.warn("CheckVersion failed, using defaults", e);
    }
    */

    const resolved = this.resolveUpstream(params.url);
    const headers = this.buildUpstreamHeaders({}, resolved.host);
    
    // Updated Hash Constant (SHA-256 length observed in AMF)
    const constantHash = "40367c3cc999a9f9e951a1d33211545b84b2d5a63933b0020433000c3bb410fb";
    
    // Updated Signature Logic: TS + HASH + TS*4
    // Only generate if not provided
    const signature = params.signature || `${timestamp}${constantHash}${timestamp}${timestamp}${timestamp}${timestamp}`;

    const loginArgs = [
      username,
      passwordHash,
      timestamp,
      8101544, // Arg 3 (Updated from 1011067)
      8101544, // Arg 4 (Updated from 1011067)
      token,
      signature,
      "99149538210645874119841391921301329328", // Arg 7 (Updated)
      9 // Arg 8 (Updated from 11)
    ];

    const body = this.buildRemotingRequest({
      target: "SystemLogin.loginUser",
      // IMPORTANT: Server expects a single argument which is an Array of params.
      // So we wrap loginArgs in another array.
      args: [loginArgs],
    });

    const upstream = await this.fetchUpstream(resolved.url, headers, body, undefined, this.timeoutMs);
    const respBuf = Buffer.from(await upstream.arrayBuffer());

    if (!upstream.ok) {
      return { status: 0, error: "upstream_http_error", http_status: upstream.status };
    }

    try {
      const envelope = this.decodeRemotingEnvelope(respBuf);
      return { status: 1, decoded: envelope, debug_args: loginArgs };
    } catch (e: any) {
      return { status: 0, error: "decode_error", message: String(e?.message || e), debug_args: loginArgs };
    }
  }

  private generateLoginSignature(timestamp: number, token: string): string {

    // Structure observed: timestamp + sha256_hash + timestamp*4
    // We use a best-effort hash since the secret is unknown
    const crypto = this.require("crypto");
    // Placeholder hash logic: sha256(timestamp + token)
    const hash = crypto.createHash("sha256").update(`${timestamp}${token}`).digest("hex");
    return `${timestamp}${hash}${timestamp}${timestamp}${timestamp}${timestamp}`;
  }

  buildRemotingRequest(params: { target: string; args: unknown[] }): Uint8Array {
    const target = Buffer.from(String(params.target || ""), "utf8");
    if (!target.length) throw new BadRequestException("target_required");

    const response = Buffer.from("/1", "utf8");
    const argsAmf3 = this.encodeAmf3(params.args || []);

    console.log(`[Remoting] Target: ${params.target}, Args Type: ${Array.isArray(params.args) ? "Array" : typeof params.args}, Args Len: ${Array.isArray(params.args) ? params.args.length : "N/A"}`);
    // console.log(`[Remoting] Args:`, JSON.stringify(params.args));

    const body = Buffer.concat([Buffer.from([0x0a]), this.writeU32BE(1), Buffer.from([0x11]), argsAmf3]);

    const envelope = Buffer.concat([
      this.writeU16BE(3),
      this.writeU16BE(0),
      this.writeU16BE(1),
      this.writeUtf8WithU16Len(target),
      this.writeUtf8WithU16Len(response),
      this.writeU32BE(0),
      body,
    ]);

    return envelope;
  }

  leaderboardStream(params: {
    charId: number;
    sessionKey: string;
    category: string;
    intervalMs: number;
    url?: string;
  }): Observable<MessageEventLike> {
    const key = String(params.sessionKey || "").trim();
    const categoryKey = String(params.category || "").trim();
    if (!key) throw new BadRequestException("session_key_required");
    if (!categoryKey) throw new BadRequestException("category_required");
    if (!Number.isFinite(params.charId)) throw new BadRequestException("char_id_required");

    const intervalMs = Math.max(1000, Math.min(60000, Math.floor(Number(params.intervalMs) || 5000)));
    const streamKey = [key, params.charId, categoryKey, intervalMs, String(params.url || "")].join("|");

    let entry = this.leaderboardStreams.get(streamKey);
    if (!entry) {
      const subject = new Subject<MessageEventLike>();
      let inFlight = false;
      const poll = async () => {
        if (inFlight) return;
        inFlight = true;
        try {
          const payload = await this.fetchLeaderboard({
            charId: params.charId,
            sessionKey: key,
            category: categoryKey,
            url: params.url,
          });

          if (payload && typeof payload === "object" && (payload as any).status === 1 && (payload as any).error === 0) {
            subject.next({ type: "leaderboard", data: payload });
          } else {
            subject.next({ type: "error", data: payload });
          }
        } catch (e: any) {
          subject.next({ type: "error", data: { error: "fetch_error", message: String(e?.message || e) } });
        } finally {
          inFlight = false;
        }
      };

      void poll();
      const timer = setInterval(() => void poll(), intervalMs);
      entry = { subject, refCount: 0, timer };
      this.leaderboardStreams.set(streamKey, entry);
    }

    entry.refCount += 1;

    return new Observable<MessageEventLike>((sub) => {
      sub.next({ type: "meta", data: { interval_ms: intervalMs, category: categoryKey } });
      const s = entry.subject.subscribe({
        next: (v) => sub.next(v),
        error: (err) => sub.error(err),
        complete: () => sub.complete(),
      });

      return () => {
        s.unsubscribe();
        const cur = this.leaderboardStreams.get(streamKey);
        if (!cur) return;
        cur.refCount = Math.max(0, cur.refCount - 1);
        if (cur.refCount === 0) {
          if (cur.timer) clearInterval(cur.timer);
          cur.subject.complete();
          this.leaderboardStreams.delete(streamKey);
        }
      };
    });
  }

  private buildLeaderboardGetDataRequest(params: { charId: number; sessionKey: string; category: string }): Uint8Array {
    const { charId, sessionKey, category } = params;

    const target = Buffer.from("LeaderboardService.getData", "utf8");
    const response = Buffer.from("/1", "utf8");

    const argsAmf3 = this.encodeAmf3([charId, sessionKey, category]);
    const body = Buffer.concat([
      Buffer.from([0x0a]),
      this.writeU32BE(1),
      Buffer.from([0x11]),
      argsAmf3,
    ]);

    const envelope = Buffer.concat([
      this.writeU16BE(3),
      this.writeU16BE(0),
      this.writeU16BE(1),
      this.writeUtf8WithU16Len(target),
      this.writeUtf8WithU16Len(response),
      this.writeU32BE(0),
      body,
    ]);

    return envelope;
  }

  decodeRemotingEnvelope(buf: Buffer): {
    version: number;
    headers: Array<{ name: string; must_understand: boolean; value: unknown }>;
    bodies: Array<{ target: string; response: string; value: unknown }>;
  } {
    let o = 0;

    const readU8 = () => {
      if (o + 1 > buf.length) throw new Error("unexpected_eof");
      const v = buf.readUInt8(o);
      o += 1;
      return v;
    };
    const readU16 = () => {
      if (o + 2 > buf.length) throw new Error("unexpected_eof");
      const v = buf.readUInt16BE(o);
      o += 2;
      return v;
    };
    const readU32 = () => {
      if (o + 4 > buf.length) throw new Error("unexpected_eof");
      const v = buf.readUInt32BE(o);
      o += 4;
      return v;
    };
    const readUtf8U16 = () => {
      const len = readU16();
      if (o + len > buf.length) throw new Error("unexpected_eof");
      const s = buf.slice(o, o + len).toString("utf8");
      o += len;
      return s;
    };

    const version = readU16();
    const headerCount = readU16();

    const headers: Array<{ name: string; must_understand: boolean; value: unknown }> = [];
    for (let i = 0; i < headerCount; i++) {
      const name = readUtf8U16();
      const mustUnderstand = readU8() !== 0;
      readU32();
      const len = readU32();

      if (len === 0xffffffff) {
        const decoded = this.decodeAmf0Value(buf, o);
        o = decoded.offset;
        headers.push({ name, must_understand: mustUnderstand, value: decoded.value });
      } else {
        if (o + len > buf.length) throw new Error("unexpected_eof");
        const slice = buf.slice(o, o + len);
        o += len;
        try {
          const decoded = this.decodeAmf0Value(slice, 0, slice.length);
          headers.push({ name, must_understand: mustUnderstand, value: decoded.value });
        } catch {
          headers.push({ name, must_understand: mustUnderstand, value: null });
        }
      }
    }

    const bodyCount = readU16();
    const bodies: Array<{ target: string; response: string; value: unknown }> = [];

    for (let i = 0; i < bodyCount; i++) {
      const target = readUtf8U16();
      const response = readUtf8U16();
      const contentLen = readU32();

      const remaining = buf.length - o;
      const payloadLen = contentLen === 0xffffffff || contentLen > remaining ? remaining : contentLen;
      if (payloadLen < 0) throw new Error("unexpected_eof");

      const slice = buf.slice(o, o + payloadLen);
      o += payloadLen;

      try {
        const decoded = this.decodeAmf0Value(slice, 0, slice.length);
        bodies.push({ target, response, value: decoded.value });
      } catch (e: any) {
        bodies.push({ target, response, value: { error: "decode_error", message: String(e?.message || e) } });
      }
    }

    return { version, headers, bodies };
  }

  extractSessionKey(value: unknown): string | null {
    const seen = new Set<unknown>();
    const isSessionKeyKey = (k: string) => /^session[-_]?key$/i.test(k);
    const isHashKey = (k: string) => /hash/i.test(k);
    const walk = (v: unknown, keyHint?: string): string | null => {
      if (v === null || v === undefined) return null;
      if (typeof v === "string") {
        const s = v.trim();
        if (keyHint && isSessionKeyKey(keyHint)) return s !== "" ? s : null;
        if (keyHint && isHashKey(keyHint)) return null;
        return null;
      }
      if (typeof v !== "object") return null;
      if (seen.has(v)) return null;
      seen.add(v);

      if (Array.isArray(v)) {
        for (const it of v) {
          const found = walk(it, keyHint);
          if (found) return found;
        }
        return null;
      }

      const entries = Object.entries(v as Record<string, unknown>);

      for (const [k, vv] of entries) {
        if (!isSessionKeyKey(k)) continue;
        const found = walk(vv, k);
        if (found) return found;
      }

      for (const [k, vv] of entries) {
        const found = walk(vv, k);
        if (found) return found;
      }
      return null;
    };
    return walk(value);
  }

  private encodeAmf3(value: unknown): Buffer {
    const amfjs = this.require("amfjs");
    const out = new PassThrough();
    const chunks: Buffer[] = [];
    out.on("data", (c) => chunks.push(Buffer.from(c)));
    const encoder = new amfjs.AMFEncoder(out);
    encoder.writeObject(value, amfjs.AMF3);
    out.end();
    return Buffer.concat(chunks);
  }

  private decodeAmf3(buf: Buffer): any {
    const amfjs = this.require("amfjs");
    const input = new PassThrough();
    input.end(buf);
    const decoder = new amfjs.AMFDecoder(input);
    return decoder.decode(amfjs.AMF3);
  }

  private decodeAmf0Value(buf: Buffer, startOffset: number, endOffset?: number): { value: unknown; offset: number } {
    let o = startOffset;
    const end = endOffset ?? buf.length;

    const need = (n: number) => {
      if (o + n > end) throw new Error("unexpected_eof");
    };
    const readU8 = () => {
      need(1);
      const v = buf.readUInt8(o);
      o += 1;
      return v;
    };
    const readU16 = () => {
      need(2);
      const v = buf.readUInt16BE(o);
      o += 2;
      return v;
    };
    const readU32 = () => {
      need(4);
      const v = buf.readUInt32BE(o);
      o += 4;
      return v;
    };
    const readDouble = () => {
      need(8);
      const v = buf.readDoubleBE(o);
      o += 8;
      return v;
    };
    const readStringU16 = () => {
      const len = readU16();
      need(len);
      const s = buf.slice(o, o + len).toString("utf8");
      o += len;
      return s;
    };
    const readStringU32 = () => {
      const len = readU32();
      need(len);
      const s = buf.slice(o, o + len).toString("utf8");
      o += len;
      return s;
    };

    const marker = readU8();
    if (marker === 0x00) {
      return { value: readDouble(), offset: o };
    }
    if (marker === 0x01) {
      return { value: readU8() !== 0, offset: o };
    }
    if (marker === 0x02) {
      return { value: readStringU16(), offset: o };
    }
    if (marker === 0x03) {
      const out: Record<string, unknown> = {};
      while (true) {
        need(2);
        const nameLen = buf.readUInt16BE(o);
        o += 2;
        if (nameLen === 0) {
          need(1);
          const endMarker = buf.readUInt8(o);
          o += 1;
          if (endMarker === 0x09) break;
          throw new Error("bad_object_end");
        }
        need(nameLen);
        const name = buf.slice(o, o + nameLen).toString("utf8");
        o += nameLen;
        const decoded = this.decodeAmf0Value(buf, o, end);
        o = decoded.offset;
        out[name] = decoded.value;
      }
      return { value: out, offset: o };
    }
    if (marker === 0x05) {
      return { value: null, offset: o };
    }
    if (marker === 0x06) {
      return { value: null, offset: o };
    }
    if (marker === 0x08) {
      readU32();
      const out: Record<string, unknown> = {};
      while (true) {
        need(2);
        const nameLen = buf.readUInt16BE(o);
        o += 2;
        if (nameLen === 0) {
          need(1);
          const endMarker = buf.readUInt8(o);
          o += 1;
          if (endMarker === 0x09) break;
          throw new Error("bad_ecma_end");
        }
        need(nameLen);
        const name = buf.slice(o, o + nameLen).toString("utf8");
        o += nameLen;
        const decoded = this.decodeAmf0Value(buf, o, end);
        o = decoded.offset;
        out[name] = decoded.value;
      }
      return { value: out, offset: o };
    }
    if (marker === 0x0a) {
      const n = readU32();
      const arr: unknown[] = [];
      for (let i = 0; i < n; i++) {
        const decoded = this.decodeAmf0Value(buf, o, end);
        o = decoded.offset;
        arr.push(decoded.value);
        if (o >= end) break;
      }
      return { value: arr, offset: o };
    }
    if (marker === 0x0b) {
      const ms = readDouble();
      readU16();
      return { value: new Date(ms).toISOString(), offset: o };
    }
    if (marker === 0x0c) {
      return { value: readStringU32(), offset: o };
    }
    if (marker === 0x11) {
      const amf3Buf = buf.slice(o, end);
      const v = this.decodeAmf3(amf3Buf);
      return { value: v, offset: end };
    }

    throw new Error(`unsupported_amf0_marker_${marker.toString(16)}`);
  }

  private writeU16BE(v: number): Buffer {
    const b = Buffer.alloc(2);
    b.writeUInt16BE(v & 0xffff, 0);
    return b;
  }

  private writeU32BE(v: number): Buffer {
    const b = Buffer.alloc(4);
    b.writeUInt32BE(v >>> 0, 0);
    return b;
  }

  private writeUtf8WithU16Len(buf: Buffer): Buffer {
    return Buffer.concat([this.writeU16BE(buf.length), buf]);
  }
}
