"use client";

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { TransactionRow, StatementRow, TagRow, TripRow, TripAnalytics, ParticipantBalance } from "./queries";
export type { TripRow, TripAnalytics };
import type { CurrentUser } from "./auth";

interface TransactionsResponse {
  data: TransactionRow[];
  total: number;
  limit: number;
  offset: number;
}

interface TransactionFilters {
  from?: string;
  to?: string;
  categories?: string[];
  exclude_categories?: string[];
  bank_names?: string[];
  tag_ids?: string[];
  transaction_types?: string[];
  search?: string;
  statement_id?: string;
  sort_by?: string;
  sort_dir?: string;
  limit?: number;
  offset?: number;
  amount_min?: number;
  amount_max?: number;
  has_split?: string;
  trip_id?: string;
  /** Only the trip detail view sets this — see the note on the server-side filter. */
  trip_all_rows?: boolean;
}

function buildParams(filters: TransactionFilters): string {
  const params = new URLSearchParams();
  Object.entries(filters).forEach(([key, val]) => {
    if (val === undefined || val === "") return;
    if (Array.isArray(val)) {
      if (val.length > 0) params.set(key, val.join(","));
    } else if (typeof val === "boolean") {
      // The route reads "1", not "true" — String(true) would silently not match.
      if (val) params.set(key, "1");
    } else {
      params.set(key, String(val));
    }
  });
  return params.toString();
}

export function useTransactions(filters: TransactionFilters) {
  return useQuery<TransactionsResponse>({
    queryKey: ["transactions", filters],
    queryFn: async () => {
      const res = await fetch(`/api/transactions?${buildParams(filters)}`);
      return res.json();
    },
  });
}

export function useTransaction(id: number) {
  return useQuery<TransactionRow>({
    queryKey: ["transaction", id],
    queryFn: async () => {
      const res = await fetch(`/api/transactions/${id}`);
      return res.json();
    },
  });
}

export function useStatements() {
  return useQuery<StatementRow[]>({
    queryKey: ["statements"],
    queryFn: async () => {
      const res = await fetch("/api/statements");
      return res.json();
    },
  });
}

export function useStatement(id: number) {
  return useQuery<StatementRow>({
    queryKey: ["statement", id],
    queryFn: async () => {
      const res = await fetch(`/api/statements/${id}`);
      return res.json();
    },
  });
}

export function useBanks() {
  return useQuery<string[]>({
    queryKey: ["banks"],
    queryFn: async () => {
      const res = await fetch("/api/merchants?type=banks");
      return res.json();
    },
  });
}

export function useCreateTransaction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (data: {
      date: string;
      description: string;
      amount: number;
      transaction_type?: string;
      merchant_normalized?: string;
      category?: string;
      payment_method?: string;
      splits?: { participant_id: number; share_percent: number }[];
    }) => {
      const res = await fetch("/api/transactions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Failed to create transaction");
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["splits"] });
      qc.invalidateQueries({ queryKey: ["shared-transactions"] });
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
    },
  });
}

