"use strict";

const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT =
  "vendorget-gvl-evidence-export";
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT =
  "vendorget-gvl-revision-evidence";
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
const VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND = "iab-gvl-revision";

const VENDORGET_GVL_EVIDENCE_STORE_NAMES = [
  VENDORGET_STORE_NAMES.gvlRawEvidence,
  VENDORGET_STORE_NAMES.gvlSnapshots,
  VENDORGET_STORE_NAMES.gvlVendors,
  VENDORGET_STORE_NAMES.gvlPurposes,
  VENDORGET_STORE_NAMES.gvlSpecialPurposes,
  VENDORGET_STORE_NAMES.gvlFeatures,
  VENDORGET_STORE_NAMES.gvlSpecialFeatures,
  VENDORGET_STORE_NAMES.gvlDataCategories,
  VENDORGET_STORE_NAMES.gvlVendorRelationships
];

const VENDORGET_GVL_EVIDENCE_ARRAY_NAMES = [
  "gvl_raw_evidence",
  "gvl_snapshots",
  "gvl_vendors",
  "gvl_purposes",
  "gvl_special_purposes",
  "gvl_features",
  "gvl_special_features",
  "gvl_data_categories",
  "gvl_vendor_relationships"
];

const VENDORGET_GVL_NORMALIZED_STORE_NAMES = [
  VENDORGET_STORE_NAMES.gvlVendors,
  VENDORGET_STORE_NAMES.gvlPurposes,
  VENDORGET_STORE_NAMES.gvlSpecialPurposes,
  VENDORGET_STORE_NAMES.gvlFeatures,
  VENDORGET_STORE_NAMES.gvlSpecialFeatures,
  VENDORGET_STORE_NAMES.gvlDataCategories,
  VENDORGET_STORE_NAMES.gvlVendorRelationships
];

async function exportVendorGetGvlEvidenceJson() {
  const db = await openVendorGetDb();
  const exportedStores = await readGvlEvidenceStores(db);

  return {
    metadata: {
      exportFormat: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT,
      exportFormatVersion: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION,
      exportedAt: new Date().toISOString(),
      dbName: db.name,
      dbVersion: db.version
    },
    ...exportedStores
  };
}

async function exportVendorGetGvlRevisionEvidenceJson(snapshotSha256) {
  if (!snapshotSha256) {
    throw new Error("missing_snapshot_sha256");
  }

  const db = await openVendorGetDb();
  const snapshot = await getGvlEvidenceRecordByKey(
    db,
    VENDORGET_STORE_NAMES.gvlSnapshots,
    snapshotSha256
  );

  if (!snapshot) {
    throw new Error("gvl_snapshot_not_found");
  }

  const rawGvlSha256 = snapshot.rawGvlSha256 ?? null;
  const rawEvidence = rawGvlSha256
    ? await getGvlEvidenceRecordByKey(
        db,
        VENDORGET_STORE_NAMES.gvlRawEvidence,
        rawGvlSha256
      )
    : null;
  const normalized = await readGvlRevisionNormalizedRecords(db, snapshotSha256);
  const exportedAt = new Date();
  const exportedAtUtcCompact = formatGvlEvidenceUtcCompact(exportedAt);
  const payload = {
    metadata: {
      exportFormat: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT,
      exportFormatVersion: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION,
      exportedAt: exportedAt.toISOString(),
      exportedAtUtcCompact,
      dbName: db.name,
      dbVersion: db.version,
      vendorListVersion: snapshot.vendorListVersion ?? null,
      snapshotSha256: snapshot.sha256 ?? snapshot.snapshotSha256 ?? null,
      rawGvlSha256,
      contentKind: VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND
    },
    rawEvidence,
    snapshot,
    normalized
  };
  const exportPayloadSha256 = await calculateGvlEvidencePayloadSha256(payload);

  return {
    ...payload,
    metadata: {
      ...payload.metadata,
      exportPayloadSha256
    }
  };
}

