import type { RecordUsage } from "@/types/usage";

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

/** How a rule reaches a session (milestone 307). */
export type RuleTier = "always_on" | "conditional";

/**
 * A typed edge between two rules. Each kind exists because its absence forced
 * a workaround: merging two rules into one row, writing an override as a
 * near-copy, or leaving a local addendum with nothing to say it is one.
 */
export type RuleRelationKind = "co_surfaces" | "overrides" | "elaborates";

export interface RuleRelation {
  id: number;
  kind: RuleRelationKind;
  /** The rule at the OTHER end. */
  rule_id: number;
  direction: "outgoing" | "incoming";
  note: string;
}

export interface Rulebook {
  id: number;
  owner_user_id: number;
  title: string;
  description: string;
  always_on: boolean;
  created_at: string | null;
  updated_at: string | null;
}

export interface RulebookTopic {
  id: number;
  rulebook_id: number;
  title: string;
  description: string;
  order_index: number;
  created_at: string | null;
  updated_at: string | null;
}

export interface Rule {
  id: number;
  topic_id: number | null;
  project_id: number | null;
  title: string;
  statement: string;
  /** WHEN this rule fires — the trigger, not the instruction. */
  when_to_apply: string;
  /**
   * always_on preloads into every session; conditional is reachable and
   * surfaced when its trigger fires. A rule with no tier set behaves as
   * always_on, which is how every rule behaved before this existed.
   */
  tier: RuleTier;
  why: string;
  how_to_apply: string;
  /**
   * How to check the rule is still true, and the state that ends it. Set
   * only on a rule that asserts a fact about something outside the
   * operator's control; empty on a rule that is a decision, which is most
   * of them. Empty is meaningful, not missing.
   */
  verify_with: string;
  expires_when: string;
  /** When the check last passed. Null means never checked. */
  verified_at: string | null;
  /** The note or task that caused this rule, if one was recorded. */
  arose_from_id: number | null;
  order_index: number;
  created_at: string | null;
  updated_at: string | null;
  /** Present only when the rule has them (the server omits empty keys). */
  systems?: { id: number; name: string }[];
  relations?: RuleRelation[];
}

/**
 * A rule as a LIST ROW — services.rulebooks.rule_brief's output. Carries the
 * age deliberately: a rule written before the capability it duplicates is
 * otherwise indistinguishable, at a glance, from one still doing work.
 */
export interface RuleHeader {
  id: number;
  title: string;
  statement: string;
  topic_id: number | null;
  tier: RuleTier;
  /** A date (YYYY-MM-DD), not a timestamp. */
  updated_at: string | null;
  when_to_apply?: string;
  arose_from_id?: number;
  /**
   * Present ONLY on a rule that carries a check — the presence of the key
   * is itself the signal that this rule asserts a fact that can go false.
   * A date (YYYY-MM-DD), or the literal "never".
   */
  last_verified?: string;
  /**
   * Surfaced-vs-opened counts from `rule_usage_events` (milestone 333).
   * Zero-filled by the list route, so a rule predating the table reads as
   * "never surfaced" rather than as a missing field — which for a while is
   * every rule on every install.
   */
  usage?: RecordUsage;
}

export interface ApplicableRules {
  // Both lists are rule_brief's output — the SAME builder, so they are
  // described the same way here rather than as two hand-written shapes that
  // drift from it and from each other (which is what the server side had).
  rules: (RuleHeader & {
    topic_title: string;
    rulebook_id: number;
    rulebook_title: string;
  })[];
  project_rules: RuleHeader[];
  suppressed_rules: {
    id: number;
    title: string;
    topic_id: number;
    topic_title: string;
    rulebook_id: number;
    rulebook_title: string;
  }[];
  suppressed_topics: {
    id: number;
    title: string;
    rulebook_id: number;
    rulebook_title: string;
  }[];
  truncated: boolean;
  subscribed_rulebooks: { id: number; title: string }[];
  /** Always-on rulebooks this project opted out of at inception (milestone 297). */
  excluded_always_on: { id: number; title: string }[];
}

// ── Rulebooks ───────────────────────────────────────────────────────

export async function listRulebooks(): Promise<Rulebook[]> {
  const data = await apiGet<{ rulebooks: Rulebook[] }>("/api/rulebooks");
  return data.rulebooks;
}