export function useUpdateTransaction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({
      id,
      ...data
    }: {
      id: number;
      category?: string;
      merchant_normalized?: string;
      notes?: string;
      transaction_type?: string;
      my_share_percent?: number | null;
      description?: string;
      amount?: number;
      transaction_date?: string;
      trip_id?: number | null;
      payment_method?: string | null;
      owner_id?: number;
      release?: boolean;
    }) => {
      const res = await fetch(`/api/transactions/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      // Without this every rejection resolved as success: the modal closed, the
      // list refetched, and the edit silently vanished. The owner guard returns
      // 409 with a `code`, so the caller needs both the message and the code.
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw Object.assign(new Error(body.error || "Failed to update transaction"), {
          code: body.code as string | undefined,
        });
      }
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["transaction"] });
      qc.invalidateQueries({ queryKey: ["analytics"] });
      // An owner change flips which side of a balance the splits sit on.
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
    },
  });
}

export function useBulkAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (body: {
      action: string;
      ids: number[];
      category?: string;
      merchant_normalized?: string;
      splits?: { participant_id: number; share_percent: number }[];
      tag_id?: number;
      rule_id?: number;
    }) => {
      const res = await fetch("/api/transactions/bulk", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Bulk action failed");
      }
      return res.json();
    },
    onSuccess: (_data, variables) => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      if (variables.action === "split") {
        qc.invalidateQueries({ queryKey: ["splits"] });
        qc.invalidateQueries({ queryKey: ["shared-transactions"] });
        qc.invalidateQueries({ queryKey: ["participant-balances"] });
      }
      if (variables.action === "tag" || variables.action === "untag") {
        qc.invalidateQueries({ queryKey: ["tags"] });
      }
      // A quick action can touch category, tags and splits at once, and records
      // a revertible run — refresh everything it could have changed.
      if (variables.action === "apply_rule") {
        qc.invalidateQueries({ queryKey: ["tags"] });
        qc.invalidateQueries({ queryKey: ["splits"] });
        qc.invalidateQueries({ queryKey: ["shared-transactions"] });
        qc.invalidateQueries({ queryKey: ["participant-balances"] });
        qc.invalidateQueries({ queryKey: ["analytics"] });
        qc.invalidateQueries({ queryKey: ["rule-runs"] });
      }
    },
  });
}

export function useParticipants() {
  return useQuery<{ id: number; name: string; created_at: string }[]>({
    queryKey: ["participants"],
    queryFn: async () => {
      const res = await fetch("/api/participants");
      return res.json();
    },
  });
}

export function useParticipantBalances(tagIds?: string[]) {
  return useQuery<ParticipantBalance[]>({
    queryKey: ["participant-balances", tagIds],
    queryFn: async () => {
      const params = tagIds?.length ? `?tag_ids=${tagIds.join(",")}` : "";
      const res = await fetch(`/api/participants/balances${params}`);
      return res.json();
    },
  });
}

export function useSharedTransactions(tagIds?: string[], participantId?: number) {
  return useQuery({
    queryKey: ["shared-transactions", tagIds, participantId],
    queryFn: async () => {
      const sp = new URLSearchParams();
      if (tagIds?.length) sp.set("tag_ids", tagIds.join(","));
      if (participantId) sp.set("participant_id", String(participantId));
      const query = sp.toString() ? `?${sp.toString()}` : "";
      const res = await fetch(`/api/shared-transactions${query}`);
      return res.json();
    },
  });
}

export function useTransactionSplits(transactionId: number) {
  return useQuery({
    queryKey: ["splits", transactionId],
    queryFn: async () => {
      const res = await fetch(`/api/transactions/${transactionId}/splits`);
      return res.json();
    },
  });
}

export interface OrderReceipt {
  /** 'email' | 'receipt-scan' | 'order-bridge' | ... — which lane wrote the row. */
  source: string | null;
  platform: string | null;
  order_reference: string | null;
  line_items: { qty: number; description: string; amount: number; options?: string[] }[];
  route: { label: string; time: string | null; address: string }[];
  subtotal: string | null;
  amount: string | null;
  currency: string | null;
  card_last4: string | null;
  flags: string[];
  source_email_subject: string | null;
  transaction_date: string | null;
}

/** The receipt behind a transaction, or null when it did not come from one. */
export function useOrderReceipt(transactionId: number) {
  return useQuery<OrderReceipt | null>({
    queryKey: ["order-receipt", transactionId],
    queryFn: async () => {
      const res = await fetch(`/api/transactions/${transactionId}/order`);
      if (!res.ok) return null;
      return res.json();
    },
    staleTime: Infinity, // a receipt never changes
  });
}

export type OrderRating = "loved" | "liked" | "ok" | "bad" | "never";
export type ItemVerdict = "loved" | "never";

export interface ItemOpinion {
  item: string;
  verdict: ItemVerdict;
}

export interface OrderReviewRow {
  transaction_id: number;
  participant_id: number;
  participant_name: string;
  rating: OrderRating | null;
  order_again: boolean | null;
  note: string | null;
  item_verdicts: ItemOpinion[];
}

export interface OrderReviewState {
  /** One row per person who has an opinion. Empty until someone records one. */
  reviews: OrderReviewRow[];
  /** Current splits — an empty list means the order was not shared. */
  splits: { participant_id: number; share_percent: string }[];
  merchant: {
    merchant: string;
    history: {
      transaction_id: number;
      participant_id: number;
      participant_name: string;
      rating: OrderRating | null;
      note: string | null;
      transaction_date: string | null;
    }[];
    counts: Record<OrderRating, number>;
    warn: boolean;
    items: { item: string; loved: number; never: number }[];
  } | null;
}

/**
 * The verdict on an order and this merchant's track record.
 *
 * No `staleTime: Infinity` here, unlike the receipt hook next to it — a receipt
 * never changes, but a verdict is the one part of an order that does.
 */
export function useOrderReview(transactionId: number) {
  return useQuery<OrderReviewState>({
    queryKey: ["order-review", transactionId],
    queryFn: async () => {
      const res = await fetch(`/api/transactions/${transactionId}/review`);
      if (!res.ok) return { reviews: [], splits: [], merchant: null };
      return res.json();
    },
  });
}

export function useSetOrderReview() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({
      transactionId,
      participantId,
      rating,
      note,
      itemVerdicts,
    }: {
      transactionId: number;
      participantId: number;
      rating: OrderRating | null;
      note?: string | null;
      /** Omit to leave existing item opinions untouched. */
      itemVerdicts?: ItemOpinion[];
    }) => {
      const res = await fetch(`/api/transactions/${transactionId}/review`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          participant_id: participantId,
          rating,
          note,
          ...(itemVerdicts === undefined ? {} : { item_verdicts: itemVerdicts }),
        }),
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Failed to save verdict");
      }
      return res.json();
    },
    // Every order from the same merchant now shows a different track record,
    // so invalidate the whole key rather than this one transaction.
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["order-review"] });
    },
  });
}

export function useSetSplits() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({
      transactionId,
      splits,
    }: {
      transactionId: number;
      splits: { participant_id: number; share_percent: number }[];
    }) => {
      const res = await fetch(`/api/transactions/${transactionId}/splits`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ splits }),
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Failed to set splits");
      }
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["splits"] });
      qc.invalidateQueries({ queryKey: ["shared-transactions"] });
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
      // The order panel shows share state from this same data.
      qc.invalidateQueries({ queryKey: ["order-review"] });
    },
  });
}

/**
 * Remove every split from a transaction — the un-share half of a toggle.
 *
 * Separate from `useSetSplits` because that endpoint requires a set of shares
 * totalling 100%, and "no split at all" is not a set of shares. Posting `[]` to
 * it was rejected, which is why the order panel's toggle could not be turned
 * off.
 */
export function useClearSplits() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (transactionId: number) => {
      const res = await fetch(`/api/transactions/${transactionId}/splits`, {
        method: "DELETE",
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Failed to clear splits");
      }
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["splits"] });
      qc.invalidateQueries({ queryKey: ["shared-transactions"] });
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
      qc.invalidateQueries({ queryKey: ["order-review"] });
    },
  });
}

export interface SplitPayment {
  id: number;
  from_participant_id: number;
  from_name: string;
  to_participant_id: number;
  to_name: string;
  amount: number;
  payment_date: string;
  notes: string | null;
  linked_transaction_id: number | null;
  /** Which tab this payment settles. null = the ongoing household tab. */
  trip_id: number | null;
  trip_name: string | null;
  created_at: string;
}

export function usePaymentHistory(participantId: number | null) {
  return useQuery<SplitPayment[]>({
    queryKey: ["split-payments", participantId],
    queryFn: async () => {
      if (!participantId) return [];
      const res = await fetch(`/api/split-payments?participant_id=${participantId}`);
      return res.json();
    },
    enabled: !!participantId,
  });
}

export function useRecordPayment() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (body: {
      from_participant_id: number;
      to_participant_id: number;
      amount: number;
      payment_date: string;
      notes?: string;
      linked_transaction_id?: number;
      /** Which tab this settles. null/omitted = the ongoing household tab. */
      trip_id?: number | null;
    }) => {
      const res = await fetch("/api/split-payments", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || "Failed to record payment");
      }
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
      qc.invalidateQueries({ queryKey: ["split-payments"] });
      // A trip-scoped payment changes that trip's owed figures too.
      qc.invalidateQueries({ queryKey: ["trip-analytics"] });
    },
  });
}

export function useDeletePayment() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (id: number) => {
      const res = await fetch(`/api/split-payments?id=${id}`, { method: "DELETE" });
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
      qc.invalidateQueries({ queryKey: ["split-payments"] });
    },
  });
}

export function useCurrentUser() {
  return useQuery<CurrentUser>({
    queryKey: ["me"],
    queryFn: async () => {
      const res = await fetch("/api/me");
      if (!res.ok) throw new Error("Not authenticated");
      return res.json();
    },
  });
}

export function useUpdateStatement() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, owner_id }: { id: number; owner_id: number }) => {
      const res = await fetch(`/api/statements/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ owner_id }),
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body.error || "Failed to reassign statement");
      }
      return res.json() as Promise<{ statement_id: number; rows_moved: number }>;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["statements"] });
      qc.invalidateQueries({ queryKey: ["transactions"] });
      // The rows moved with it, so every per-person total changed.
      qc.invalidateQueries({ queryKey: ["analytics"] });
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
    },
  });
}

