/**
 * Persistent cache of threads surfaced by the discovery agent.
 *
 * Why this exists: a single failed scan-now (rate limit, MCP server down,
 * model returning empty JSON) used to wipe Activity. Now discovery is
 * additive: each run upserts items into this cache; nothing is silently
 * deleted. /api/now reads from MEMORY scan + this cache + manual_tasks.
 *
 * Cache map keys: synthetic `file` paths (NOT the full itemKey). The agent
 * paraphrases the same Linear issue differently across runs, so a cache
 * keyed by `<file>:<line>|<text-80>` would accumulate near-duplicates of
 * the same thread. Synthesized file paths are stable per source+location:
 *   external/linear/AWS-456
 *   external/notion/<page-id>
 *   external/m365/outlook/<message-id>
 *   external/posthog/<entity-id>
 *   external/claude-output/<basename>      (agent-surfaced files outside MEMORY)
 *   external/recent-chats/<session-id>
 *
 * The full itemKey() is still used by /api/now → resolve / postpone /
 * check-history. We preserve `text` from the first observation so that
 * itemKey stays stable across runs even if the agent rewords things.
 */
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { DISCOVERED_THREADS_FILE } from "./paths";
import type { NowItem } from "./now";

export type DiscoveredThread = NowItem & {
  /** First time this thread was surfaced by any discovery run. */
  firstSeenAt: string;
  /** Most recent run that surfaced this thread. */
  lastSeenAt: string;
  /** External pointer (URL, Linear identifier, Notion page id, file path). */
  location?: string;
  /** Computed weight from the EXPERIMENT(weighting) ranking pass — see
   *  scan-discovery.ts top-of-file experiment block for the full plan.
   *  Phantom today (declared, never written). Phase 2 of the experiment
   *  wires it through end-to-end with uniform coefficients. */
  weight?: number;
};

/**
 * Calendar-day age threshold. An item whose `lastSeenAt` is older than this
 * gets `stale: true` in /api/now (rendered dimmed). Cleanup happens manually
 * via resolve / postpone — never auto-deleted.
 *
 * Note: this is calendar days, not "N consecutive runs" — discovery only
 * runs on user-triggered scans, so days-since is the meaningful metric.
 */
export const STALE_AFTER_DAYS = 14;

export function loadDiscoveredThreads(): Record<string, DiscoveredThread> {
  if (!existsSync(DISCOVERED_THREADS_FILE)) return {};
  try {
    const data = JSON.parse(readFileSync(DISCOVERED_THREADS_FILE, "utf8"));
    if (data && typeof data === "object") return data as Record<string, DiscoveredThread>;
  } catch (err) {
    console.error("[discovered-threads] read failed:", err);
  }
  return {};
}

function persist(map: Record<string, DiscoveredThread>) {
  try {
    mkdirSync(dirname(DISCOVERED_THREADS_FILE), { recursive: true });
    writeFileSync(DISCOVERED_THREADS_FILE, JSON.stringify(map, null, 2));
  } catch (err) {
    console.error("[discovered-threads] write failed:", err);
  }
}

/**
 * Upsert a batch of items from a discovery run. Keyed by `file` (synthetic
 * source/location path). Existing entries: bump lastSeenAt, update kind +
 * daysOff + date (these can change as deadlines pass), but PRESERVE the
 * canonical text + line from the first observation. That keeps itemKey()
 * stable across runs so resolve / postpone / check-history match.
 */
export function upsertDiscovered(items: DiscoveredThread[]): {
  added: number;
  refreshed: number;
} {
  const map = loadDiscoveredThreads();
  const now = new Date().toISOString();
  let added = 0;
  let refreshed = 0;
  for (const it of items) {
    const key = it.file;
    const existing = map[key];
    if (existing) {
      map[key] = {
        ...existing,
        kind: it.kind,
        source: it.source,
        date: it.date,
        daysOff: it.daysOff,
        location: it.location ?? existing.location,
        lastSeenAt: now,
        // Intentionally NOT updating text/line — preserves itemKey stability.
      };
      refreshed++;
    } else {
      map[key] = { ...it, firstSeenAt: now, lastSeenAt: now };
      added++;
    }
  }
  persist(map);
  return { added, refreshed };
}

/** All cached discovered threads as NowItem-shaped entries (with extras). */
export function listDiscovered(): DiscoveredThread[] {
  return Object.values(loadDiscoveredThreads());
}

/** True when an entry's lastSeenAt is older than STALE_AFTER_DAYS. */
export function isDiscoveredStale(d: DiscoveredThread): boolean {
  const t = Date.parse(d.lastSeenAt);
  if (isNaN(t)) return false;
  return t < Date.now() - STALE_AFTER_DAYS * 86_400_000;
}