async function verifyVendorGetGvlRevisionEvidenceJson(exportContainer) {
  const errors = [];
  const metadata = exportContainer?.metadata ?? {};
  const normalized = exportContainer?.normalized ?? {};
  const rawBody = exportContainer?.rawEvidence?.rawBody ?? null;
  const snapshot = exportContainer?.snapshot ?? null;
  const snapshotSha256 = metadata.snapshotSha256 ?? null;
  const rawGvlSha256 = metadata.rawGvlSha256 ?? null;

  if (!exportContainer || typeof exportContainer !== "object") {
    errors.push("invalid_export_container");
  }

  if (metadata.exportFormat !== VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT) {
    errors.push("invalid_export_format");
  }

  if (
    metadata.exportFormatVersion !==
    VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION
  ) {
    errors.push("unsupported_export_format_version");
  }

  if (metadata.contentKind !== VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND) {
    errors.push("invalid_content_kind");
  }

  if (metadata.vendorListVersion === null || metadata.vendorListVersion === undefined) {
    errors.push("missing_vendor_list_version");
  }

  if (!snapshotSha256) {
    errors.push("missing_snapshot_sha256");
  }

  if (!rawGvlSha256) {
    errors.push("missing_raw_gvl_sha256");
  }

  if (typeof rawBody !== "string") {
    errors.push("missing_raw_body");
  } else if ((await VendorGetGvlService.calculateRawGvlSha256(rawBody)) !== rawGvlSha256) {
    errors.push("raw_body_sha256_mismatch");
  }

  const snapshotRecordSha256 = snapshot?.sha256 ?? snapshot?.snapshotSha256 ?? null;

  if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
    errors.push("missing_snapshot");
  } else if (snapshotRecordSha256 !== snapshotSha256) {
    errors.push("snapshot_sha256_mismatch");
  } else if (
    (await VendorGetGvlService.calculateGvlSnapshotSha256(snapshot.rawJson)) !==
    snapshotSha256
  ) {
    errors.push("snapshot_sha256_mismatch");
  }

  for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
    if (!Array.isArray(normalized[storeName])) {
      errors.push(`missing_normalized_store_${storeName}`);
      continue;
    }

    for (const record of normalized[storeName]) {
      if (record?.snapshotSha256 !== snapshotSha256) {
        errors.push(`normalized_record_snapshot_mismatch_${storeName}`);
        break;
      }
    }
  }

  if (!metadata.exportPayloadSha256) {
    errors.push("missing_export_payload_sha256");
  } else {
    const recalculatedPayloadSha256 =
      await calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(exportContainer);

    if (recalculatedPayloadSha256 !== metadata.exportPayloadSha256) {
      errors.push("export_payload_sha256_mismatch");
    }
  }

  return {
    valid: errors.length === 0,
    errors,
    vendorListVersion: metadata.vendorListVersion ?? null,
    snapshotSha256,
    rawGvlSha256,
    exportPayloadSha256: metadata.exportPayloadSha256 ?? null,
    normalizedCounts: countGvlRevisionNormalizedRecords(normalized)
  };
}

function getGvlEvidenceRecordByKey(db, storeName, key) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction([storeName], "readonly");
    const getRequest = tx.objectStore(storeName).get(key);
    let record = null;

    getRequest.onerror = () => reject(getRequest.error);
    getRequest.onsuccess = () => {
      record = getRequest.result ?? null;
    };
    tx.onerror = () => reject(tx.error);
    tx.onabort = () => reject(tx.error);
    tx.oncomplete = () => resolve(record);
  });
}