export function useTags() {
  return useQuery<(TagRow & { transaction_count: number })[]>({
    queryKey: ["tags"],
    queryFn: async () => {
      const res = await fetch("/api/tags");
      return res.json();
    },
  });
}

export function useCreateTag() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ name, color }: { name: string; color?: string }) => {
      const res = await fetch("/api/tags", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, color }),
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Failed to create tag");
      }
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["tags"] }),
  });
}

export function useDeleteTag() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (id: number) => {
      await fetch(`/api/tags/${id}`, { method: "DELETE" });
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["tags"] });
      qc.invalidateQueries({ queryKey: ["transactions"] });
    },
  });
}

export function useAddTransactionTag() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ transactionId, tagId }: { transactionId: number; tagId: number }) => {
      await fetch(`/api/transactions/${transactionId}/tags`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ tag_id: tagId }),
      });
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["transactions"] }),
  });
}

export function useRemoveTransactionTag() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ transactionId, tagId }: { transactionId: number; tagId: number }) => {
      await fetch(`/api/transactions/${transactionId}/tags`, {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ tag_id: tagId }),
      });
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["transactions"] }),
  });
}

export function useCreateParticipant() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ name, email }: { name: string; email?: string }) => {
      const res = await fetch("/api/participants", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email }),
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error || "Failed to create participant");
      }
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["participants"] });
      qc.invalidateQueries({ queryKey: ["participant-balances"] });
    },
  });
}

