const decoder = new TextDecoder("utf-8", { fatal: false });

function toBytes(input) {
  if (input == null) return new Uint8Array(0);
  if (input instanceof Uint8Array) return input;
  if (input instanceof ArrayBuffer) return new Uint8Array(input);
  if (ArrayBuffer.isView(input)) {
    return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
  }
  if (typeof input === "string") return new Uint8Array(Buffer.from(input, "latin1"));
  throw new TypeError("decodeJetfuel expects a Buffer, ArrayBuffer, TypedArray or binary string");
}

function u16le(b, i) {
  return b[i] | (b[i + 1] << 8);
}

function reversedHex(b, i, n) {
  let out = "";
  for (let k = n - 1; k >= 0; k--) {
    out += b[i + k].toString(16).padStart(2, "0");
  }
  return out;
}

function looksLikeString(b, start, len, n) {
  if (len < 1 || start + len > n) return false;
  let hasAlpha = false;
  for (let k = start; k < start + len; k++) {
    const c = b[k];
    if (c < 0x20 || c >= 0x7f) return false;
    if ((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)) hasAlpha = true;
  }
  return hasAlpha;
}

export function decodeJetfuel(input) {
  const b = toBytes(input);
  const n = b.length;

  const frame =
    n >= 6
      ? {
          payloadLen: u16le(b, 0),
          count: b[5],
          footer: n >= 3 ? reversedHex(b, n - 3, 3).match(/../g).reverse().join("") : null,
        }
      : { payloadLen: null, count: null, footer: null };

  const tokens = [];
  const strings = [];
  const atoms = [];
  const lists = [];
  const nodes = [];

  let i = n > 6 ? 6 : 0;
  const end = n > 3 ? n - 3 : n;

  while (i < end) {
    const byte = b[i];

    // length-prefixed UTF-8 string: <len:u8> <bytes>
    if (byte >= 1 && byte <= 0xff && looksLikeString(b, i + 1, byte, n)) {
      const value = decoder.decode(b.subarray(i + 1, i + 1 + byte));
      const tok = { type: "string", value, offset: i };
      tokens.push(tok);
      strings.push(value);
      i += 1 + byte;
      continue;
    }

    // node intro: 0x11 then [typeString, keyString]
    if (byte === 0x11) {
      tokens.push({ type: "node", offset: i });
      nodes.push({ type: null, key: null, offset: i });
      i += 1;
      continue;
    }

    // atom ref: 02 <field:u8> 00 00 00 <hash:u32>
    if (byte === 0x02 && i + 9 <= n && b[i + 2] === 0 && b[i + 3] === 0 && b[i + 4] === 0) {
      const field = b[i + 1];
      const hash = reversedHex(b, i + 5, 4);
      tokens.push({ type: "atom", field, hash, offset: i });
      atoms.push({ field, hash });
      i += 9;
      continue;
    }

    // ref list: 03 <kind:u8> <count:u8> 00 00 00 00 <hash:u32 × count>
    if (byte === 0x03 && i + 7 <= n && b[i + 3] === 0 && b[i + 4] === 0 && b[i + 5] === 0 && b[i + 6] === 0) {
      const kind = b[i + 1];
      const count = b[i + 2];
      const hashes = [];
      let p = i + 7;
      for (let k = 0; k < count && p + 4 <= n; k++) {
        hashes.push(reversedHex(b, p, 4));
        p += 4;
      }
      tokens.push({ type: "list", kind, count, hashes, offset: i });
      lists.push({ kind, count, hashes });
      i = p;
      continue;
    }

    // unknown control bytes: collapse the run
    let j = i;
    while (
      j < end &&
      !(b[j] >= 1 && b[j] <= 0xff && looksLikeString(b, j + 1, b[j], n)) &&
      b[j] !== 0x11 &&
      !(b[j] === 0x02 && j + 9 <= n && b[j + 2] === 0 && b[j + 3] === 0 && b[j + 4] === 0) &&
      !(b[j] === 0x03 && j + 7 <= n && b[j + 3] === 0 && b[j + 4] === 0 && b[j + 5] === 0 && b[j + 6] === 0)
    ) {
      j++;
    }
    tokens.push({ type: "raw", hex: bytesToHex(b, i, j - i), offset: i });
    i = j > i ? j : i + 1;
  }

  for (let k = 0; k < tokens.length; k++) {
    if (tokens[k].type !== "node") continue;
    const node = nodes.find((nd) => nd.offset === tokens[k].offset);
    if (!node) continue;
    const following = [];
    for (let j = k + 1; j < tokens.length && following.length < 2; j++) {
      if (tokens[j].type === "node") break;
      if (tokens[j].type === "string") following.push(tokens[j].value);
    }
    [node.type = null, node.key = null] = following;
  }

  return {
    frame,
    tokens,
    strings,
    nodes,
    atoms,
    lists,
    timelineTokens: extractTimelineTokens(b),
  };
}