function readGvlRevisionNormalizedRecords(db, snapshotSha256) {
  return new Promise((resolve, reject) => {
    const normalized = {};
    const tx = db.transaction(VENDORGET_GVL_NORMALIZED_STORE_NAMES, "readonly");

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

    for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
      const records = [];
      const cursorRequest = tx
        .objectStore(storeName)
        .index("snapshotSha256")
        .openCursor(IDBKeyRange.only(snapshotSha256));

      normalized[storeName] = records;
      cursorRequest.onerror = () => reject(cursorRequest.error);
      cursorRequest.onsuccess = () => {
        const cursor = cursorRequest.result;

        if (!cursor) {
          return;
        }

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

async function calculateGvlEvidencePayloadSha256(payload) {
  return sha256Hex(stableStringify(payload));
}

async function calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(
  exportContainer
) {
  const metadata = {
    ...(exportContainer?.metadata ?? {})
  };

  delete metadata.exportPayloadSha256;

  return calculateGvlEvidencePayloadSha256({
    ...exportContainer,
    metadata
  });
}

function countGvlRevisionNormalizedRecords(normalized) {
  return Object.fromEntries(
    VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
      storeName,
      Array.isArray(normalized?.[storeName]) ? normalized[storeName].length : 0
    ])
  );
}

function formatGvlEvidenceUtcCompact(date) {
  return [
    date.getUTCFullYear(),
    padGvlEvidenceDatePart(date.getUTCMonth() + 1),
    padGvlEvidenceDatePart(date.getUTCDate()),
    "T",
    padGvlEvidenceDatePart(date.getUTCHours()),
    padGvlEvidenceDatePart(date.getUTCMinutes()),
    padGvlEvidenceDatePart(date.getUTCSeconds()),
    "Z"
  ].join("");
}

function padGvlEvidenceDatePart(value) {
  return String(value).padStart(2, "0");
}

function readGvlEvidenceStores(db) {
  return new Promise((resolve, reject) => {
    const exportContainer = {};
    const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readonly");

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

    for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
      const records = [];
      const cursorRequest = tx.objectStore(storeName).openCursor();

      exportContainer[storeName] = records;

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

        if (!cursor) {
          return;
        }

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

async function importVendorGetGvlEvidenceJson(importContainer) {
  validateVendorGetGvlEvidenceImport(importContainer);

  const db = await openVendorGetDb();

  return importGvlEvidenceStores(db, importContainer);
}

function validateVendorGetGvlEvidenceImport(importContainer) {
  if (!importContainer || typeof importContainer !== "object") {
    throw new Error("invalid_gvl_evidence_export");
  }

  const metadata = importContainer.metadata ?? {};

  if (metadata.exportFormat !== VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT) {
    throw new Error("invalid_gvl_evidence_export_format");
  }

  if (
    metadata.exportFormatVersion !==
    VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION
  ) {
    throw new Error("unsupported_gvl_evidence_export_version");
  }

  for (const arrayName of VENDORGET_GVL_EVIDENCE_ARRAY_NAMES) {
    if (!Array.isArray(importContainer[arrayName])) {
      throw new Error(`missing_gvl_evidence_store_${arrayName}`);
    }
  }
}

function importGvlEvidenceStores(db, importContainer) {
  return new Promise((resolve, reject) => {
    const counts = buildEmptyGvlEvidenceImportCounts();
    const seenKeysByStore = new Map();
    const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");

    tx.onerror = () => reject(tx.error);
    tx.onabort = () => reject(tx.error);
    tx.oncomplete = () => {
      resolve({
        importedAt: new Date().toISOString(),
        counts
      });
    };

    for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
      importGvlEvidenceStoreRecords(
        tx.objectStore(storeName),
        importContainer[storeName],
        counts[storeName],
        getSeenKeysForStore(seenKeysByStore, storeName)
      );
    }
  });
}

function buildEmptyGvlEvidenceImportCounts() {
  return Object.fromEntries(
    VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
      storeName,
      {
        read: 0,
        inserted: 0,
        skippedExisting: 0,
        skippedInvalid: 0
      }
    ])
  );
}

function getSeenKeysForStore(seenKeysByStore, storeName) {
  if (!seenKeysByStore.has(storeName)) {
    seenKeysByStore.set(storeName, new Set());
  }

  return seenKeysByStore.get(storeName);
}

function importGvlEvidenceStoreRecords(objectStore, records, counts, seenKeys) {
  for (const record of records) {
    counts.read += 1;

    const key = getGvlEvidenceRecordKey(objectStore, record);
    const keySignature = JSON.stringify(key);

    if (key === null || seenKeys.has(keySignature)) {
      counts.skippedInvalid += 1;
      continue;
    }

    seenKeys.add(keySignature);

    const getRequest = objectStore.get(key);

    getRequest.onsuccess = () => {
      if (getRequest.result !== undefined) {
        counts.skippedExisting += 1;
        return;
      }

      const addRequest = objectStore.add(record);

      addRequest.onsuccess = () => {
        counts.inserted += 1;
      };
    };
  }
}

function getGvlEvidenceRecordKey(objectStore, record) {
  if (!record || typeof record !== "object" || Array.isArray(record)) {
    return null;
  }

  if (objectStore.name === VENDORGET_STORE_NAMES.gvlSnapshots) {
    return record.sha256 ?? record.snapshotSha256 ?? null;
  }

  const keyPath = objectStore.keyPath;

  if (typeof keyPath !== "string") {
    return null;
  }

  return record[keyPath] ?? null;
}
