// Per-user secure key/value store. Replaces Tauri's plugin-stronghold +
// custom file vault with Electron's safeStorage (DPAPI on Windows,
// Keychain on macOS, libsecret on Linux). Encryption is at the file
// level — the whole entries map is a single encrypted blob — so there's
// no per-set ciphertext rotation to track.
//
// Pre-encrypt JSON: {version:1, entries: { <key>: <utf8-string-value> }}.
// When safeStorage is unavailable we degrade to a `.plaintext` JSON file
// and flag `encrypted: false` back to the renderer so the renderer can
// warn the user and avoid long-lived secrets.

import { app, ipcMain, safeStorage } from 'electron';
import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';

import {
  CHANNELS,
  type SecureStoreHandle,
  type SecureStoreOpenArgs,
} from '../ipc-types';

interface HandleState {
  filePath: string;
  encrypted: boolean;
  entries: Map<string, string>;
  saveTimer: NodeJS.Timeout | null;
}

const handles = new Map<string, HandleState>();

function hashUserId(userId: string): string {
  return createHash('sha256').update(userId).digest('hex').slice(0, 16);
}

function filePathFor(userId: string, encrypted: boolean): string {
  const suffix = hashUserId(userId);
  const ext = encrypted ? 'bin' : 'plaintext';
  return path.join(app.getPath('userData'), `chatapp-secure-${suffix}.${ext}`);
}

async function loadState(filePath: string, encrypted: boolean): Promise<Map<string, string>> {
  // Read step. Distinguish "no file yet" (genuinely new user — empty Map
  // is correct) from "file exists but unreadable" (corruption / DPAPI
  // breakage — we MUST NOT let the next write overwrite those bytes,
  // because the original ciphertext is the only path back to the user's
  // device keys if a future build can fix the read path).
  let rawBuf: Buffer | null = null;
  let rawStr: string | null = null;
  try {
    if (encrypted) {
      rawBuf = await fs.readFile(filePath);
    } else {
      rawStr = await fs.readFile(filePath, 'utf8');
    }
  } catch (err: unknown) {
    const code = (err as NodeJS.ErrnoException | null)?.code;
    if (code === 'ENOENT') return new Map();
    console.warn('[secure-store] read failed (non-ENOENT)', filePath, err);
    // For non-ENOENT read failures (EACCES, EBUSY, …) don't quarantine —
    // the file might be transiently locked. Empty map + future writes
    // will attempt to overwrite, matching the pre-0.17.1 behaviour for
    // these rarer cases.
    return new Map();
  }

  // Parse / decrypt step.
  try {
    if (encrypted && rawBuf) {
      const json = safeStorage.decryptString(rawBuf);
      const parsed = JSON.parse(json) as { version?: number; entries?: Record<string, string> };
      return new Map(Object.entries(parsed.entries ?? {}));
    }
    if (rawStr) {
      const parsed = JSON.parse(rawStr) as { version?: number; entries?: Record<string, string> };
      return new Map(Object.entries(parsed.entries ?? {}));
    }
    return new Map();
  } catch (err: unknown) {
    // CRITICAL: file existed but we couldn't decrypt or parse it. In the
    // pre-0.17.1 build we silently started fresh — the next set() then
    // scheduledSave() over-wrote the original ciphertext, destroying the
    // user's device keys forever. Now we rename the original to
    // `<file>.broken-<iso-ts>` BEFORE returning the empty map so the next
    // write goes to a new file and the original bytes survive for
    // forensics or a future decrypt-recovery path.
    const ts = new Date().toISOString().replace(/[:.]/g, '-');
    const brokenPath = `${filePath}.broken-${ts}`;
    try {
      await fs.rename(filePath, brokenPath);
      console.error(
        `[secure-store] DECRYPT/PARSE FAILED for ${filePath} — preserved original at ${brokenPath}. Original error:`,
        err,
      );
    } catch (renameErr: unknown) {
      // Even rename failed — fall back to the old behaviour (silent empty
      // map) but log loudly so it's visible in the main-process output.
      console.error(
        '[secure-store] rename of broken file failed; original may be overwritten on next save',
        renameErr,
        'original decrypt error:',
        err,
      );
    }
    return new Map();
  }
}

async function writeStateNow(state: HandleState): Promise<void> {
  const obj: Record<string, string> = {};
  for (const [k, v] of state.entries) obj[k] = v;
  const serialised = JSON.stringify({ version: 1, entries: obj });
  await fs.mkdir(path.dirname(state.filePath), { recursive: true });
  if (state.encrypted) {
    const buf = safeStorage.encryptString(serialised);
    await fs.writeFile(state.filePath, buf);
  } else {
    await fs.writeFile(state.filePath, serialised, 'utf8');
  }
}

function scheduleSave(state: HandleState): void {
  if (state.saveTimer) clearTimeout(state.saveTimer);
  state.saveTimer = setTimeout(() => {
    state.saveTimer = null;
    void writeStateNow(state).catch((err: unknown) => {
      console.warn('[secure-store] persist failed', err);
    });
  }, 100);
}

function requireState(handle: string): HandleState {
  const s = handles.get(handle);
  if (!s) throw new Error('secure-store: unknown handle');
  return s;
}

export function register(): void {
  ipcMain.handle(
    CHANNELS.SECURE_STORE_OPEN,
    async (_evt, args: SecureStoreOpenArgs): Promise<SecureStoreHandle> => {
      const encrypted = safeStorage.isEncryptionAvailable();
      const filePath = filePathFor(args.userId, encrypted);
      const entries = await loadState(filePath, encrypted);
      const handle = hashUserId(args.userId);
      handles.set(handle, { filePath, encrypted, entries, saveTimer: null });
      return { handle, encrypted };
    },
  );

  ipcMain.handle(
    CHANNELS.SECURE_STORE_GET,
    async (_evt, handle: string, key: string): Promise<string | null> => {
      const state = requireState(handle);
      return state.entries.get(key) ?? null;
    },
  );

  ipcMain.handle(
    CHANNELS.SECURE_STORE_SET,
    async (_evt, handle: string, key: string, value: string): Promise<void> => {
      const state = requireState(handle);
      state.entries.set(key, value);
      scheduleSave(state);
    },
  );

  ipcMain.handle(
    CHANNELS.SECURE_STORE_REMOVE,
    async (_evt, handle: string, key: string): Promise<void> => {
      const state = requireState(handle);
      state.entries.delete(key);
      scheduleSave(state);
    },
  );

  ipcMain.handle(CHANNELS.SECURE_STORE_CLOSE, async (_evt, handle: string): Promise<void> => {
    const state = handles.get(handle);
    if (!state) return;
    if (state.saveTimer) {
      clearTimeout(state.saveTimer);
      state.saveTimer = null;
      try {
        await writeStateNow(state);
      } catch (err: unknown) {
        console.warn('[secure-store] close-flush failed', err);
      }
    }
    handles.delete(handle);
  });
}
