/**
 * Channel-meta cache. Records (channelId → xType) for every fabric
 * channel the gateway has seen at least one inbound message in.
 *
 * Populated lazily from inbound (`recordChannelType` is called for
 * every `message.created` event with non-empty `xType`). Persisted to
 * `~/.openclaw/fabric-channel-meta.json` so the cache survives
 * gateway restarts (so the very first DM after restart still gets the
 * right xType without waiting for a fresh inbound).
 *
 * Exposed cross-plugin via `globalThis.__fabric.getChannelType`. Used
 * by ClawPrompts' fabric-chat-injector to narrow its prompt injection
 * to xType==='dm' only.
 *
 * Failure mode: lookup misses (channel never seen / inbound dropped
 * xType) return null. Callers MUST treat null as "unknown" — DO NOT
 * fall back to "assume DM" or you re-introduce the false-positive on
 * group channels.
 */
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';

const CACHE_FILE = join(homedir(), '.openclaw', 'fabric-channel-meta.json');

interface ChannelMetaFile {
  // channelId → xType ('dm' | 'triage' | 'group' | etc.)
  channels: Record<string, string>;
}

let memory = new Map<string, string>();
let loaded = false;
let dirty = false;
let flushTimer: ReturnType<typeof setTimeout> | null = null;

function load(): void {
  if (loaded) return;
  loaded = true;
  try {
    if (!existsSync(CACHE_FILE)) return;
    const raw = readFileSync(CACHE_FILE, 'utf8');
    const parsed = JSON.parse(raw) as ChannelMetaFile;
    for (const [k, v] of Object.entries(parsed.channels ?? {})) {
      if (typeof k === 'string' && typeof v === 'string') memory.set(k, v);
    }
  } catch {
    // ignore — start with empty cache on corruption
  }
}

function scheduleFlush(): void {
  if (flushTimer) return;
  // Debounce writes — many inbound messages may arrive in a burst.
  // 250ms coalesces them; on gateway_stop the channel plugin can force
  // a synchronous flush via flushChannelMeta().
  flushTimer = setTimeout(() => {
    flushTimer = null;
    if (!dirty) return;
    dirty = false;
    flushSync();
  }, 250);
}

function flushSync(): void {
  try {
    const dir = dirname(CACHE_FILE);
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
    const out: ChannelMetaFile = { channels: Object.fromEntries(memory) };
    const tmp = CACHE_FILE + '.tmp';
    writeFileSync(tmp, JSON.stringify(out, null, 2) + '\n', 'utf8');
    renameSync(tmp, CACHE_FILE);
  } catch {
    // swallow — cache is an optimization; loss-on-write is recoverable
  }
}

/** Called by inbound on every message.created. xType empty → no-op. */
export function recordChannelType(channelId: string, xType: string | undefined): void {
  if (!channelId || !xType) return;
  load();
  const existing = memory.get(channelId);
  if (existing === xType) return;
  memory.set(channelId, xType);
  dirty = true;
  scheduleFlush();
}

/** Cross-plugin lookup. null when channel never seen / unknown. */
export function getChannelType(channelId: string): string | null {
  if (!channelId) return null;
  load();
  return memory.get(channelId) ?? null;
}

/** Force-flush — called on plugin shutdown to make sure recently
 *  recorded entries hit disk before the gateway dies. */
export function flushChannelMeta(): void {
  if (flushTimer) {
    clearTimeout(flushTimer);
    flushTimer = null;
  }
  if (dirty) {
    dirty = false;
    flushSync();
  }
}

export const CHANNEL_META_PATH = CACHE_FILE;
