/**
 * Atomic multi-file auth state for Baileys.
 *
 * Replaces Baileys' useMultiFileAuthState, whose writeData is a bare fs.writeFile
 * (truncate-then-write, no rename). A process death between the truncate and the
 * write leaves a 0-byte creds.json; on the next boot Baileys' readData swallows the
 * JSON.parse error and silently mints a FRESH identity — the session looks "logged
 * out" and the user is forced to re-scan the QR even though WhatsApp never unlinked
 * the device. creds.update fires constantly while connected (key rotations,
 * app-state sync), so that corruption window recurs for the whole session.
 *
 * Hardening over upstream:
 * - Atomic writes: every file is written to a temp name then rename()d into place,
 *   so a kill at any moment leaves the old or the new content — never a truncated file.
 * - creds.json is mirrored to creds.json.bak on every save; on load, a missing or
 *   corrupt creds.json is restored from the backup instead of re-initializing.
 * - A corrupt creds.json with no usable backup is quarantined (renamed) so
 *   hasCredentials() stops treating the dead session as linked.
 */

import { BufferJSON, initAuthCreds, proto } from '@whiskeysockets/baileys';
import type { AuthenticationCreds, AuthenticationState, SignalDataTypeMap } from '@whiskeysockets/baileys';
import { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';
import fs from 'fs';
import path from 'path';
import { log } from '../../shared/logger.js';

/** Serializes reads/writes per file path (Baileys events fire concurrently). In-process
 *  only — cross-process safety comes from the atomic rename, not from this. */
const fileQueues = new Map<string, Promise<unknown>>();

function queued<T>(file: string, task: () => Promise<T>): Promise<T> {
  const prev = fileQueues.get(file) || Promise.resolve();
  const next = prev.then(task, task);
  // Store a value-free tail (a resolved read would otherwise pin the file's contents
  // in memory forever) and drop the entry once its queue drains.
  const tail = next.then(() => undefined, () => undefined);
  fileQueues.set(file, tail);
  void tail.then(() => {
    if (fileQueues.get(file) === tail) fileQueues.delete(file);
  });
  return next;
}

/** Await everything currently queued for an auth folder — drained on disconnect so a
 *  process exit right after can't clip a signal-key or creds write. */
export function flushAuthWrites(folder: string): Promise<void> {
  const prefix = folder.endsWith(path.sep) ? folder : folder + path.sep;
  const tails: Promise<unknown>[] = [];
  for (const [file, tail] of fileQueues) {
    if (file.startsWith(prefix)) tails.push(tail);
  }
  return Promise.all(tails).then(() => undefined);
}

/** Same name mangling as upstream so existing auth dirs keep working as-is. */
const fixFileName = (file: string) => file.replace(/\//g, '__').replace(/:/g, '-');

/** True when the parsed object looks like real Baileys credentials (not null/empty/garbage). */
function isValidCreds(creds: unknown): creds is AuthenticationCreds {
  const c = creds as AuthenticationCreds | null;
  return !!(
    c && typeof c === 'object' &&
    c.noiseKey && c.signedIdentityKey && c.signedPreKey &&
    typeof c.registrationId === 'number'
  );
}

async function writeAtomic(filePath: string, data: string): Promise<void> {
  const tmp = `${filePath}.${process.pid}.tmp`;
  try {
    await writeFile(tmp, data);
    await rename(tmp, filePath);
  } catch (err) {
    try { await unlink(tmp); } catch {}
    throw err;
  }
}

export interface AtomicAuthState {
  state: AuthenticationState;
  saveCreds: () => Promise<void>;
  /** creds.json was missing/corrupt but recovered from creds.json.bak */
  restoredFromBackup: boolean;
  /** No usable credentials found — a brand-new identity was initialized (pairing needed) */
  freshIdentity: boolean;
}

export async function useAtomicMultiFileAuthState(folder: string): Promise<AtomicAuthState> {
  await mkdir(folder, { recursive: true });

  const filePath = (file: string) => path.join(folder, fixFileName(file));

  // Sweep temp files orphaned by a previous process death mid-write
  try {
    for (const f of fs.readdirSync(folder)) {
      if (f.endsWith('.tmp')) {
        try { fs.unlinkSync(path.join(folder, f)); } catch {}
      }
    }
  } catch {}

  const readData = async (file: string): Promise<any> => {
    try {
      const data = await queued(filePath(file), () => readFile(filePath(file), { encoding: 'utf-8' }));
      return JSON.parse(data, BufferJSON.reviver);
    } catch {
      return null;
    }
  };

  const writeData = (data: unknown, file: string): Promise<void> => {
    // Snapshot synchronously — the creds object mutates while writes are queued
    const json = JSON.stringify(data, BufferJSON.replacer);
    return queued(filePath(file), () => writeAtomic(filePath(file), json));
  };

  const removeData = (file: string): Promise<void> =>
    queued(filePath(file), async () => {
      try { await unlink(filePath(file)); } catch {}
    });

  // ── Load creds, recovering from backup when the primary is missing/corrupt ──
  let creds: AuthenticationCreds | null = await readData('creds.json');
  let restoredFromBackup = false;
  if (!isValidCreds(creds)) {
    const backup = await readData('creds.json.bak');
    if (isValidCreds(backup)) {
      creds = backup;
      restoredFromBackup = true;
      await writeData(backup, 'creds.json');
      log.warn('[whatsapp] creds.json was missing or corrupt — restored from creds.json.bak');
    } else {
      if (fs.existsSync(filePath('creds.json'))) {
        // Present but unreadable and no usable backup: quarantine it so the dead
        // session isn't half-trusted, and start a clean pairing flow.
        const quarantine = filePath(`creds.json.corrupt-${Date.now()}`);
        try { await rename(filePath('creds.json'), quarantine); } catch {}
        log.warn(`[whatsapp] creds.json is corrupt with no backup — quarantined as ${path.basename(quarantine)}; re-link required`);
      }
      // The corrupt backup must go too, or hasValidCredsFile() keeps presenting the
      // dead session as "linked" on every future boot.
      if (fs.existsSync(filePath('creds.json.bak'))) {
        try { await rename(filePath('creds.json.bak'), filePath(`creds.json.bak.corrupt-${Date.now()}`)); } catch {}
      }
      creds = null;
    }
  }
  const freshIdentity = !creds;
  if (freshIdentity) {
    // Any key material on disk is bound to a dead identity — sweep it so the fresh
    // pairing starts clean instead of tripping over stale sessions and pre-keys.
    // (Quarantined creds files are kept for forensics.)
    try {
      for (const f of fs.readdirSync(folder)) {
        if (f === 'creds.json' || f.includes('.corrupt-')) continue;
        try { fs.unlinkSync(path.join(folder, f)); } catch {}
      }
    } catch {}
  }
  const liveCreds: AuthenticationCreds = creds || initAuthCreds();

  return {
    state: {
      creds: liveCreds,
      keys: {
        get: async (type, ids) => {
          const data: { [_: string]: SignalDataTypeMap[typeof type] } = {};
          await Promise.all(
            ids.map(async (id) => {
              let value = await readData(`${type}-${id}.json`);
              if (type === 'app-state-sync-key' && value) {
                value = proto.Message.AppStateSyncKeyData.fromObject(value);
              }
              data[id] = value;
            }),
          );
          return data;
        },
        set: async (data) => {
          const tasks: Promise<void>[] = [];
          for (const category in data) {
            const entries = data[category as keyof SignalDataTypeMap];
            if (!entries) continue;
            for (const id in entries) {
              const value = entries[id];
              const file = `${category}-${id}.json`;
              tasks.push(value ? writeData(value, file) : removeData(file));
            }
          }
          await Promise.all(tasks);
        },
      },
    },
    saveCreds: async () => {
      // One snapshot for both files so primary and backup are always identical
      const json = JSON.stringify(liveCreds, BufferJSON.replacer);
      await queued(filePath('creds.json'), () => writeAtomic(filePath('creds.json'), json));
      await queued(filePath('creds.json.bak'), () => writeAtomic(filePath('creds.json.bak'), json));
    },
    restoredFromBackup,
    freshIdentity,
  };
}

/** Sync check used by status endpoints: a linked session exists when creds.json (or its
 *  backup) is present and non-empty. A 0-byte creds.json — the artifact of an interrupted
 *  legacy write — no longer counts as "linked". */
export function hasValidCredsFile(folder: string): boolean {
  for (const name of ['creds.json', 'creds.json.bak']) {
    try {
      if (fs.statSync(path.join(folder, name)).size > 0) return true;
    } catch {}
  }
  return false;
}
