import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";

/** One canonical location of a reusable thing. A snippet that unifies several
 *  one-offs carries several — one per call site. */
export interface SnippetLocation {
  repo: string;
  path: string;
  symbol: string;
}

/** Structured fields parsed out of a snippet note (mirrors the backend
 *  `parse_snippet_fields` — see services/snippets.py). `repo`/`path`/`symbol`
 *  mirror the first location for back-compat; `locations` is the full list. */
export interface SnippetFields {
  name: string;
  when_to_use: string;
  signature: string;
  language: string;
  repo: string;
  path: string;
  symbol: string;
  locations: SnippetLocation[];
  /** The snippets folded into this one by merge, oldest first. Read-only: merge
   *  is the only thing that adds to it, and an edit carries it forward.
   *
   *  Each entry records what THAT source contributed — never what the survivor
   *  already had — which is what lets un-merge subtract exactly. An entry with
   *  no `locations`/`tags` predates that attribution and cannot be un-merged. */
  merged_from: { id: number; locations?: SnippetLocation[]; tags?: string[] }[];
  code: string;
}

/** A full snippet record: the note dict plus the parsed `snippet` sub-object,
 *  as returned by the backend `snippet_to_dict`. */
export interface Snippet {
  id: number;
  title: string;
  body: string;
  tags: string[];
  note_type: string;
  project_id: number | null;
  permission?: string;
  created_at: string;
  updated_at: string;
  snippet: SnippetFields;
  systems?: { id: number; name: string }[];
  /** Set when another user owns this record; `owner` is their username and
   *  `permission` your access level on it. */
  shared?: boolean;
  owner?: string | null;
}

/** How often a record was put in front of an agent versus actually opened.
 *  A high `surfaced_count` with `pull_count: 0` is dead weight — it occupies a
 *  slot in every future auto-inject menu while never being used. */
export interface SnippetUsage {
  surfaced_count: number;
  pull_count: number;
  last_surfaced_at: string | null;
  last_pulled_at: string | null;
}

/** Result of the last drift check — does the recorded location and code still
 *  match source? The check runs agent-side (Scribe has no checkout); this is the
 *  remembered verdict. `current` is false once the snippet has been edited since
 *  the check, at which point the verdict describes code that's no longer there. */
export interface SnippetVerification {
  status: "ok" | "missing" | "moved" | "changed" | "unverified";
  current: boolean;
  checked_at: string | null;
  detail?: string | null;
  path?: string | null;
  needs_attention?: boolean;
}

/** Lightweight list item from the knowledge preview feed. Note: the `snippet`
 *  field here is a truncated *body preview* (the knowledge feed's naming), not
 *  the parsed fields above. */
export interface SnippetListItem {
  id: number;
  title: string;
  tags: string[];
  note_type: string;
  snippet: string;
  created_at: string;
  updated_at: string;
  /** Present only when the record belongs to someone else — a suggestion from
   *  them, not one of your own. Absent means it's yours. */
  shared?: boolean;
  owner?: string | null;
  /** The recorded language, when one was given. Projected from the `data`
   *  mirror so lists can show it without parsing the body — and so a prior-art
   *  hit can be flagged as being in a DIFFERENT language than the file being
   *  written, which is a shape to adapt rather than code to paste. */
  language?: string;
  /** Always present from the backend, zero-filled for records with no events. */
  usage?: SnippetUsage;
  /** Present on the detail record; the list feed carries it when a check has
   *  been recorded. */
  verification?: SnippetVerification;
}

/** Create/update payload — discrete fields the backend serializes into the
 *  note (title/body/tags). Empty strings are applied; omitted keys are left
 *  unchanged on update. */
export interface SnippetInput {
  name: string;
  code: string;
  language?: string;
  signature?: string;
  when_to_use?: string;
  locations?: SnippetLocation[];
  tags?: string[];
  project_id?: number | null;
  system_ids?: number[];
  /** Create only: record it even though a near-duplicate already exists. */
  force?: boolean;
}

/** `repo`/`path`/`symbol` are the reverse lookup — "what already lives here?"
 *  They must all match the same recorded location; `path` also matches anything
 *  beneath it. Combinable with `q`. */
export async function listSnippets(
  params: {
    q?: string;
    tag?: string;
    projectId?: number | null;
    repo?: string;
    path?: string;
    symbol?: string;
    /** Drift check: "attention" | "ok" | "unverified" | "drifted" | a status. */
    verification?: string;
  } = {},
): Promise<{ snippets: SnippetListItem[]; total: number }> {
  const qs = new URLSearchParams();
  if (params.q) qs.set("q", params.q);
  if (params.tag) qs.set("tag", params.tag);
  if (params.projectId) qs.set("project_id", String(params.projectId));
  if (params.repo) qs.set("repo", params.repo);
  if (params.path) qs.set("path", params.path);
  if (params.symbol) qs.set("symbol", params.symbol);
  if (params.verification) qs.set("verification", params.verification);
  const query = qs.toString();
  return apiGet(`/api/snippets${query ? `?${query}` : ""}`);
}

export async function getSnippet(id: number): Promise<Snippet> {
  return apiGet(`/api/snippets/${id}`);
}

export async function createSnippet(data: SnippetInput): Promise<Snippet> {
  return apiPost("/api/snippets", data);
}

export async function updateSnippet(
  id: number,
  data: Partial<SnippetInput>,
): Promise<Snippet> {
  return apiPatch(`/api/snippets/${id}`, data);
}

export async function deleteSnippet(id: number): Promise<void> {
  return apiDelete(`/api/snippets/${id}`);
}

/** A set of snippets that resemble each other closely enough to be worth
 *  merging. Grouping is transitive, so a set can hold members that don't
 *  directly resemble each other — read it as a proposal, not a verdict. */
export interface DuplicateGroup {
  note_ids: number[];
  snippets: { id: number; title: string }[];
  /** The strongest resemblance within the set — how confident the suggestion is. */
  top_score: number;
}

/** Near-duplicates already in the record. The create gate prevents new ones and
 *  merge cures the ones you point it at; this is what finds them. */
export async function findDuplicateSnippets(
  threshold?: number,
): Promise<{ groups: DuplicateGroup[]; threshold: number }> {
  const qs = threshold ? `?threshold=${threshold}` : "";
  return apiGet(`/api/snippets/duplicates${qs}`);
}

/** Record a drift-check verdict. The check itself runs where the code is — an
 *  agent with the working tree — since Scribe has no checkout. This stores what
 *  was found, and is how the UI clears a stale marker after a manual fix. */
export async function verifySnippet(
  id: number,
  verdict: { status: string; detail?: string; path?: string },
): Promise<Snippet> {
  return apiPost(`/api/snippets/${id}/verify`, verdict);
}

/** Pull one source back out of a merged survivor: restores it and strips exactly
 *  what it contributed. Also repairs a half-undone merge — a source restored
 *  from the trash by hand leaves the survivor still claiming its call sites. */
export async function unmergeSnippet(
  survivorId: number,
  sourceId: number,
): Promise<{ survivor: Snippet; restored: Snippet | null }> {
  return apiPost(`/api/snippets/${survivorId}/unmerge`, { source_id: sourceId });
}

/** Unify `sourceIds` into the canonical snippet `targetId`. Returns the merged
 *  survivor plus `merged_ids` — the sources actually folded in and trashed. */
export async function mergeSnippets(
  targetId: number,
  sourceIds: number[],
): Promise<Snippet & { merged_ids: number[] }> {
  return apiPost(`/api/snippets/${targetId}/merge`, { source_ids: sourceIds });
}
