/**
 * Types for the persistent trend store (research-engine §5, foundation layer).
 *
 * The store is the research engine's accumulating memory: a topic-tagged,
 * provenance-bearing inventory of trend signals captured over time, so the
 * engine reasons over HISTORY instead of starting amnesiac each session. It is
 * the trend-side twin of the lived-specifics bank (scripts/specifics-bank):
 * same deterministic store/dedup/query discipline, different dedupe key
 * (normalized title+URL, since a trend is identified by its headline+source,
 * not by free-text content).
 *
 * GENERIC BY ARCHITECTURE (retning §5 — nisje-agnostisk motor): nothing
 * niche-specific lives in this module. A `TrendRecord` carries free-form
 * `topics` tags and a free-form `source` string; which topics matter and which
 * sources to poll are decided upstream (config/profile + the capture agent),
 * never hard-coded here. The same store serves any niche.
 *
 * Forward-compatible with the profile-evolution / "second brain" architecture:
 * a typed store in the per-user data dir (`${LINKEDIN_STUDIO_DATA}`), so the
 * trend history survives plugin upgrades/reinstalls via the M0 data-path seam.
 * The minimal core here (title, url, source, capturedAt, topics, optional
 * summary) can gain fields (first-mover timing, status) in a later slice without
 * breaking the shape — the relevance `score` field (RE-R3a) is the first such
 * realized addition.
 */

import type { TrendScore } from "./score.js";

/**
 * The lifecycle state of a trend. Absent on a record ⇒ "new" (see effectiveStatus).
 * RE-R3b introduced new/acted/skipped; N6 (A1-7) inserts `selected` — the operator's
 * triage pick — so the full arc is new → selected → acted | skipped. `selected` and
 * `acted` are the two "in production" states the morning brief surfaces separately.
 */
export type TrendStatus = "new" | "selected" | "acted" | "skipped";

/**
 * The reader-side utility verdict (N6 / MR-F7, mechanism from `nytteloftet.md`): a
 * candidate is BÆRENDE (carries an edition on its own), STØTTE (supports one), or NYHET
 * (news only — no reader grip yet). A CLOSED mechanism vocabulary — avsender-neutral, so
 * the three names are the mechanism, NOT niche calibration (what counts as a grip lives in
 * the user profile / data dir, never here). The N7 band-cap gate reads verdict+actionability.
 */
export type TrendVerdict = "BÆRENDE" | "STØTTE" | "NYHET";

/**
 * The reader-grip signal (N6 / MR-F7): whether a reader-actionable grip is formulated, plus
 * an optional one-line statement of it. The five relevance dimensions all measure SENDER fit;
 * this is the missing reader side. The N7 band-cap gate caps a candidate whose grip is not
 * formulated (`formulated: false`) to at most `High`, regardless of composite.
 */
export interface Actionability {
  /** Is a reader-actionable grip formulated? The yes/no the N7 gate reads. */
  formulated: boolean;
  /** Optional short statement of the grip (or of why none). */
  note?: string;
}

/**
 * The demand-side signal (N7.5 / MR-F9): the «innenfra og ut» demand-sweep's RANKABLE judgment of
 * a reader question — how strongly it is actually asked (etterspørsel) and whether a good answer is
 * already out there (ikke-besvart). The rankable twin of the verbatim `saturation` text, exactly as
 * `actionability.formulated` (bool) is the rankable twin of its verbatim `note` — one for arithmetic,
 * one for the human. Avsender-neutral mechanism: what counts as demand is the sweep's judgment, never
 * niche calibration hard-coded here. Absent on a record ⇒ UNMEASURED (the honest null — the sweep is
 * allowed to say "thin signal" via `strength: "thin"`, or nothing at all, rather than invent demand).
 */
export interface DemandSignal {
  /**
   * How strongly the question is asked — the sweep's honest ordinal. `"thin"` is a real thin-signal
   * FINDING (measured, and low), distinct from the field being absent (never measured).
   */
  strength: "strong" | "moderate" | "thin";
  /** Is a good answer already out there? `true` ⇒ saturated (the supply gap is closed — don't write). */
  answered: boolean;
}

