async function calculateGvlSnapshotSha256(rawJson) {
  const stableValue = normalizeGvlSnapshotValueForHash(rawJson);
  const data = new TextEncoder().encode(stableValue);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));

  return hashArray
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
}

async function buildGvlSnapshotRecord(rawJson, sourceUrl, fetchedAt) {
  const gvlJson = normalizeGvlSnapshotValueForMetadata(rawJson);

  return {
    sha256: await calculateGvlSnapshotSha256(rawJson),
    vendorListVersion: gvlJson?.vendorListVersion ?? null,
    gvlSpecificationVersion: gvlJson?.gvlSpecificationVersion ?? null,
    tcfPolicyVersion: gvlJson?.tcfPolicyVersion ?? null,
    fetchedAt: fetchedAt,
    sourceUrl: sourceUrl,
    rawJson: rawJson,
    vendorCount: countObjectEntries(gvlJson?.vendors),
    purposeCount: countObjectEntries(gvlJson?.purposes),
    // Existing GVL snapshots already use createdAt as the local mirror timestamp;
    // keep that field instead of duplicating it as recordedAt.
    createdAt: new Date().toISOString()
  };
}

function storeGvlSnapshotIfNew(db, snapshot) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(["gvl_snapshots"], "readwrite");
    const snapshotsStore = tx.objectStore("gvl_snapshots");
    const getRequest = snapshotsStore.get(snapshot.sha256);
    let result = null;

    getRequest.onerror = () => reject(getRequest.error);

    getRequest.onsuccess = () => {
      if (getRequest.result) {
        result = {
          stored: false,
          sha256: snapshot.sha256,
          vendorListVersion: snapshot.vendorListVersion
        };
        return;
      }

      snapshotsStore.add(snapshot);

      result = {
        stored: true,
        sha256: snapshot.sha256,
        vendorListVersion: snapshot.vendorListVersion
      };
    };

    tx.onerror = () => reject(tx.error);
    tx.oncomplete = () => resolve(result);
  });
}

function recordGvlSnapshotEvent(db, event) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(["gvl_snapshot_events"], "readwrite");
    const eventsStore = tx.objectStore("gvl_snapshot_events");
    const addRequest = eventsStore.add({
      eventType: event.eventType,
      capturedAt: event.capturedAt ?? new Date().toISOString(),
      vendorListVersion: event.vendorListVersion,
      sha256: event.sha256,
      sourceUrl: event.sourceUrl,
      diagnostics: event.diagnostics ?? null
    });
    let eventId = null;

    addRequest.onerror = () => reject(addRequest.error);
    addRequest.onsuccess = () => {
      eventId = addRequest.result;
    };
    tx.onerror = () => reject(tx.error);
    tx.oncomplete = () => resolve(eventId);
  });
}

async function ingestGvlSnapshot(db, rawJson, options = {}) {
  const snapshot = await buildGvlSnapshotRecord(
    rawJson,
    options.sourceUrl ?? null,
    options.fetchedAt ?? null
  );
  const storeResult = await storeGvlSnapshotIfNew(db, snapshot);
  const alreadyKnown = !storeResult.stored;

  await recordGvlSnapshotEvent(db, {
    eventType: alreadyKnown
      ? "gvl_snapshot_already_known"
      : "gvl_snapshot_ingested",
    capturedAt: new Date().toISOString(),
    vendorListVersion: snapshot.vendorListVersion,
    sha256: snapshot.sha256,
    sourceUrl: snapshot.sourceUrl,
    diagnostics: {
      ingestionSource: "manual_pipeline",
      ...(options.diagnostics ?? {}),
      alreadyKnown: alreadyKnown
    }
  });

  return {
    snapshot: snapshot,
    storeResult: storeResult,
    alreadyKnown: alreadyKnown
  };
}

function normalizeGvlSnapshotValueForHash(rawJson) {
  if (typeof rawJson !== "string") {
    return stableStringify(rawJson);
  }

  try {
    return stableStringify(JSON.parse(rawJson));
  } catch (error) {
    return rawJson;
  }
}

function normalizeGvlSnapshotValueForMetadata(rawJson) {
  if (typeof rawJson !== "string") {
    return rawJson;
  }

  try {
    return JSON.parse(rawJson);
  } catch (error) {
    return null;
  }
}

function countObjectEntries(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return 0;
  }

  return Object.keys(value).length;
}

globalThis.VendorGetGvlService = {
  calculateGvlSnapshotSha256,
  buildGvlSnapshotRecord,
  storeGvlSnapshotIfNew,
  recordGvlSnapshotEvent,
  ingestGvlSnapshot
};
