const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { MongoClient } = require('mongodb');

const COLLECTIONS = [
  { key: 'proofs', collection: 'chain_proofs' },
  { key: 'transactions', collection: 'chain_transactions' },
  { key: 'blocks', collection: 'chain_blocks' },
  { key: 'callbackEvents', collection: 'chain_callbacks' },
  { key: 'nodes', collection: 'node_participants' },
  { key: 'nodeEconomyEntries', collection: 'node_economy_ledger' },
  { key: 'pointEntries', collection: 'point_ledger' },
  { key: 'validatorVotes', collection: 'validator_votes' },
  { key: 'backupIndexes', collection: 'backup_indexes' },
  { key: 'distributedChunks', collection: 'distributed_chunks' },
  { key: 'wallets', collection: 'wallets' },
  { key: 'walletTransactions', collection: 'wallet_transactions' }
];

const dataDir = path.join(__dirname, '..', 'data');
const statusPath = path.join(dataDir, 'mongo-observation-status.json');
const comparePath = path.join(dataDir, 'mongo-observation-compare.json');
const windowPath = path.join(dataDir, 'mongo-observation-window.json');

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

function stableStringify(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
}

function getDbUri() {
  return process.env.AISFOR_CHAIN_LEDGER_DB_URI || process.env.DATABASE_URL || process.env.MONGODB_URI || '';
}

function isRequested() {
  return process.env.AISFOR_CHAIN_LEDGER_MONGO_OBSERVE === '1';
}

function isWriteEnabled() {
  return isRequested() && process.env.AISFOR_CHAIN_LEDGER_DB_WRITE === '1';
}

function observationName(collection) {
  return `observation_${collection}`;
}

function toDocument(item, row, index, syncedAt) {
  const sourceHash = sha256(stableStringify(row));
  const observationId = sha256(`${item.key}:${index}:${sourceHash}`);
  return {
    _id: observationId,
    _observationId: observationId,
    _observationKind: 'ledger-json-dual-write-observation',
    _targetCollection: item.collection,
    _sourceKey: item.key,
    _sourceIndex: index,
    _sourceHash: sourceHash,
    observedAt: syncedAt,
    payload: row
  };
}

function buildDocuments(state, syncedAt = new Date().toISOString()) {
  return COLLECTIONS.map((item) => {
    const rows = Array.isArray(state[item.key]) ? state[item.key] : [];
    return { ...item, observationCollection: observationName(item.collection), docs: rows.map((row, index) => toDocument(item, row, index, syncedAt)) };
  });
}

function readJson(filePath, fallback) {
  try {
    const stat = fs.statSync(filePath);
    return { ...JSON.parse(fs.readFileSync(filePath, 'utf8')), exists: true, fileUpdatedAt: stat.mtime.toISOString() };
  } catch (err) {
    return { ...fallback, exists: false, fileUpdatedAt: null, path: filePath };
  }
}

function writeJson(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
}

function buildWindow(history) {
  const rows = Array.isArray(history) ? history.slice(-48) : [];
  let consecutiveOk = 0;
  for (let i = rows.length - 1; i >= 0; i -= 1) {
    if (!rows[i].ok) break;
    consecutiveOk += 1;
  }
  const latest = rows[rows.length - 1] || null;
  return {
    ok: rows.length > 0 && consecutiveOk >= 3 && rows.every((row) => row.ok),
    mode: 'mongo-observation-window',
    sampleCount: rows.length,
    consecutiveOk,
    failedSamples: rows.filter((row) => !row.ok).length,
    latestCheckedAt: latest ? latest.checkedAt : null,
    latestExpected: latest ? latest.totalExpected : 0,
    latestActual: latest ? latest.totalActual : 0,
    path: windowPath,
    history: rows.slice(-12)
  };
}