// --- Rules ---

export interface RuleRow {
  id: number;
  name: string;
  conditions: { field: string; operator: string; value: string }[];
  actions: { set_category?: string; add_tag_ids?: number[]; set_merchant?: string; apply_split?: { participant_id: number; share_percent: number }[] };
  enabled: boolean;
  /** Excluded from the apply-all run; fired by hand as a quick action instead. */
  manual_only?: boolean;
  priority: number;
  created_at: string;
}

export function useRules() {
  return useQuery<RuleRow[]>({
    queryKey: ["rules"],
    queryFn: async () => {
      const res = await fetch("/api/rules");
      return res.json();
    },
  });
}

export function useCreateRule() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (data: Omit<RuleRow, "id" | "created_at">) => {
      const res = await fetch("/api/rules", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error("Failed to create rule");
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["rules"] }),
  });
}

export function useUpdateRule() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, ...data }: Partial<RuleRow> & { id: number }) => {
      const res = await fetch(`/api/rules/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error("Failed to update rule");
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["rules"] }),
  });
}

export function useDeleteRule() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (id: number) => {
      await fetch(`/api/rules/${id}`, { method: "DELETE" });
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["rules"] }),
  });
}

export interface RuleMatchChange {
  field: "category" | "merchant" | "tags" | "split";
  from: string | null;
  to: string;
}
export interface RuleMatchRow {
  id: number;
  transaction_date: string;
  description: string;
  amount: number;
  amount_aud: number | null;
  currency: string;
  bank_name: string;
  effective_merchant: string;
  effective_category: string;
  changes: RuleMatchChange[];
}
export interface RuleMatches {
  rule: { id: number; name: string; matches_everything: boolean };
  total_matched: number;
  would_change: number;
  already_correct: number;
  transactions: RuleMatchRow[];
  truncated: boolean;
}

/** Dry run — what a rule would change. Writes nothing. */
export function useRuleMatches(ruleId: number | null) {
  return useQuery<RuleMatches>({
    queryKey: ["rule-matches", ruleId],
    enabled: ruleId != null,
    queryFn: async () => {
      const res = await fetch(`/api/rules/${ruleId}/matches`);
      if (!res.ok) throw new Error("Failed to load rule matches");
      return res.json();
    },
  });
}

/** Apply a rule to a hand-picked set of transactions (conditions ignored). */
export function useApplyRuleToSelection() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ ruleId, ids }: { ruleId: number; ids: number[] }) => {
      const res = await fetch("/api/transactions/bulk", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "apply_rule", rule_id: ruleId, ids }),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Failed to apply rule");
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["rule-matches"] });
      qc.invalidateQueries({ queryKey: ["rules"] });
    },
  });
}

export function useApplyRules() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (args?: { splitFrom?: string; ruleId?: number }) => {
      const res = await fetch("/api/rules/apply", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ splitFrom: args?.splitFrom || null, ruleId: args?.ruleId || null }),
      });
      if (!res.ok) throw new Error("Failed to apply rules");
      return res.json() as Promise<{ id: number; matched: number; transactions_affected: number }>;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["rules"] });
      qc.invalidateQueries({ queryKey: ["rule-runs"] });
    },
  });
}

export interface RuleRun {
  id: number;
  applied_at: string;
  split_from: string | null;
  matched: number;
  transactions_affected: number;
  reverted_at: string | null;
  // Provenance (migration 0017). NULL on runs recorded before it existed.
  rule_id: number | null;
  rule_name: string | null;
  source: "all" | "rule" | "selection" | null;
}

export interface RuleRunDetail {
  run: RuleRun & { transactions_affected: number };
  transactions: {
    id: number;
    transaction_date: string;
    description: string;
    amount: number;
    amount_aud: number | null;
    bank_name: string;
    merchant: string | null;
    changes: { field: string; from: string | null; to: string | null }[];
  }[];
  still_changed: number;
}

/** What a run actually did — loaded on demand when a row is expanded. */
export function useRuleRunDetail(runId: number | null) {
  return useQuery<RuleRunDetail>({
    queryKey: ["rule-run-detail", runId],
    enabled: runId != null,
    queryFn: async () => {
      const res = await fetch(`/api/rules/runs/${runId}`);
      if (!res.ok) throw new Error("Failed to load run detail");
      return res.json();
    },
  });
}

export function useRuleRuns() {
  return useQuery({
    queryKey: ["rule-runs"],
    queryFn: async () => {
      const res = await fetch("/api/rules/apply");
      if (!res.ok) throw new Error("Failed to fetch rule runs");
      return res.json() as Promise<RuleRun[]>;
    },
  });
}

export function useRevertRuleRun() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (runId: number) => {
      const res = await fetch(`/api/rules/apply/${runId}/revert`, { method: "POST" });
      if (!res.ok) throw new Error("Failed to revert run");
      return res.json() as Promise<{ reverted: number }>;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["rule-runs"] });
    },
  });
}

// --- Budgets & Analytics ---

export interface BudgetRow {
  id: number;
  category: string;
  month: string;
  amount_limit: number;
}

export interface MonthlyAnalyticsRow {
  category: string;
  spent: Record<string, number>;
  budget: Record<string, number>;
  txCount: Record<string, number>;
}

export interface MonthlyAnalytics {
  months: string[];
  rows: MonthlyAnalyticsRow[];
  income: Record<string, number>;
  investments: Record<string, number>;
  totals: Record<string, { spent: number; income: number; investments: number; net: number }>;
}

export function useBudgets(month: string) {
  return useQuery<BudgetRow[]>({
    queryKey: ["budgets", month],
    queryFn: async () => {
      const res = await fetch(`/api/budgets?month=${month}`);
      return res.json();
    },
  });
}

export function useUpsertBudget() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (data: { category: string; month: string; amount_limit: number }) => {
      const res = await fetch("/api/budgets", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error("Failed to save budget");
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["budgets"] }),
  });
}

export function useDeleteBudget() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (id: number) => {
      await fetch(`/api/budgets/${id}`, { method: "DELETE" });
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["budgets"] }),
  });
}

export function useMonthlyAnalytics(months?: number) {
  const m = months || 6;
  return useQuery<MonthlyAnalytics>({
    queryKey: ["analytics", "monthly", m],
    queryFn: async () => {
      const res = await fetch(`/api/analytics/monthly?months=${m}`);
      return res.json();
    },
  });
}

/**
 * Sparse by day-of-month; a missing day means zero.
 *   daily      → { "2026-07": { 3: 42.10 } }
 *   byCategory → { "2026-07": { dining: { 3: 42.10 } } }
 */
export interface DailySpend {
  daily: Record<string, Record<number, number>>;
  byCategory: Record<string, Record<string, Record<number, number>>>;
}

export function useDailySpend(months?: number) {
  const m = months || 12;
  return useQuery<DailySpend>({
    queryKey: ["analytics", "daily", m],
    queryFn: async () => {
      const res = await fetch(`/api/analytics/daily?months=${m}`);
      return res.json();
    },
  });
}

export interface SubscriptionRow {
  merchant: string;
  category: string;
  frequency: string;
  avg_amount: number;
  monthly_equiv: number;
  first_seen: string;
  last_seen: string;
  occurrences: number;
  total_paid: number;
  is_active: boolean;
}

export function useSubscriptions() {
  return useQuery<{ subscriptions: SubscriptionRow[]; total_monthly_equiv: number }>({
    queryKey: ["analytics", "subscriptions"],
    queryFn: async () => {
      const res = await fetch("/api/analytics/subscriptions");
      return res.json();
    },
  });
}

export interface FeeBankRow {
  bank_name: string;
  fees: number;
  interest: number;
  total: number;
}

export interface FeeTxnRow {
  id: number;
  transaction_date: string;
  description: string;
  merchant_name: string | null;
  transaction_type: string;
  my_amount: number;
  bank_name: string;
}

export interface FeePeriod {
  months: number;
  from: string | null;
  to: string | null;
  all_time: boolean;
}

/** `months = 0` means all time. */
export function useFees(months = 12) {
  return useQuery<{
    by_bank: FeeBankRow[];
    transactions: FeeTxnRow[];
    total_fees: number;
    total_interest: number;
    period: FeePeriod;
  }>({
    queryKey: ["analytics", "fees", months],
    queryFn: async () => {
      const res = await fetch(`/api/analytics/fees?months=${months}`);
      return res.json();
    },
  });
}

export interface MerchantRow {
  merchant: string;
  category: string;
  debit_count: number;
  refund_count: number;
  gross_spend: number;
  total_refunds: number;
  net_spend: number;
  avg_debit: number;
  first_seen: string;
  last_seen: string;
  months_active: number;
  monthly_trend: Record<string, number>;
}

export function useMerchants(months = 12) {
  return useQuery<{ merchants: MerchantRow[]; months: number }>({
    queryKey: ["analytics", "merchants", months],
    queryFn: async () => {
      const res = await fetch(`/api/analytics/merchants?months=${months}`);
      return res.json();
    },
  });
}

export interface MerchantTxnRow {
  id: number;
  transaction_date: string;
  description: string;
  amount: number;
  amount_aud: number | null;
  my_amount: number;
  transaction_type: string;
  category: string;
  bank_name: string;
  statement_id: number;
}

// --- CSV Import & Reconcile ---

export function useImportCSV() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (body: {
      bank_name: string;
      source?: string;
      transactions: {
        date: string; description: string; amount: number; transaction_type: string;
        merchant_name?: string; foreign_currency_amount?: number; foreign_currency_code?: string;
        category?: string; account?: string; source_ref?: string;
      }[];
    }) => {
      const res = await fetch("/api/import/csv", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Import failed");
      return res.json() as Promise<{ inserted: number }>;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["tags"] });
      qc.invalidateQueries({ queryKey: ["reconcile-pending"] });
    },
  });
}

import type { ManualTxWithMatches } from "./queries";

export function usePendingReconciliations() {
  return useQuery<ManualTxWithMatches[]>({
    queryKey: ["reconcile-pending"],
    queryFn: async () => {
      const res = await fetch("/api/reconcile/pending");
      if (!res.ok) throw new Error("Failed to fetch");
      return res.json();
    },
  });
}

export function useReconcile() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (matches: { manual_id: number; statement_tx_id: number }[]) => {
      const res = await fetch("/api/transactions/reconcile", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ matches }),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Reconcile failed");
      return res.json() as Promise<{ reconciled: number }>;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["reconcile-pending"] });
    },
  });
}

export function useMerchantTransactions(merchant: string | null) {
  return useQuery<{ transactions: MerchantTxnRow[] }>({
    queryKey: ["analytics", "merchant-txns", merchant],
    queryFn: async () => {
      const res = await fetch(`/api/analytics/merchants/${encodeURIComponent(merchant!)}`);
      return res.json();
    },
    enabled: !!merchant,
  });
}

// ─── Trips ───────────────────────────────────────────────────────────────────

export function useTrips() {
  return useQuery<TripRow[]>({
    queryKey: ["trips"],
    queryFn: async () => (await fetch("/api/trips")).json(),
  });
}

export function useTrip(id: number) {
  return useQuery<TripRow>({
    queryKey: ["trip", id],
    queryFn: async () => (await fetch(`/api/trips/${id}`)).json(),
    enabled: id > 0,
  });
}

export function useTripAnalytics(id: number) {
  return useQuery<TripAnalytics>({
    queryKey: ["trip-analytics", id],
    queryFn: async () => (await fetch(`/api/trips/${id}/analytics`)).json(),
    enabled: id > 0,
  });
}

export function useCreateTrip() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (data: Omit<TripRow, "id" | "owner_id" | "created_at" | "total_spend" | "transaction_count">) => {
      const res = await fetch("/api/trips", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Failed");
      return res.json() as Promise<TripRow>;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["trips"] }),
  });
}

export function useUpdateTrip() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, ...data }: Partial<TripRow> & { id: number }) => {
      const res = await fetch(`/api/trips/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error((await res.json()).error || "Failed");
      return res.json() as Promise<TripRow>;
    },
    onSuccess: (_d, { id }) => {
      qc.invalidateQueries({ queryKey: ["trips"] });
      qc.invalidateQueries({ queryKey: ["trip", id] });
      qc.invalidateQueries({ queryKey: ["trip-analytics", id] });
    },
  });
}

