console.log("VG-Observe background loaded");

// VG-Observe module registration lives in src/modules/vg-observe/module.js.

const OFFICIAL_IAB_GVL_URL =
  "https://vendor-list.consensu.org/v3/vendor-list.json";
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
const GVL_AUTO_UPDATE_SOURCE = "extension_startup";
const AUTO_GVL_CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000;
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";

let isAutoGvlCheckRunning = false;
let lastAutoGvlCheckStartedAt = null;
let latestGvlUpdateStatus = null;

browser.runtime.onMessage.addListener((message, sender) =>
  handleVendorGetMessage(message, sender)
);

browser.webRequest.onBeforeRequest.addListener(
  handleObservedRequest,
  { urls: ["<all_urls>"] }
);

void runStartupGvlAutoUpdateCheck();

async function handleVendorGetMessage(message, sender) {
  if (!message) {
    return null;
  }

  if (message.type === "gvl_import_json") {
    return handleGvlImportJsonMessage(message);
  }

  if (message.type === "fetch_official_gvl") {
    return handleFetchOfficialGvlMessage();
  }

  if (message.type === "export_evidence_json") {
    return handleExportEvidenceJsonMessage();
  }

  if (message.type === "start_evidence_maintenance_session") {
    return startEvidenceMaintenanceSession(message?.payload?.source);
  }

  if (message.type === "refresh_evidence_maintenance_session") {
    return refreshEvidenceMaintenanceSession(message?.payload?.source);
  }

  if (message.type === "end_evidence_maintenance_session") {
    return endEvidenceMaintenanceSession(message?.payload?.source);
  }

  if (message.type === "get_evidence_maintenance_status") {
    return getEvidenceMaintenanceStatus();
  }

  if (message.type === "get_evidence_retention_status") {
    return handleGetEvidenceRetentionStatusMessage();
  }

  if (message.type === "get_latest_gvl_update_status") {
    return handleGetLatestGvlUpdateStatusMessage();
  }

  if (message.type === "list_gvl_snapshots") {
    return handleListGvlSnapshotsMessage();
  }

  if (message.type === "get_gvl_snapshot_summary") {
    return handleGetGvlSnapshotSummaryMessage(message);
  }

  if (message.type === "rebuild_gvl_snapshot_normalized_data") {
    return handleRebuildGvlSnapshotNormalizedDataMessage(message);
  }

  if (message.type === "list_gvl_vendors_for_snapshot") {
    return handleListGvlVendorsForSnapshotMessage(message);
  }

  if (message.type === "get_gvl_vendor_detail") {
    return handleGetGvlVendorDetailMessage(message);
  }

  if (message.type === "get_latest_consent_state") {
    return handleGetLatestConsentStateMessage();
  }

  if (message.type === "list_recent_consent_states") {
    return handleListRecentConsentStatesMessage();
  }

  if (message.type === "list_recent_observed_requests") {
    return handleListRecentObservedRequestsMessage();
  }

  if (message.type === "purge_unlocked_evidence_records") {
    return handlePurgeUnlockedEvidenceRecordsMessage();
  }

  if (message.type === "delete_all_evidence_database") {
    return handleDeleteAllEvidenceDatabaseMessage();
  }

  if (message.type === "lock_all_evidence_records") {
    return lockAllEvidenceRecords(
      message?.payload?.reason ?? "dsar_used",
      message?.payload?.note ?? null
    );
  }

  if (message.type === "unlock_all_evidence_records") {
    return unlockAllEvidenceRecords();
  }

  if (message.type !== "vendorget_capture") {
    return;
  }

  if (!(await isConsentCaptureEnabled())) {
    return;
  }

  const eventName = message?.payload?.eventName ?? null;
  const tabId = sender?.tab?.id ?? null;

  if (eventName === "tcf_ping") {
    const pingData = message?.payload?.payload?.data ?? null;

    if (tabId !== null && pingData) {
      rememberLatestTcfPing(tabId, pingData);
    }

    console.log("VG-Observe tcf ping", {
      payload: message.payload.payload,
      sender
    });

    return;
  }

  if (eventName !== "consent_capture") {
    console.log("VG-Observe ignored event", message);
    return;
  }

  if (isEvidenceWriteSuspended()) {
    console.info("VG-Observe evidence write skipped: maintenance mode");
    return;
  }

  const latestPingData = tabId !== null ? getLatestTcfPing(tabId) : null;

  const consentState = buildConsentStateV1(
    message.payload.payload,
    sender,
    latestPingData
  );

  consentState.stateFingerprint = await sha256Hex(
    stableStringify(consentState.fingerprintSource)
  );

  rememberLatestConsentState(consentState);

  const result = await persistConsentState(
    consentState,
    message.payload.payload?.rawTcData ?? null
  );

  console.log("VG-Observe consent state persisted", result);
}

async function handleGetEvidenceRetentionStatusMessage() {
  const db = await openVendorGetDb();
  const totalCount = await countEvidenceRecords(db);
  const lockedCount = await countLockedEvidenceRecords(db);
  const storeCounts = await getEvidenceStoreCounts(db);

  return {
    success: true,
    totalCount,
    lockedCount,
    unlockedCount: totalCount - lockedCount,
    storeCounts
  };
}

async function handleExportEvidenceJsonMessage() {
  return {
    success: true,
    export: await exportVendorGetEvidenceJson()
  };
}

async function handleGetLatestGvlUpdateStatusMessage() {
  const db = await openVendorGetDb();
  const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);

  if (latestGvlUpdateStatus) {
    return {
      success: true,
      status: {
        ...latestGvlUpdateStatus,
        latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
        latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
        latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null
      }
    };
  }

  const throttleState = await getAutoGvlCheckThrottleState();
  const throttleDecision = shouldThrottleAutoGvlCheck(
    throttleState?.lastAutoGvlCheckAt ?? null
  );

  return {
    success: true,
    status: {
      checkedAt: null,
      previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
      currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
      latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
      latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
      lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
      nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
      result: "not_checked_since_background_start",
      message: "Noch kein automatischer GVL-Update-Check seit Background-Start."
    }
  };
}