export async function getRulebook(id: number): Promise<Rulebook & { topics: RulebookTopic[] }> {
  return apiGet(`/api/rulebooks/${id}`);
}

export async function createRulebook(data: { title: string; description?: string }): Promise<Rulebook> {
  return apiPost("/api/rulebooks", data);
}

export async function updateRulebook(id: number, data: Partial<{ title: string; description: string; always_on: boolean }>): Promise<Rulebook> {
  return apiPatch(`/api/rulebooks/${id}`, data);
}

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

// ── Topics ─────────────────────────────────────────────────────────

export async function listTopics(rulebookId: number): Promise<RulebookTopic[]> {
  const data = await apiGet<{ topics: RulebookTopic[] }>(`/api/rulebooks/${rulebookId}/topics`);
  return data.topics;
}

export async function createTopic(rulebookId: number, data: { title: string; description?: string; order_index?: number }): Promise<RulebookTopic> {
  return apiPost(`/api/rulebooks/${rulebookId}/topics`, data);
}

export async function updateTopic(id: number, data: Partial<{ title: string; description: string; order_index: number }>): Promise<RulebookTopic> {
  return apiPatch(`/api/rulebook-topics/${id}`, data);
}

export async function deleteTopic(id: number): Promise<void> {
  return apiDelete(`/api/rulebook-topics/${id}`);
}

// ── Rules ──────────────────────────────────────────────────────────

export async function listRules(filters: { rulebook_id?: number; topic_id?: number; project_id?: number } = {}): Promise<Rule[]> {
  const params = new URLSearchParams();
  if (filters.rulebook_id) params.set("rulebook_id", String(filters.rulebook_id));
  if (filters.topic_id) params.set("topic_id", String(filters.topic_id));
  if (filters.project_id) params.set("project_id", String(filters.project_id));
  const qs = params.toString();
  const data = await apiGet<{ rules: Rule[] }>(`/api/rules${qs ? `?${qs}` : ""}`);
  return data.rules;
}

export async function getRule(id: number): Promise<Rule> {
  return apiGet(`/api/rules/${id}`);
}

/**
 * The fields both write paths accept. `system_ids` REPLACES a rule's areas.
 *
 * Sending "" for a nullable text field CLEARS it here — the server maps an
 * empty string to NULL, so an emptied form input does what it looks like it
 * does. (The MCP door reads "" as "leave unchanged" and needs an explicit
 * clear_fields list instead; the two idioms reach the same state.)
 */
export interface RuleWrite {
  title: string;
  statement: string;
  when_to_apply: string;
  tier: RuleTier;
  why: string;
  how_to_apply: string;
  order_index: number;
  system_ids: number[];
  arose_from_id: number | null;
  verify_with: string;
  expires_when: string;
}

export async function createRule(topicId: number, data: Partial<RuleWrite> & { title: string; statement: string }): Promise<Rule> {
  return apiPost(`/api/rulebook-topics/${topicId}/rules`, data);
}

export async function updateRule(id: number, data: Partial<RuleWrite>): Promise<Rule> {
  return apiPatch(`/api/rules/${id}`, data);
}

/** Draw a typed edge from one rule to another. Idempotent. */
export async function relateRules(
  fromRuleId: number,
  data: { to_rule_id: number; kind: RuleRelationKind; note?: string },
): Promise<{ id: number }> {
  return apiPost(`/api/rules/${fromRuleId}/relations`, data);
}

export async function unrelateRules(relationId: number): Promise<void> {
  return apiDelete(`/api/rule-relations/${relationId}`);
}

/**
 * One entry in a rule's edit history.
 *
 * Each entry holds the text the edit REPLACED, not the text it introduced —
 * so the newest entry is what the rule said before its most recent change,
 * and what that change produced is the rule as it stands now. Read the other
 * way round, every diff comes out backwards.
 *
 * The listing form omits the long fields; open one to get them.
 */
export interface RuleVersion {
  id: number;
  rule_id: number;
  /** Who made the edit. Null when that account has since been deleted. */
  user_id: number | null;
  title: string;
  created_at: string;
  statement?: string;
  why?: string;
  how_to_apply?: string;
  when_to_apply?: string;
  tier?: string;
  verify_with?: string;
  expires_when?: string;
}

export async function listRuleVersions(ruleId: number): Promise<RuleVersion[]> {
  const data = await apiGet<{ versions: RuleVersion[] }>(
    `/api/rules/${ruleId}/versions`,
  );
  return data.versions;
}