function appendWindowSample(compareResult) {
  const current = readJson(windowPath, { history: [] });
  const history = Array.isArray(current.history) ? current.history.slice(-47) : [];
  history.push({
    ok: Boolean(compareResult.ok),
    checkedAt: compareResult.checkedAt,
    totalExpected: compareResult.totalExpected,
    totalActual: compareResult.totalActual,
    failedCollections: compareResult.failedCollections
  });
  const next = buildWindow(history);
  writeJson(windowPath, next);
  return next;
}
async function syncState(state, options = {}) {
  const uri = getDbUri();
  if (!uri) throw new Error('AISFOR_CHAIN_LEDGER_DB_URI is required for Mongo observation sync.');

  const syncedAt = new Date().toISOString();
  const collections = buildDocuments(state, syncedAt);
  const client = new MongoClient(uri, { serverSelectionTimeoutMS: 5000 });

  await client.connect();
  try {
    const db = client.db();
    const rows = [];
    for (const item of collections) {
      const col = db.collection(item.observationCollection);
      await col.deleteMany({ _observationKind: 'ledger-json-dual-write-observation' });
      if (item.docs.length) {
        await col.bulkWrite(
          item.docs.map((doc) => ({
            updateOne: {
              filter: { _id: doc._id },
              update: { $set: doc },
              upsert: true
            }
          })),
          { ordered: false }
        );
      }
      await col.createIndex({ _observationId: 1 }, { unique: true });
      await col.createIndex({ _sourceHash: 1 });
      await col.createIndex({ _sourceIndex: 1 });
      rows.push({
        key: item.key,
        targetCollection: item.collection,
        observationCollection: item.observationCollection,
        documents: item.docs.length,
        hash: sha256(stableStringify(item.docs.map((doc) => doc._sourceHash)))
      });
    }

    const status = {
      ok: true,
      mode: 'mongo-dual-write-observation',
      writesLiveCollections: false,
      requested: isRequested(),
      writeEnabled: isWriteEnabled(),
      reason: options.reason || 'manual-sync',
      syncedAt,
      totalDocuments: rows.reduce((sum, row) => sum + row.documents, 0),
      collections: rows,
      path: statusPath
    };

    await db.collection('observation_ledger_meta').updateOne(
      { _id: 'latest-ledger-json-observation' },
      { $set: status, $push: { history: { syncedAt, totalDocuments: status.totalDocuments, reason: status.reason } } },
      { upsert: true }
    );
    writeJson(statusPath, status);
    return status;
  } finally {
    await client.close();
  }
}

async function compareState(state) {
  const uri = getDbUri();
  if (!uri) throw new Error('AISFOR_CHAIN_LEDGER_DB_URI is required for Mongo observation compare.');

  const expected = buildDocuments(state, 'compare-only');
  const client = new MongoClient(uri, { serverSelectionTimeoutMS: 5000 });
  await client.connect();
  try {
    const db = client.db();
    const rows = [];
    for (const item of expected) {
      const col = db.collection(item.observationCollection);
      const actualCount = await col.countDocuments({ _observationKind: 'ledger-json-dual-write-observation' });
      const actualDocs = await col.find(
        { _observationKind: 'ledger-json-dual-write-observation' },
        { projection: { _sourceHash: 1, _sourceIndex: 1 } }
      ).sort({ _sourceIndex: 1 }).toArray();
      const expectedHashes = item.docs.map((doc) => doc._sourceHash);
      const actualHashes = actualDocs.map((doc) => doc._sourceHash);
      const mismatches = [];
      const max = Math.max(expectedHashes.length, actualHashes.length);
      for (let i = 0; i < max && mismatches.length < 5; i += 1) {
        if (expectedHashes[i] !== actualHashes[i]) mismatches.push({ index: i, expected: expectedHashes[i] || null, actual: actualHashes[i] || null });
      }
      rows.push({
        key: item.key,
        observationCollection: item.observationCollection,
        expectedCount: item.docs.length,
        actualCount,
        ok: actualCount === item.docs.length && mismatches.length === 0,
        mismatches
      });
    }
    const failed = rows.filter((row) => !row.ok);
    const result = {
      ok: failed.length === 0,
      mode: 'mongo-observation-compare',
      totalExpected: rows.reduce((sum, row) => sum + row.expectedCount, 0),
      totalActual: rows.reduce((sum, row) => sum + row.actualCount, 0),
      failedCollections: failed.length,
      rows,
      checkedAt: new Date().toISOString(),
      path: comparePath
    };
    writeJson(comparePath, result);
    result.window = appendWindowSample(result);
    return result;
  } finally {
    await client.close();
  }
}

function writeObservationCollections(state) {
  if (!isWriteEnabled()) return Promise.resolve(getStatus());
  return syncState(state, { reason: 'persistentStore.writeState' });
}

function getStatus() {
  const latest = readJson(statusPath, {
    ok: false,
    mode: 'not-run',
    requested: isRequested(),
    writeEnabled: isWriteEnabled(),
    totalDocuments: 0,
    collections: [],
    path: statusPath
  });
  const window = readJson(windowPath, {
    ok: false,
    mode: 'not-run',
    sampleCount: 0,
    consecutiveOk: 0,
    failedSamples: 0,
    latestCheckedAt: null,
    latestExpected: 0,
    latestActual: 0,
    history: [],
    path: windowPath
  });
  const compare = readJson(comparePath, {
    ok: false,
    mode: 'not-run',
    totalExpected: 0,
    totalActual: 0,
    failedCollections: 0,
    rows: [],
    path: comparePath
  });
  return {
    ...latest,
    requested: isRequested(),
    writeEnabled: isWriteEnabled(),
    statusPath,
    comparePath,
    compare,
    window
  };
}

module.exports = {
  COLLECTIONS,
  buildDocuments,
  syncState,
  compareState,
  writeObservationCollections,
  getStatus,
  buildWindow,
  isRequested,
  isWriteEnabled
};