async function handleListGvlSnapshotsMessage() {
  const db = await openVendorGetDb();
  const snapshots = await listRecentGvlSnapshots(db, 25);
  const snapshotsWithEvents = await Promise.all(
    snapshots.map(async (snapshot) => {
      const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);

      return {
        vendorListVersion: snapshot.vendorListVersion ?? null,
        sha256: snapshot.sha256 ?? null,
        fetchedAt: snapshot.fetchedAt ?? null,
        sourceUrl: snapshot.sourceUrl ?? null,
        eventType: event?.eventType ?? null,
        eventCapturedAt: event?.capturedAt ?? null
      };
    })
  );

  return {
    success: true,
    gvlSnapshots: snapshotsWithEvents
  };
}

async function handleGetGvlSnapshotSummaryMessage(message) {
  const db = await openVendorGetDb();
  const payload = message?.payload ?? {};
  const snapshot = await getGvlSnapshotByIdentifier(db, {
    sha256: payload.sha256 ?? null,
    vendorListVersion: payload.vendorListVersion ?? null
  });

  if (!snapshot) {
    return {
      success: false,
      error: "gvl_snapshot_not_found"
    };
  }

  const vendorListVersion = snapshot.vendorListVersion ?? null;
  const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);
  const counts = await countGvlNormalizedRecordsForVersion(
    db,
    vendorListVersion
  );

  return {
    success: true,
    summary: {
      vendorListVersion,
      sha256: snapshot.sha256 ?? null,
      fetchedAt: snapshot.fetchedAt ?? null,
      sourceUrl: snapshot.sourceUrl ?? null,
      eventType: event?.eventType ?? null,
      eventCapturedAt: event?.capturedAt ?? null,
      vendorCount: snapshot.vendorCount ?? counts.vendorCount,
      snapshotVendorCount: snapshot.vendorCount ?? null,
      normalizedVendorCount: counts.vendorCount,
      purposeCount: snapshot.purposeCount ?? counts.purposeCount,
      specialPurposeCount: counts.specialPurposeCount,
      featureCount: counts.featureCount,
      specialFeatureCount: counts.specialFeatureCount,
      dataCategoryCount: counts.dataCategoryCount,
      vendorRelationshipCount: counts.vendorRelationshipCount,
      normalizedVendorRelationshipCount: counts.vendorRelationshipCount,
      technicalFields: {
        snapshotStore: "gvl_snapshots",
        vendorListVersion: "vendorListVersion",
        sha256: "sha256",
        fetchedAt: "fetchedAt",
        sourceUrl: "sourceUrl"
      },
      diagnostics: {
        eventDiagnostics: event?.diagnostics ?? null
      }
    }
  };
}

async function handleRebuildGvlSnapshotNormalizedDataMessage(message) {
  const db = await openVendorGetDb();
  const payload = message?.payload ?? {};
  const snapshot = await getGvlSnapshotByIdentifier(db, {
    sha256: payload.sha256 ?? null,
    vendorListVersion: payload.vendorListVersion ?? null
  });

  if (!snapshot) {
    return {
      success: false,
      error: "gvl_snapshot_not_found"
    };
  }

  const normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
    snapshot
  );
  const counts = await countGvlNormalizedRecordsForVersion(
    db,
    snapshot.vendorListVersion ?? null
  );

  return {
    success: true,
    snapshotSha256: snapshot.sha256 ?? null,
    vendorListVersion: snapshot.vendorListVersion ?? null,
    normalizationSummary,
    counts
  };
}

async function handleListGvlVendorsForSnapshotMessage(message) {
  const db = await openVendorGetDb();
  const payload = message?.payload ?? {};
  const snapshot = await getGvlSnapshotByIdentifier(db, {
    sha256: payload.sha256 ?? null,
    vendorListVersion: payload.vendorListVersion ?? null
  });

  if (!snapshot) {
    return {
      success: false,
      error: "gvl_snapshot_not_found"
    };
  }

  const vendors = await listGvlVendorsForSnapshot(db, snapshot);

  return {
    success: true,
    snapshotSha256: snapshot.sha256 ?? null,
    vendorListVersion: snapshot.vendorListVersion ?? null,
    vendors
  };
}

function listGvlVendorsForSnapshot(db, snapshot) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
    const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
    const vendorListVersionIndex = vendorsStore.index("vendorListVersion");
    const cursorRequest = vendorListVersionIndex.openCursor(
      IDBKeyRange.only(snapshot.vendorListVersion)
    );
    const vendors = [];

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (!cursor) {
        return;
      }

      if (cursor.value?.snapshotSha256 === snapshot.sha256) {
        vendors.push({
          vendorId: cursor.value.vendorId ?? null,
          name: cursor.value.name ?? null,
          deletedDate: cursor.value.deletedDate ?? null,
          snapshotSha256: cursor.value.snapshotSha256 ?? null
        });
      }

      cursor.continue();
    };

    tx.onerror = () => reject(tx.error);
    tx.onabort = () => reject(tx.error);
    tx.oncomplete = () => {
      resolve(
        vendors.sort((left, right) => {
          return (
            toComparableNumber(left.vendorId) -
            toComparableNumber(right.vendorId)
          );
        })
      );
    };
  });
}

async function handleGetGvlVendorDetailMessage(message) {
  const db = await openVendorGetDb();
  const vendorId = parseGvlVendorDetailId(message?.payload?.vendorId);

  if (vendorId === null) {
    return {
      success: false,
      error: "invalid_vendor_id"
    };
  }

  const vendorRecord = await getLatestGvlVendorByVendorId(db, vendorId);

  if (!vendorRecord) {
    return {
      success: false,
      error: "gvl_vendor_not_found"
    };
  }

  const snapshotSha256 = vendorRecord.snapshotSha256 ?? null;
  const snapshot = snapshotSha256
    ? await getGvlSnapshotBySha256(db, snapshotSha256)
    : null;
  const rawGvlSha256 = snapshot?.rawGvlSha256 ?? null;
  const rawEvidence = rawGvlSha256
    ? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
    : null;

  return {
    success: true,
    vendorDetail: {
      vendor: vendorRecord,
      snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
      rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
        rawEvidence,
        rawGvlSha256
      )
    }
  };
}