function bytesToHex(b, start, len) {
  let out = "";
  for (let k = 0; k < len; k++) out += b[start + k].toString(16).padStart(2, "0");
  return out;
}

export function extractTimelineTokens(input) {
  const b = toBytes(input);
  const text = Buffer.from(b).toString("latin1");
  const out = [];
  const seen = new Set();
  
  const re = /VGltZWxpbmU6[A-Za-z0-9+/]+={0,2}/g; // hard coded because why not
  let m;
  while ((m = re.exec(text))) {
    if (seen.has(m[0])) continue;
    try {
      const decoded = Buffer.from(m[0], "base64").toString("latin1");
      if (decoded.startsWith("Timeline:")) {
        seen.add(m[0]);
        out.push(m[0]);
      }
    } catch {}
  }
  return out;
}

const ENGAGEMENT_FIELD = 1;
const COUNTRY_FIELD = 3;
const PERIOD_FIELD = 6;

export function buildTimelineToken({ engagement = "Likes", country = "All", period = "Daily" } = {}) {
  const strField = (id, value) => {
    const valueBytes = Buffer.from(String(value), "utf8");
    const head = Buffer.alloc(7);
    head[0] = 0x0b; // thrift binary: STRING
    head.writeUInt16BE(id, 1);
    head.writeUInt32BE(valueBytes.length, 3);
    return Buffer.concat([head, valueBytes]);
  };

  const inner = Buffer.concat([
    strField(ENGAGEMENT_FIELD, engagement),
    strField(COUNTRY_FIELD, country),
    strField(PERIOD_FIELD, period),
    Buffer.from([0x00]),
  ]);

  const thrift = Buffer.concat([
    Buffer.from([0x0c, 0x00, 0xca]), // STRUCT, field id 202
    inner,
    Buffer.from([0x00]),
  ]);

  return Buffer.concat([Buffer.from("Timeline:", "utf8"), thrift]).toString("base64");
}

export function decodeTimelineToken(token) {
  const raw = Buffer.from(token, "base64");
  const prefix = Buffer.from("Timeline:", "utf8");
  if (!raw.subarray(0, prefix.length).equals(prefix)) {
    throw new Error("not a Timeline: token");
  }
  const b = raw.subarray(prefix.length);
  const fields = {};
  let i = 0;
  if (b[i] === 0x0c) i += 3; // skip outer struct header (STRUCT, id 202)
  while (i < b.length && b[i] !== 0x00) {
    const type = b[i];
    const id = b.readUInt16BE(i + 1);
    i += 3;
    if (type !== 0x0b) break; // only string fields expected
    const len = b.readUInt32BE(i);
    i += 4;
    fields[id] = b.subarray(i, i + len).toString("utf8");
    i += len;
  }
  return {
    engagement: fields[ENGAGEMENT_FIELD] ?? null,
    country: fields[COUNTRY_FIELD] ?? null,
    period: fields[PERIOD_FIELD] ?? null,
  };
}