export function useDeleteTrip() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (id: number) => {
      await fetch(`/api/trips/${id}`, { method: "DELETE" });
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ["trips"] }),
  });
}

export function useAssignTransactionsToTrip() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ tripId, transactionIds }: { tripId: number | null; transactionIds: number[] }) => {
      const res = await fetch("/api/transactions/bulk", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "assign_trip", ids: transactionIds, trip_id: tripId }),
      });
      if (!res.ok) throw new Error("Failed to assign trip");
      return res.json();
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["transactions"] });
      qc.invalidateQueries({ queryKey: ["trips"] });
      qc.invalidateQueries({ queryKey: ["trip-analytics"] });
    },
  });
}

// ---------------------------------------------------------------- orders --
// Browse surface over the ingestion spine. See lib/order-feed.ts for why the
// spine is read raw and why /orders is gated by an explicit viewer allowlist.

import type { OrderRow, OrderFacets, OrderDetail, OrderFilters } from "./order-feed";
export type { OrderRow, OrderFacets, OrderDetail, OrderFilters };

interface OrdersResponse {
  data: OrderRow[];
  total: number;
  limit: number;
  offset: number;
  facets: OrderFacets;
}

export function useOrders(filters: Record<string, unknown>) {
  return useQuery<OrdersResponse>({
    queryKey: ["orders", filters],
    queryFn: async () => {
      const res = await fetch(`/api/orders?${buildParams(filters as never)}`);
      if (!res.ok) throw new Error("Failed to load orders");
      return res.json();
    },
  });
}