function parseGvlVendorDetailId(value) {
  const vendorId = Number(value);

  if (!Number.isInteger(vendorId) || vendorId <= 0) {
    return null;
  }

  return vendorId;
}

function getLatestGvlVendorByVendorId(db, vendorId) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
    const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
    const vendorIdIndex = vendorsStore.index("vendorId");
    const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
    const vendors = [];

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (!cursor) {
        return;
      }

      vendors.push(cursor.value);
      cursor.continue();
    };

    tx.onerror = () => reject(tx.error);
    tx.onabort = () => reject(tx.error);
    tx.oncomplete = () => {
      resolve(sortGvlVendorRecordsNewestFirst(vendors)[0] ?? null);
    };
  });
}

function sortGvlVendorRecordsNewestFirst(vendorRecords) {
  return vendorRecords.slice().sort((left, right) => {
    const fetchedAtComparison =
      toComparableTime(right.snapshotFetchedAt) -
      toComparableTime(left.snapshotFetchedAt);

    if (fetchedAtComparison !== 0) {
      return fetchedAtComparison;
    }

    return (
      toComparableNumber(right.vendorListVersion) -
      toComparableNumber(left.vendorListVersion)
    );
  });
}

function getGvlRawEvidenceBySha256(db, rawGvlSha256) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlRawEvidence], "readonly");
    const rawEvidenceStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlRawEvidence);
    const getRequest = rawEvidenceStore.get(rawGvlSha256);
    let rawEvidenceOrNull = null;

    getRequest.onerror = () => reject(getRequest.error);
    getRequest.onsuccess = () => {
      rawEvidenceOrNull = getRequest.result ?? null;
    };

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

function buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256) {
  if (!snapshot) {
    return {
      snapshotSha256: snapshotSha256 ?? null,
      rawGvlSha256: null,
      vendorListVersion: null,
      tcfPolicyVersion: null,
      fetchedAt: null,
      createdAt: null
    };
  }

  return {
    snapshotSha256: snapshot.sha256 ?? snapshotSha256 ?? null,
    rawGvlSha256: snapshot.rawGvlSha256 ?? null,
    vendorListVersion: snapshot.vendorListVersion ?? null,
    tcfPolicyVersion: snapshot.tcfPolicyVersion ?? null,
    fetchedAt: snapshot.fetchedAt ?? null,
    createdAt: snapshot.createdAt ?? null
  };
}

function buildGvlVendorDetailRawEvidenceSummary(rawEvidence, rawGvlSha256) {
  if (!rawEvidence) {
    return {
      rawGvlSha256: rawGvlSha256 ?? null,
      sourceUrl: null,
      fetchedAt: null,
      httpStatus: null,
      contentType: null,
      hasRawBody: false
    };
  }

  return {
    rawGvlSha256: rawEvidence.rawGvlSha256 ?? rawGvlSha256 ?? null,
    sourceUrl: rawEvidence.sourceUrl ?? null,
    fetchedAt: rawEvidence.fetchedAt ?? null,
    httpStatus: rawEvidence.httpStatus ?? null,
    contentType: rawEvidence.contentType ?? null,
    hasRawBody: typeof rawEvidence.rawBody === "string"
  };
}

async function handleGetLatestConsentStateMessage() {
  const db = await openVendorGetDb();
  const latestStateOrNull = await getLatestConsentState(db);

  return {
    success: true,
    consentState: latestStateOrNull
  };
}

async function handleListRecentConsentStatesMessage() {
  const db = await openVendorGetDb();
  const consentStates = await listRecentConsentStates(db, 25);

  return {
    success: true,
    consentStates
  };
}

async function handleListRecentObservedRequestsMessage() {
  const db = await openVendorGetDb();
  const observedRequests = await listRecentObservedRequests(db, 50);

  return {
    success: true,
    observedRequests
  };
}

function getLatestConsentState(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(["consent_states"], "readonly");
    const statesStore = tx.objectStore("consent_states");
    const lastSeenAtIndex = statesStore.index("lastSeenAt");
    const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
    let latestStateOrNull = null;

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (cursor) {
        latestStateOrNull = cursor.value;
      }
    };

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