export interface TrendRecord {
  /** Stable id — a short hash of the normalized title+url; doubles as the dedupe key. */
  id: string;
  /** The trend headline, stored VERBATIM (normalized only to derive the id). */
  title: string;
  /** The source URL, stored VERBATIM (normalized only to derive the id). */
  url: string;
  /** Capture origin: a research-MCP name (e.g. "tavily"), "websearch", or "manual". */
  source: string;
  /** ISO-8601 date the trend was captured. Supplied by the caller (CLI edge). */
  capturedAt: string;
  /**
   * The SOURCE's own publish date (ISO-8601), if the captured item carried one —
   * distinct from `capturedAt` (when WE saw it): this is when the source published.
   * First-sight provenance: kept, never overwritten on re-capture. Forward-compat
   * for B4 freshness ranking. Absent when the source gave no date (key omitted).
   */
  publishedAt?: string;
  /** Topic tags for query-by-topic. Unioned across re-captures of the same trend. */
  topics: string[];
  /** Optional short summary of the trend, stored VERBATIM. */
  summary?: string;
  /**
   * The persisted relevance assessment (RE-R3a): the agent's judgment (mode + the
   * five 1–10 dimension scores) plus the deterministically-derived composite + band,
   * computed once at first sight by the store's single scorer owner. First-sight,
   * never updated on re-capture (re-score pairs with the R3b status slice). Absent on
   * pre-R3a records and on the score-free `add` manual path (key omitted).
   *
   * RE-R3b makes `score` the one MUTABLE field: a re-capture carrying a fresh judgment
   * refreshes it (last-wins, timing decays), via addTrend's duplicate branch.
   */
  score?: TrendScore;
  /**
   * The trend's lifecycle status (RE-R3b). Absent ⇒ "new" (effectiveStatus). Set only by
   * the act/skip/reset CLI verbs, never on capture — a freshly-captured trend is implicitly
   * new. The morning brief excludes anything not "new" (a work queue, not an archive).
   */
  status?: TrendStatus;
  /**
   * The seen-log count (RE-R3b, B4): distinct days this trend has appeared in a generated
   * brief. Absent ⇒ 0. Incremented (per-day-idempotent) by the brief CLI after the pure
   * ranking — the temporal foundation slices (c)+(b) read.
   */
  surfacedCount?: number;
  /** ISO date of the most recent surfacing (RE-R3b). Absent ⇒ never. The per-day idempotency key. */
  lastSurfacedAt?: string;

  // ── N6 proposal layer (A1-4/A1-5): the discovery agent's proposal, persisted (was discarded).
  //    All first-sight (like source/capturedAt) — a re-capture unions topics + refreshes score only.
  /** The proposed article angle — the agent's take, stored VERBATIM. Absent on the score-free `add` path. */
  angle?: string;
  /**
   * The proposed target level, on a span DEFINED BY THE USER PROFILE (e.g. praktiker→beslutter) —
   * a free string, NEVER a hard-coded enum. The plugin owns the field; the profile owns the span's values.
   */
  targetLevel?: string;
  /** Why now / practical applicability — the agent's timeliness rationale, VERBATIM. */
  rationale?: string;
  /** Related trend ids (A1-5, multi-source candidate): other records this trend is a facet of. */
  relatedIds?: string[];

  // ── N6 reader-side (MR-F7): the reader-grip signal the N7 band-cap gate reads. ──
  /** Whether a reader-actionable grip is formulated (+ optional note). See {@link Actionability}. */
  actionability?: Actionability;
  /** The reader-side utility verdict (BÆRENDE/STØTTE/NYHET). See {@link TrendVerdict}. */
  verdict?: TrendVerdict;

  // ── N6 reader-side (MR-F9): schema only here — the demand-sweep that FILLS these is N7.5. ──
  /** The reader's OWN formulation of the question, in the reader's words (not the field's jargon). */
  readerQuestion?: string;
  /** The cost/risk/duty/tool the topic hits for the reader (what it competes against). */
  painPoint?: string;
  /** The market saturation judgment — is this already answered by others? (metningsdom), VERBATIM. */
  saturation?: string;
  /**
   * The demand-sweep's rankable demand signal (N7.5). First-sight like the other N6/F9 fields — a
   * re-capture never clobbers a triaged demand read. Absent ⇒ unmeasured. See {@link DemandSignal}.
   */
  demand?: DemandSignal;
}

export interface TrendStore {
  schemaVersion: number;
  trends: TrendRecord[];
}

/** A query hit: the trend plus how many of the queried topics it matched. */
export interface TrendQueryHit {
  trend: TrendRecord;
  /** Number of queried topics present on the trend (≥1 for a hit). */
  topicOverlap: number;
}

/**
 * The store schema version. Still 4 after N6 AND N7.5: the eight N6 proposal fields
 * (angle/targetLevel/rationale/relatedIds/actionability/verdict/readerQuestion/painPoint/saturation)
 * plus the N7.5 `demand` signal are all additive-optional — a pre-N6 v4 record is already a valid
 * post-N7.5 v4 record that simply lacks them, so NO record needs migrating and a version bump would
 * be a marker with no migration behind it. (Earlier slices bumped because they were the first to add
 * fields at all; the choice is deliberate.)
 */
export const SCHEMA_VERSION = 4;