export function useOrderDetail(entityKey: string | null) {
  return useQuery<OrderDetail>({
    queryKey: ["order", entityKey],
    enabled: Boolean(entityKey),
    // An order's history does not change while you are looking at it; the
    // lifecycle only moves when the ingestion runner ticks.
    staleTime: 60_000,
    queryFn: async () => {
      const res = await fetch(`/api/orders/${encodeURIComponent(entityKey!)}`);
      if (!res.ok) {
        // Carry the status. Collapsing every failure into one Error is how a
        // 500 on this route rendered as "that order could not be found" on
        // every single order for weeks — a server fault wearing the costume of
        // a data condition, which reads as "the spine is empty" and gets
        // investigated nowhere near the actual bug.
        const err = new Error(res.status === 404 ? "Order not found" : "Failed to load order");
        (err as Error & { status?: number }).status = res.status;
        throw err;
      }
      return res.json();
    },
  });
}

/**
 * How far each account's statements already reach, for the CSV import modal.
 *
 * Fetched once when the modal opens: the review step uses it to drop rows the
 * ledger already holds rather than offering them for import.
 */
export function useStatementCoverage() {
  return useQuery({
    queryKey: ["statement-coverage"],
    queryFn: async () => {
      const res = await fetch("/api/import/statement-coverage");
      if (!res.ok) throw new Error("Could not load statement coverage");
      return (await res.json()).coverage as { last4: string; coveredTo: string }[];
    },
  });
}