function listRecentConsentStates(db, limit) {
  return new Promise((resolve, reject) => {
    const consentStates = [];
    const tx = db.transaction(["consent_states"], "readonly");
    const statesStore = tx.objectStore("consent_states");
    const lastSeenAtIndex = statesStore.index("lastSeenAt");
    const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (!cursor || consentStates.length >= limit) {
        return;
      }

      consentStates.push(cursor.value);

      if (consentStates.length < limit) {
        cursor.continue();
      }
    };

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

function listRecentObservedRequests(db, limit) {
  return new Promise((resolve, reject) => {
    const observedRequests = [];
    const tx = db.transaction(
      [VENDORGET_STORE_NAMES.observedRequests],
      "readonly"
    );
    const requestsStore = tx.objectStore(VENDORGET_STORE_NAMES.observedRequests);
    const lastSeenAtIndex = requestsStore.index("lastSeenAt");
    const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (!cursor || observedRequests.length >= limit) {
        return;
      }

      observedRequests.push(cursor.value);

      if (observedRequests.length < limit) {
        cursor.continue();
      }
    };

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

function listRecentGvlSnapshots(db, limit) {
  return new Promise((resolve, reject) => {
    const snapshots = [];
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
    const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
    const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
    const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (!cursor || snapshots.length >= limit) {
        return;
      }

      snapshots.push(cursor.value);

      if (snapshots.length < limit) {
        cursor.continue();
      }
    };

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

function getAnyGvlSnapshotEventBySha256(db, sha256) {
  if (!sha256) {
    return Promise.resolve(null);
  }

  return new Promise((resolve, reject) => {
    const tx = db.transaction(
      [VENDORGET_STORE_NAMES.gvlSnapshotEvents],
      "readonly"
    );
    const eventsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshotEvents);
    const sha256Index = eventsStore.index("sha256");
    const getRequest = sha256Index.get(sha256);
    let eventOrNull = null;

    getRequest.onerror = () => reject(getRequest.error);
    getRequest.onsuccess = () => {
      eventOrNull = getRequest.result ?? null;
    };

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

async function getGvlSnapshotByIdentifier(db, { sha256, vendorListVersion }) {
  if (sha256) {
    return getGvlSnapshotBySha256(db, sha256);
  }

  if (vendorListVersion !== null && vendorListVersion !== undefined) {
    return getGvlSnapshotByVendorListVersion(db, vendorListVersion);
  }

  return null;
}

function getGvlSnapshotBySha256(db, sha256) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
    const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
    const getRequest = snapshotsStore.get(sha256);
    let snapshotOrNull = null;

    getRequest.onerror = () => reject(getRequest.error);
    getRequest.onsuccess = () => {
      snapshotOrNull = getRequest.result ?? null;
    };

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

function getGvlSnapshotByVendorListVersion(db, vendorListVersion) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
    const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
    const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
    const getRequest = vendorListVersionIndex.get(vendorListVersion);
    let snapshotOrNull = null;

    getRequest.onerror = () => reject(getRequest.error);
    getRequest.onsuccess = () => {
      snapshotOrNull = getRequest.result ?? null;
    };

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

function countGvlNormalizedRecordsForVersion(db, vendorListVersion) {
  if (vendorListVersion === null || vendorListVersion === undefined) {
    return Promise.resolve({
      vendorCount: 0,
      purposeCount: 0,
      specialPurposeCount: 0,
      featureCount: 0,
      specialFeatureCount: 0,
      dataCategoryCount: 0,
      vendorRelationshipCount: 0
    });
  }

  const countDefinitions = [
    ["vendorCount", VENDORGET_STORE_NAMES.gvlVendors],
    ["purposeCount", VENDORGET_STORE_NAMES.gvlPurposes],
    ["specialPurposeCount", VENDORGET_STORE_NAMES.gvlSpecialPurposes],
    ["featureCount", VENDORGET_STORE_NAMES.gvlFeatures],
    ["specialFeatureCount", VENDORGET_STORE_NAMES.gvlSpecialFeatures],
    ["dataCategoryCount", VENDORGET_STORE_NAMES.gvlDataCategories],
    [
      "vendorRelationshipCount",
      VENDORGET_STORE_NAMES.gvlVendorRelationships
    ]
  ];

  return new Promise((resolve, reject) => {
    const counts = {};
    const tx = db.transaction(
      countDefinitions.map((definition) => definition[1]),
      "readonly"
    );

    tx.onerror = () => reject(tx.error);
    tx.onabort = () => reject(tx.error);
    tx.oncomplete = () => resolve(counts);

    for (const [countName, storeName] of countDefinitions) {
      const store = tx.objectStore(storeName);
      const vendorListVersionIndex = store.index("vendorListVersion");
      const countRequest = vendorListVersionIndex.count(
        IDBKeyRange.only(vendorListVersion)
      );

      countRequest.onsuccess = () => {
        counts[countName] = countRequest.result;
      };
    }
  });
}

async function handlePurgeUnlockedEvidenceRecordsMessage() {
  const db = await openVendorGetDb();

  return purgeUnlockedEvidenceRecords(db);
}

function handleDeleteAllEvidenceDatabaseMessage() {
  return deleteVendorGetDatabase();
}

async function handleGvlImportJsonMessage(message) {
  const rawJson = message?.payload?.rawJson ?? null;

  if (!isGvlImportCandidate(rawJson)) {
    return {
      success: false,
      error: "invalid_gvl_json"
    };
  }

  const db = await openVendorGetDb();
  const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
    sourceUrl: message?.payload?.sourceUrl ?? null,
    diagnostics: {
      importSource: "local_file"
    }
  });

  return {
    success: true,
    alreadyKnown: result.alreadyKnown,
    vendorListVersion: result.snapshot.vendorListVersion,
    sha256: result.snapshot.sha256
  };
}

function isGvlImportCandidate(value) {
  return (
    value &&
    typeof value === "object" &&
    !Array.isArray(value) &&
    value.vendorListVersion !== undefined &&
    value.vendors &&
    typeof value.vendors === "object" &&
    !Array.isArray(value.vendors)
  );
}

async function handleFetchOfficialGvlMessage() {
  try {
    const { rawJson, rawGvlSha256, responseStatus } =
      await fetchOfficialGvlJson();

    if (!isGvlImportCandidate(rawJson)) {
      return {
        success: false,
        error: "invalid_gvl_json",
        responseStatus: responseStatus
      };
    }

    const db = await openVendorGetDb();
    const currentVendorListVersion = rawJson.vendorListVersion ?? null;
    const existingSnapshot = await getGvlSnapshotByVendorListVersion(
      db,
      currentVendorListVersion
    );
    const ingestResult = existingSnapshot
      ? null
      : await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
          sourceUrl: OFFICIAL_IAB_GVL_URL,
          fetchedAt: new Date().toISOString(),
          rawGvlSha256: rawGvlSha256,
          diagnostics: {
            ingestionSource: "official_iab_fetch",
            responseStatus: responseStatus
          }
        });
    const snapshot = existingSnapshot ?? ingestResult.snapshot;
    const completeness = await getGvlSnapshotNormalizedCompleteness(
      db,
      snapshot
    );
    let normalizationSummary = null;
    let syncStatus = "current_and_locally_available";

    if (!existingSnapshot) {
      normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
        snapshot
      );
      syncStatus = "new_gvl_revision_stored_and_normalized";
    } else if (!completeness.complete) {
      normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
        existingSnapshot
      );
      syncStatus = "known_gvl_rebuilt_from_local_evidence";
    }

    const counts = await countGvlNormalizedRecordsForVersion(
      db,
      snapshot.vendorListVersion ?? null
    );

    return {
      success: true,
      alreadyKnown: Boolean(existingSnapshot),
      syncStatus,
      vendorListVersion: snapshot.vendorListVersion,
      sha256: snapshot.sha256,
      normalizationSummary,
      counts
    };
  } catch (error) {
    console.warn("VG-Observe official GVL fetch failed", error);

    return {
      success: false,
      error: "official_gvl_fetch_failed"
    };
  }
}

async function getGvlSnapshotNormalizedCompleteness(db, snapshot) {
  const counts = await countGvlNormalizedRecordsForVersion(
    db,
    snapshot?.vendorListVersion ?? null
  );
  const expectedVendorCount = Number(snapshot?.vendorCount ?? 0);
  const hasVendors =
    expectedVendorCount > 0
      ? counts.vendorCount >= expectedVendorCount
      : counts.vendorCount > 0;
  const hasVendorRelationships = counts.vendorRelationshipCount > 0;

  return {
    complete: hasVendors && hasVendorRelationships,
    counts
  };
}

async function fetchOfficialGvlJson() {
  const response = await fetch(OFFICIAL_IAB_GVL_URL, {
    method: "GET",
    cache: "no-store",
    headers: {
      "Cache-Control": "no-cache",
      Pragma: "no-cache"
    }
  });

  if (!response.ok) {
    const error = new Error("official_gvl_fetch_failed");

    error.responseStatus = response.status;
    throw error;
  }

  const rawBody = await response.text();
  const fetchedAt = new Date().toISOString();
  const contentType = response.headers.get("Content-Type");
  const rawGvlSha256 = await VendorGetGvlService.calculateRawGvlSha256(rawBody);
  const db = await openVendorGetDb();

  await VendorGetGvlService.storeGvlRawEvidenceIfNew(db, {
    rawGvlSha256,
    sourceUrl: OFFICIAL_IAB_GVL_URL,
    fetchedAt,
    httpStatus: response.status,
    contentType,
    rawBody
  });

  return {
    rawJson: JSON.parse(rawBody),
    rawGvlSha256: rawGvlSha256,
    responseStatus: response.status
  };
}

async function runStartupGvlAutoUpdateCheck() {
  if (isAutoGvlCheckRunning) {
    return;
  }

  isAutoGvlCheckRunning = true;

  const throttleState = await getAutoGvlCheckThrottleState();
  const throttleDecision = shouldThrottleAutoGvlCheck(
    throttleState?.lastAutoGvlCheckAt ?? null
  );

  if (throttleDecision.throttled) {
    await handleThrottledStartupGvlAutoUpdateCheck(
      throttleState,
      throttleDecision
    );
    isAutoGvlCheckRunning = false;
    return;
  }

  lastAutoGvlCheckStartedAt = new Date().toISOString();
  await storeAutoGvlCheckThrottleState({
    lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
    lastAutoGvlCheckResult: "started"
  });

  const startedStatus = {
    checkedAt: lastAutoGvlCheckStartedAt,
    previousVendorListVersion: null,
    currentVendorListVersion: null,
    previousSnapshotSha256: null,
    currentSnapshotSha256: null,
    latestLocalVendorListVersion: null,
    latestLocalSnapshotSha256: null,
    latestLocalFetchedAt: null,
    lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
    nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
      lastAutoGvlCheckStartedAt
    ),
    result: "started",
    message: "Automatischer GVL-Update-Check gestartet."
  };

  latestGvlUpdateStatus = startedStatus;

  try {
    const db = await openVendorGetDb();
    const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
    const previousVendorListVersion =
      previousSnapshot?.vendorListVersion ?? null;
    const previousSnapshotSha256 = previousSnapshot?.sha256 ?? null;
    const previousFetchedAt = previousSnapshot?.fetchedAt ?? null;

    latestGvlUpdateStatus = {
      checkedAt: lastAutoGvlCheckStartedAt,
      previousVendorListVersion,
      currentVendorListVersion: null,
      previousSnapshotSha256,
      currentSnapshotSha256: null,
      latestLocalVendorListVersion: previousVendorListVersion,
      latestLocalSnapshotSha256: previousSnapshotSha256,
      latestLocalFetchedAt: previousFetchedAt,
      lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
      nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
        lastAutoGvlCheckStartedAt
      ),
      result: "started",
      message: "Automatischer GVL-Update-Check gestartet."
    };

    await recordGvlAutoUpdateEvent(db, {
      eventType: "gvl_auto_update_check_started",
      checkedAt: lastAutoGvlCheckStartedAt,
      previousSnapshot,
      currentVendorListVersion: null,
      currentSnapshotSha256: null,
      result: "started"
    });

    const { rawJson, rawGvlSha256, responseStatus } =
      await fetchOfficialGvlJson();

    if (!isGvlImportCandidate(rawJson)) {
      throw new Error("invalid_gvl_json");
    }

    const currentVendorListVersion = rawJson.vendorListVersion ?? null;
    const currentSnapshotSha256 =
      await VendorGetGvlService.calculateGvlSnapshotSha256(rawJson);
    const newVersionDetected = isNewerGvlVendorListVersion(
      currentVendorListVersion,
      previousVendorListVersion
    );

    const ingestResult = await VendorGetGvlService.ingestGvlSnapshot(
      db,
      rawJson,
      {
        sourceUrl: OFFICIAL_IAB_GVL_URL,
        fetchedAt: new Date().toISOString(),
        rawGvlSha256: rawGvlSha256,
        diagnostics: {
          ingestionSource: "official_iab_auto_update",
          responseStatus: responseStatus,
          updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
          checkedAt: lastAutoGvlCheckStartedAt,
          previousVendorListVersion: previousVendorListVersion,
          previousSnapshotSha256: previousSnapshotSha256,
          previousFetchedAt: previousFetchedAt,
          currentVendorListVersion: currentVendorListVersion,
          currentSnapshotSha256: currentSnapshotSha256,
          result: newVersionDetected ? "new_version_detected" : "no_change"
        }
      }
    );

    let normalizationSummary = null;

    if (!ingestResult.alreadyKnown && newVersionDetected) {
      normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
        ingestResult.snapshot
      );
    }

    const result = buildGvlAutoUpdateResult({
      newVersionDetected,
      alreadyKnown: ingestResult.alreadyKnown
    });

    const status = {
      checkedAt: lastAutoGvlCheckStartedAt,
      previousVendorListVersion,
      currentVendorListVersion,
      previousSnapshotSha256,
      currentSnapshotSha256,
      latestLocalVendorListVersion: ingestResult.snapshot.vendorListVersion,
      latestLocalSnapshotSha256: ingestResult.snapshot.sha256,
      latestLocalFetchedAt: ingestResult.snapshot.fetchedAt ?? null,
      lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
      nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
        lastAutoGvlCheckStartedAt
      ),
      result,
      message: buildGvlAutoUpdateMessage(result),
      normalizationSummary
    };

    latestGvlUpdateStatus = status;
    await storeAutoGvlCheckThrottleState({
      lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
      lastAutoGvlCheckResult: result
    });

    await recordGvlAutoUpdateEvent(db, {
      eventType: buildGvlAutoUpdateEventType(result),
      checkedAt: lastAutoGvlCheckStartedAt,
      previousSnapshot,
      currentVendorListVersion,
      currentSnapshotSha256,
      currentSnapshot: ingestResult.snapshot,
      result,
      normalizationSummary
    });
  } catch (error) {
    console.warn("VG-Observe automatic official GVL update check failed", error);

    const checkedAt = lastAutoGvlCheckStartedAt ?? new Date().toISOString();
    const errorMessage = error?.message ?? String(error);

    latestGvlUpdateStatus = {
      checkedAt,
      previousVendorListVersion: latestGvlUpdateStatus?.previousVendorListVersion ?? null,
      currentVendorListVersion: latestGvlUpdateStatus?.currentVendorListVersion ?? null,
      previousSnapshotSha256: latestGvlUpdateStatus?.previousSnapshotSha256 ?? null,
      currentSnapshotSha256: latestGvlUpdateStatus?.currentSnapshotSha256 ?? null,
      latestLocalVendorListVersion:
        latestGvlUpdateStatus?.latestLocalVendorListVersion ?? null,
      latestLocalSnapshotSha256:
        latestGvlUpdateStatus?.latestLocalSnapshotSha256 ?? null,
      latestLocalFetchedAt: latestGvlUpdateStatus?.latestLocalFetchedAt ?? null,
      lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
      nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
        lastAutoGvlCheckStartedAt
      ),
      result: "error",
      message: "Auto-Check fehlgeschlagen",
      error: errorMessage
    };
    await storeAutoGvlCheckThrottleState({
      lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
      lastAutoGvlCheckResult: "error"
    });

    try {
      const db = await openVendorGetDb();
      const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);

      await recordGvlAutoUpdateEvent(db, {
        eventType: "gvl_auto_update_error",
        checkedAt,
        previousSnapshot,
        currentVendorListVersion: null,
        currentSnapshotSha256: null,
        result: "error",
        error: errorMessage
      });
    } catch (eventError) {
      console.warn("VG-Observe automatic GVL error event failed", eventError);
    }
  } finally {
    isAutoGvlCheckRunning = false;
  }
}

