import { createHash } from 'node:crypto';

export const APPDATA_ARCHIVE_VERSION = 1;
export const APPDATA_ARCHIVE_CHECKSUM_ALGORITHM = 'sha256';

export function stableStringify(value) {
  return JSON.stringify(sortJsonValue(value));
}

export function sha256Hex(value) {
  return createHash('sha256').update(String(value)).digest('hex');
}

export function buildAppDataArchive(rows, options = {}) {
  const normalizedRows = [...rows]
    .sort((a, b) => String(a.key).localeCompare(String(b.key)))
    .map((row) => {
      const canonicalValue = stableStringify(row.value);
      return {
        key: String(row.key),
        value: row.value,
        version: toIsoString(row.updatedAt),
        valueChecksum: sha256Hex(canonicalValue),
        bytes: Buffer.byteLength(canonicalValue),
      };
    });
  const keys = normalizedRows.map((row) => row.key);
  const metadata = {
    archiveVersion: APPDATA_ARCHIVE_VERSION,
    exportedAt: options.exportedAt ?? new Date().toISOString(),
    appVersion: options.appVersion ?? 'unknown',
    appBuildTime: options.appBuildTime ?? null,
    sourceCommit: options.sourceCommit ?? 'unknown',
    rowCount: normalizedRows.length,
    keyCount: keys.length,
    checksumAlgorithm: APPDATA_ARCHIVE_CHECKSUM_ALGORITHM,
    keyListChecksum: sha256Hex(stableStringify(keys)),
    payloadChecksum: '',
  };
  const archive = { metadata, keys, rows: normalizedRows };
  archive.metadata.payloadChecksum = checksumPayload(archive);
  return archive;
}

export function verifyAppDataArchive(archive) {
  const errors = [];
  if (!archive || typeof archive !== 'object') {
    return { ok: false, errors: ['Archive must be a JSON object'], summary: emptySummary() };
  }

  const metadata = archive.metadata;
  const keys = Array.isArray(archive.keys) ? archive.keys : [];
  const rows = Array.isArray(archive.rows) ? archive.rows : [];

  if (!metadata || typeof metadata !== 'object') {
    errors.push('Missing metadata object');
  } else {
    if (metadata.archiveVersion !== APPDATA_ARCHIVE_VERSION) {
      errors.push(`Unsupported archiveVersion: ${metadata.archiveVersion}`);
    }
    if (metadata.checksumAlgorithm !== APPDATA_ARCHIVE_CHECKSUM_ALGORITHM) {
      errors.push(`Unsupported checksumAlgorithm: ${metadata.checksumAlgorithm}`);
    }
    if (metadata.rowCount !== rows.length) {
      errors.push(`rowCount mismatch: metadata=${metadata.rowCount}, actual=${rows.length}`);
    }
    if (metadata.keyCount !== keys.length) {
      errors.push(`keyCount mismatch: metadata=${metadata.keyCount}, actual=${keys.length}`);
    }
  }

  const rowKeys = rows.map((row) => row?.key);
  if (stableStringify(keys) !== stableStringify(rowKeys)) {
    errors.push('Key list does not match row keys');
  }

  const keyListChecksum = sha256Hex(stableStringify(keys));
  if (metadata?.keyListChecksum !== keyListChecksum) {
    errors.push('Key list checksum mismatch');
  }

  for (const row of rows) {
    if (!row || typeof row !== 'object') {
      errors.push('Archive contains a non-object row');
      continue;
    }
    if (typeof row.key !== 'string') {
      errors.push('Archive row is missing string key');
    }
    if (typeof row.version !== 'string') {
      errors.push(`Archive row ${row.key ?? '<unknown>'} is missing string version`);
    }
    const canonicalValue = stableStringify(row.value);
    const valueChecksum = sha256Hex(canonicalValue);
    if (row.valueChecksum !== valueChecksum) {
      errors.push(`Value checksum mismatch for ${row.key}`);
    }
    const bytes = Buffer.byteLength(canonicalValue);
    if (row.bytes !== bytes) {
      errors.push(`Byte size mismatch for ${row.key}`);
    }
  }

  const payloadChecksum = checksumPayload({ keys, rows });
  if (metadata?.payloadChecksum !== payloadChecksum) {
    errors.push('Payload checksum mismatch');
  }

  return {
    ok: errors.length === 0,
    errors,
    summary: {
      archiveVersion: metadata?.archiveVersion ?? null,
      exportedAt: metadata?.exportedAt ?? null,
      appVersion: metadata?.appVersion ?? null,
      sourceCommit: metadata?.sourceCommit ?? null,
      rowCount: rows.length,
      keyCount: keys.length,
      keys,
      payloadChecksum,
    },
  };
}

export function checksumPayload(archive) {
  return sha256Hex(stableStringify({
    keys: archive.keys,
    rows: archive.rows,
  }));
}

function sortJsonValue(value) {
  if (Array.isArray(value)) return value.map(sortJsonValue);
  if (!value || typeof value !== 'object') return value;
  return Object.fromEntries(
    Object.entries(value)
      .filter(([, item]) => item !== undefined)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([key, item]) => [key, sortJsonValue(item)]),
  );
}

function toIsoString(value) {
  if (value instanceof Date) return value.toISOString();
  const date = new Date(value);
  if (Number.isFinite(date.getTime())) return date.toISOString();
  throw new Error(`Invalid AppData updatedAt value: ${value}`);
}

function emptySummary() {
  return {
    archiveVersion: null,
    exportedAt: null,
    appVersion: null,
    sourceCommit: null,
    rowCount: 0,
    keyCount: 0,
    keys: [],
    payloadChecksum: null,
  };
}