export async function getRuleVersion(
  ruleId: number, versionId: number,
): Promise<RuleVersion> {
  return apiGet<RuleVersion>(`/api/rules/${ruleId}/versions/${versionId}`);
}

// No restoreRuleVersion, deliberately (milestone 323). Putting an old wording
// back goes through updateRule, which snapshots what it replaces — so the
// undo stays visible in the history like any other edit.

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

// ── Subscriptions ──────────────────────────────────────────────────

export async function subscribeProject(projectId: number, rulebookId: number): Promise<void> {
  await apiPost(`/api/projects/${projectId}/rulebook-subscriptions`, { rulebook_id: rulebookId });
}

export async function unsubscribeProject(projectId: number, rulebookId: number): Promise<void> {
  return apiDelete(`/api/projects/${projectId}/rulebook-subscriptions/${rulebookId}`);
}

export async function getProjectApplicableRules(projectId: number): Promise<ApplicableRules> {
  return apiGet(`/api/projects/${projectId}/rules`);
}

export async function createProjectRule(
  projectId: number,
  data: Partial<RuleWrite> & { statement: string },
): Promise<Rule> {
  return apiPost(`/api/projects/${projectId}/rules`, data);
}

// ── Suppressions ───────────────────────────────────────────────────

export async function suppressRuleForProject(projectId: number, ruleId: number): Promise<void> {
  await apiPost(`/api/projects/${projectId}/suppressions/rules/${ruleId}`, {});
}

export async function unsuppressRuleForProject(projectId: number, ruleId: number): Promise<void> {
  return apiDelete(`/api/projects/${projectId}/suppressions/rules/${ruleId}`);
}

export async function suppressTopicForProject(projectId: number, topicId: number): Promise<void> {
  await apiPost(`/api/projects/${projectId}/suppressions/topics/${topicId}`, {});
}

export async function unsuppressTopicForProject(projectId: number, topicId: number): Promise<void> {
  return apiDelete(`/api/projects/${projectId}/suppressions/topics/${topicId}`);
}

// ── Always-on exclusions (milestone 297) ────────────────────────────────────

export async function excludeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise<void> {
  await apiPost(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`, {});
}

export async function includeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise<void> {
  await apiDelete(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`);
}


/**
 * One row of the staleness sweep. Unlike RuleHeader this carries the CHECK
 * in full — the reader is about to go and run it, so the text is the point
 * of the payload rather than the bloat a listing avoids.
 */
export interface RuleVerificationRow {
  id: number;
  title: string;
  statement: string;
  tier: RuleTier;
  topic_id: number | null;
  project_id: number | null;
  when_to_apply: string;
  verify_with: string;
  expires_when: string;
  /** A date (YYYY-MM-DD), or the literal "never". */
  last_verified: string | null;
  /** Null when never verified — "never" is not zero days ago. */
  days_since_verified: number | null;
}

/**
 * Rules asserting a fact that may have gone false, oldest verification
 * first, never-checked at the top. Rules without a check never appear:
 * they are decisions, and there is nothing to go and check.
 *
 * Not filterable by project — a project reaches rules through project
 * scope, subscriptions, always-on rulebooks and exclusions, and a filter
 * missing one of those paths would under-report.
 */
export async function listRulesDueForVerification(opts: {
  olderThanDays?: number;
  tier?: RuleTier;
  neverOnly?: boolean;
} = {}): Promise<{ rules: RuleVerificationRow[]; total: number }> {
  const q = new URLSearchParams();
  if (opts.olderThanDays) q.set("older_than_days", String(opts.olderThanDays));
  if (opts.tier) q.set("tier", opts.tier);
  if (opts.neverOnly) q.set("never_only", "true");
  const qs = q.toString();
  return apiGet(`/api/rules-due-for-verification${qs ? `?${qs}` : ""}`);
}

/**
 * Record that a rule's check was RUN, and what it said.
 *
 * `stillTrue: false` writes nothing on purpose — a rule whose check failed
 * is not in a recordable state, it is wrong — so it stays at the top of the
 * sweep until someone corrects or retires it.
 */
export async function markRuleVerified(
  id: number, stillTrue = true,
): Promise<Rule & { verified: boolean }> {
  return apiPost(`/api/rules/${id}/verify`, { still_true: stillTrue });
}