async function handleThrottledStartupGvlAutoUpdateCheck(
  throttleState,
  throttleDecision
) {
  const checkedAt = new Date().toISOString();

  try {
    const db = await openVendorGetDb();
    const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);

    latestGvlUpdateStatus = {
      checkedAt,
      previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
      currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
      latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
      latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
      lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
      nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
      result: "throttled",
      message: "Auto-Check wegen 24h-Throttling übersprungen."
    };

    await recordGvlAutoUpdateEvent(db, {
      eventType: "gvl_auto_update_throttled",
      checkedAt,
      previousSnapshot: latestSnapshot,
      currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
      currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
      result: "throttled",
      throttleDiagnostics: {
        lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
        throttleMs: AUTO_GVL_CHECK_THROTTLE_MS,
        nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt
      }
    });
  } catch (error) {
    console.warn("VG-Observe automatic GVL throttled event failed", error);

    latestGvlUpdateStatus = {
      checkedAt,
      previousVendorListVersion: null,
      currentVendorListVersion: null,
      previousSnapshotSha256: null,
      currentSnapshotSha256: null,
      latestLocalVendorListVersion: null,
      latestLocalSnapshotSha256: null,
      latestLocalFetchedAt: null,
      lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
      nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
      result: "throttled",
      message:
        "Auto-Check wegen 24h-Throttling übersprungen; Status-Event konnte nicht geschrieben werden."
    };
  }
}

async function getAutoGvlCheckThrottleState() {
  try {
    const storedValue = await browser.storage.local.get(
      AUTO_GVL_CHECK_STORAGE_KEY
    );

    return storedValue[AUTO_GVL_CHECK_STORAGE_KEY] ?? {};
  } catch (error) {
    console.warn("VG-Observe automatic GVL throttle state unavailable", error);
    return {};
  }
}

async function storeAutoGvlCheckThrottleState(state) {
  try {
    await browser.storage.local.set({
      [AUTO_GVL_CHECK_STORAGE_KEY]: state
    });
  } catch (error) {
    console.warn("VG-Observe automatic GVL throttle state write failed", error);
  }
}

function shouldThrottleAutoGvlCheck(lastAutoGvlCheckAt) {
  if (!lastAutoGvlCheckAt) {
    return {
      throttled: false,
      nextAllowedAutoCheckAt: null
    };
  }

  const lastCheckTime = Date.parse(lastAutoGvlCheckAt);

  if (Number.isNaN(lastCheckTime)) {
    return {
      throttled: false,
      nextAllowedAutoCheckAt: null
    };
  }

  const nextAllowedTime = lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS;
  const now = Date.now();

  return {
    throttled: now < nextAllowedTime,
    nextAllowedAutoCheckAt: new Date(nextAllowedTime).toISOString()
  };
}

function getNextAllowedAutoGvlCheckAt(lastAutoGvlCheckAt) {
  if (!lastAutoGvlCheckAt) {
    return null;
  }

  const lastCheckTime = Date.parse(lastAutoGvlCheckAt);

  if (Number.isNaN(lastCheckTime)) {
    return null;
  }

  return new Date(lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS).toISOString();
}

function getLatestGvlSnapshotByVendorListVersion(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
    const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
    const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
    const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");
    let latestSnapshotOrNull = null;

    cursorRequest.onerror = () => reject(cursorRequest.error);
    cursorRequest.onsuccess = () => {
      const cursor = cursorRequest.result;

      if (cursor) {
        latestSnapshotOrNull = cursor.value;
      }
    };

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

function isNewerGvlVendorListVersion(currentVersion, previousVersion) {
  if (currentVersion === null || currentVersion === undefined) {
    return false;
  }

  if (previousVersion === null || previousVersion === undefined) {
    return true;
  }

  return Number(currentVersion) > Number(previousVersion);
}

async function normalizeGvlSnapshotWithExistingPipeline(snapshot) {
  return {
    vendors: await normalizeGvlVendorsFromSnapshot(snapshot),
    catalogs: await normalizeGvlCatalogsFromSnapshot(snapshot),
    vendorRelationships:
      await normalizeGvlVendorRelationshipsFromSnapshot(snapshot)
  };
}

function buildGvlAutoUpdateResult({ newVersionDetected, alreadyKnown }) {
  if (newVersionDetected && !alreadyKnown) {
    return "stored";
  }

  if (newVersionDetected && alreadyKnown) {
    return "already_known";
  }

  if (!newVersionDetected && !alreadyKnown) {
    return "stored";
  }

  return "no_change";
}

function buildGvlAutoUpdateEventType(result) {
  if (result === "stored") {
    return "gvl_auto_update_stored";
  }

  if (result === "already_known") {
    return "gvl_auto_update_detected";
  }

  return "gvl_auto_update_no_change";
}

function buildGvlAutoUpdateMessage(result) {
  if (result === "stored") {
    return "Neue offizielle IAB-Europe-Vendorliste gespeichert.";
  }

  if (result === "already_known") {
    return "Offizielle IAB-Europe-Vendorliste war bereits lokal bekannt.";
  }

  return "Keine neuere offizielle IAB-Europe-Vendorliste gefunden.";
}

function recordGvlAutoUpdateEvent(
  db,
  {
    eventType,
    checkedAt,
    previousSnapshot,
    currentVendorListVersion,
    currentSnapshotSha256,
    currentSnapshot,
    result,
    normalizationSummary,
    error,
    throttleDiagnostics
  }
) {
  return VendorGetGvlService.recordGvlSnapshotEvent(db, {
    eventType,
    capturedAt: checkedAt,
    vendorListVersion:
      currentVendorListVersion ?? previousSnapshot?.vendorListVersion ?? null,
    sha256: currentSnapshotSha256 ?? previousSnapshot?.sha256 ?? null,
    sourceUrl: OFFICIAL_IAB_GVL_URL,
    diagnostics: {
      updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
      checkedAt,
      previousVendorListVersion: previousSnapshot?.vendorListVersion ?? null,
      currentVendorListVersion: currentVendorListVersion ?? null,
      previousSnapshotSha256: previousSnapshot?.sha256 ?? null,
      currentSnapshotSha256: currentSnapshotSha256 ?? null,
      previousFetchedAt: previousSnapshot?.fetchedAt ?? null,
      currentFetchedAt: currentSnapshot?.fetchedAt ?? null,
      result,
      vendorCountBefore: previousSnapshot?.vendorCount ?? null,
      vendorCountAfter: currentSnapshot?.vendorCount ?? null,
      purposeCountBefore: previousSnapshot?.purposeCount ?? null,
      purposeCountAfter: currentSnapshot?.purposeCount ?? null,
      normalizationSummary: normalizationSummary ?? null,
      error: error ?? null,
      ...(throttleDiagnostics ?? {})
    }
  });
}

function buildConsentStateV1(rawCapture, sender, latestPingData) {
  const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);

  // Firefox/CMP/browser storage remains the consent source of truth; VG-Observe
  // stores an evidentiary mirror of the observed TCF state.
  const state = {
    schemaVersion: 1,
    capturedAt: new Date().toISOString(),

    page: {
      url: rawCapture?.url ?? sender?.tab?.url ?? sender?.url ?? null,
      origin: rawCapture?.origin ?? sender?.origin ?? null,
      tabId: sender?.tab?.id ?? null,
      frameId: sender?.frameId ?? null,
      incognito: sender?.tab?.incognito ?? null,
      cookieStoreId: sender?.tab?.cookieStoreId ?? null
    },

    cmp: {
      cmpId:
        rawCapture?.cmpId ??
        decodedTcString?.cmpId ??
        latestPingData?.cmpId ??
        null,

      cmpVersion:
        rawCapture?.cmpVersion ??
        decodedTcString?.cmpVersion ??
        latestPingData?.cmpVersion ??
        null,

      tcfPolicyVersion:
        rawCapture?.tcfPolicyVersion ??
        decodedTcString?.tcfPolicyVersion ??
        latestPingData?.tcfPolicyVersion ??
        null,

      gdprApplies: rawCapture?.gdprApplies ?? latestPingData?.gdprApplies ?? null,

      isServiceSpecific:
        rawCapture?.isServiceSpecific ??
        decodedTcString?.isServiceSpecific ??
        null,

      useNonStandardTexts: rawCapture?.useNonStandardTexts ?? null,

      publisherCC:
        rawCapture?.publisherCC ??
        decodedTcString?.publisherCC ??
        null,

      purposeOneTreatment:
        rawCapture?.purposeOneTreatment ??
        decodedTcString?.purposeOneTreatment ??
        null
    },

    observation: {
      eventStatus: rawCapture?.eventStatus ?? null,
      cmpStatus: rawCapture?.cmpStatus ?? latestPingData?.cmpStatus ?? null
    },

    gvl: {
      vendorListVersion:
        rawCapture?.vendorListVersion ??
        rawCapture?.gvlVersion ??
        latestPingData?.gvlVersion ??
        latestPingData?.vendorListVersion ??
        decodedTcString?.vendorListVersion ??
        null
    },

    consent: {
      tcString: rawCapture?.tcString ?? null,
      addtlConsent: rawCapture?.addtlConsent ?? null
    },

    purposes: {
      consents: rawCapture?.purpose?.consents ?? {},
      legitimateInterests: rawCapture?.purpose?.legitimateInterests ?? {}
    },

    vendors: {
      consents: rawCapture?.vendor?.consents ?? {},
      legitimateInterests: rawCapture?.vendor?.legitimateInterests ?? {},
      disclosedVendors:
        rawCapture?.vendor?.disclosedVendors ??
        rawCapture?.disclosedVendors ??
        {}
    },

    specialFeatureOptins: rawCapture?.specialFeatureOptins ?? {},

    publisher: {
      restrictions: rawCapture?.publisher?.restrictions ?? {},
      consents: rawCapture?.publisher?.consents ?? {},
      legitimateInterests: rawCapture?.publisher?.legitimateInterests ?? {}
    },

    diagnostics: {
      bridgeTimestampUtc: rawCapture?.timestampUtc ?? null,
      rawTopLevelKeys: Object.keys(rawCapture ?? {}),
      decodedTcStringCore: decodedTcString,
      latestPingData: latestPingData
    },

    fingerprintSource: null,
    stateFingerprint: null
  };

  state.fingerprintSource = buildFingerprintSource(state);

  return state;
}

function buildFingerprintSource(consentState) {
  return {
    cmp: consentState.cmp,
    gvl: consentState.gvl,
    consent: consentState.consent,
    purposes: consentState.purposes,
    vendors: consentState.vendors,
    specialFeatureOptins: consentState.specialFeatureOptins,
    publisher: consentState.publisher
  };
}

async function persistConsentState(consentState, rawTcData) {
  const db = await openVendorGetDb();
  const now = new Date().toISOString();

  return new Promise((resolve, reject) => {
    const tx = db.transaction(["consent_states", "consent_events"], "readwrite");

    const statesStore = tx.objectStore("consent_states");
    const eventsStore = tx.objectStore("consent_events");

    const getRequest = statesStore.get(consentState.stateFingerprint);

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

    getRequest.onsuccess = () => {
      const existingState = getRequest.result;

      if (existingState) {
        existingState.recordedAt =
          existingState.recordedAt ?? existingState.createdAt ?? now;
        existingState.recordingSource =
          existingState.recordingSource ?? EVIDENCE_RECORDING_SOURCE;
        existingState.lastSeenAt = now;
        existingState.seenCount = (existingState.seenCount ?? 1) + 1;
        existingState.updatedAt = now;

        statesStore.put(existingState);

        eventsStore.add({
          eventType: "duplicate_state",
          capturedAt: consentState.capturedAt,
          recordedAt: now,
          recordingSource: EVIDENCE_RECORDING_SOURCE,
          stateFingerprint: consentState.stateFingerprint,
          page: consentState.page,
          rawEventName: "consent_capture",
          rawTcData: rawTcData,
          diagnostics: consentState.diagnostics
        });

        resolve({
          action: "duplicate_state_updated",
          stateFingerprint: consentState.stateFingerprint,
          seenCount: existingState.seenCount
        });

        return;
      }

      const newStateRecord = {
        ...consentState,
        recordedAt: now,
        recordingSource: EVIDENCE_RECORDING_SOURCE,
        firstSeenAt: now,
        lastSeenAt: now,
        seenCount: 1,
        createdAt: now,
        updatedAt: now
      };

      statesStore.add(newStateRecord);

      eventsStore.add({
        eventType: "new_state",
        capturedAt: consentState.capturedAt,
        recordedAt: now,
        recordingSource: EVIDENCE_RECORDING_SOURCE,
        stateFingerprint: consentState.stateFingerprint,
        page: consentState.page,
        rawEventName: "consent_capture",
        rawTcData: rawTcData,
        diagnostics: consentState.diagnostics
      });

      resolve({
        action: "new_state_inserted",
        stateFingerprint: consentState.stateFingerprint,
        seenCount: 1
      });
    };

    tx.onerror = () => reject(tx.error);
  });
}

async function persistObservedRequest(observedRequest) {
  const db = await openVendorGetDb();

  return new Promise((resolve, reject) => {
    const tx = db.transaction(["observed_requests"], "readwrite");
    const requestsStore = tx.objectStore("observed_requests");
    const getRequest = requestsStore.get(observedRequest.requestFingerprint);

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

    getRequest.onsuccess = () => {
      const existingRequest = getRequest.result;

      if (existingRequest) {
        const updatedRequest = {
          ...existingRequest,
          requestFingerprintSource: observedRequest.requestFingerprintSource,
          recordedAt: existingRequest.recordedAt ?? observedRequest.recordedAt,
          recordingSource:
            existingRequest.recordingSource ?? observedRequest.recordingSource,
          lastSeenAt: observedRequest.lastSeenAt,
          seenCount: (existingRequest.seenCount ?? 1) + 1,
          request: observedRequest.request,
          consentParams: observedRequest.consentParams,
          context: observedRequest.context,
          correlation: observedRequest.correlation
        };

        requestsStore.put(updatedRequest);

        resolve({
          action: "observed_request_updated",
          requestFingerprint: observedRequest.requestFingerprint,
          seenCount: updatedRequest.seenCount
        });

        return;
      }

      requestsStore.add(observedRequest);

      resolve({
        action: "observed_request_inserted",
        requestFingerprint: observedRequest.requestFingerprint,
        seenCount: observedRequest.seenCount
      });
    };

    tx.onerror = () => reject(tx.error);
  });
}
