/**
 * Typed PostgREST client. Browser + server-safe.
 *
 * Replaces `@supabase/supabase-js` for all data reads/writes. Same builder
 * shape as the SDK so call sites swap `api.from("x")` → `api.from("x")`
 * with no logic change. Rows are typed via the `Database` generic
 * (see `./db-types`).
 *
 * What this is NOT:
 *   - Not an auth client. Auth lives in `better-auth` (see `src/lib/auth.ts`).
 *     Use the better-auth admin plugin (`auth.api.*`) for user management —
 *     this client does not touch GoTrue.
 *   - Not a storage client. Storage lives in MinIO + the Next.js
 *     `/storage/*` rewrite (see `next.config.ts`).
 *
 * Implementation notes:
 *   - Uses `fetch` directly. No SDK dependency.
 *   - `NEXT_PUBLIC_API_URL` points at the PostgREST base URL
 *     (in dev: the local PostgREST proxy at `http://localhost:3001`).
 *   - `apikey` header alone is sufficient for PostgREST — it accepts it
 *     for both anon and service-role access. No `Authorization: Bearer` needed.
 *     (Safe for Vercel Edge Runtime.)
 *   - `single()` / `maybeSingle()` use `Accept: application/vnd.pgrst.object+json`
 *     to get an object back. A 406 means "no rows" — `single()` returns that
 *     as an error, `maybeSingle()` returns `{ data: null, error: null }`.
 *   - The builder is a single class — a fluent chain that switches between
 *     GET (read) and POST/PATCH/DELETE (mutation) based on which terminal
 *     method was called. Filters (`.eq()`, `.in()`, ...) are shared by both.
 */

import type { Database, RowOf } from "./db-types";

const API_URL = (() => {
  const url = process.env.NEXT_PUBLIC_API_URL;
  if (!url) {
    throw new Error(
      "Missing NEXT_PUBLIC_API_URL. Set it in .env.local (e.g. http://localhost:3001 for local PostgREST proxy).",
    );
  }
  return url.replace(/\/$/, "");
})();

const ANON_KEY = process.env.NEXT_PUBLIC_API_ANON_KEY ?? "anon";

function postgrestHeaders(): Record<string, string> {
  return { apikey: ANON_KEY, "Content-Type": "application/json" };
}

export type PostgrestError = { message: string; code?: string };

export type QueryResult<T> = { data: T; error: null } | { data: null; error: PostgrestError };

// ── Filter representation ────────────────────────────────────────────────────

type Filter =
  | { col: string; op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "like" | "ilike"; val: string | number | boolean }
  | { col: string; op: "is"; val: null | boolean }
  | { col: string; op: "in"; val: ReadonlyArray<string | number | boolean> };

function buildFilterString(filters: ReadonlyArray<Filter>): string {
  return filters
    .map((f) => {
      switch (f.op) {
        case "is":
          return `${f.col}=is.${f.val === null ? "null" : f.val}`;
        case "in": {
          const list = f.val
            .map((v) => (typeof v === "string" ? `"${v.replace(/"/g, '\\"')}"` : String(v)))
            .join(",");
          return `${f.col}=in.(${list})`;
        }
        default:
          return `${f.col}=${f.op}.${encodeURIComponent(String(f.val))}`;
      }
    })
    .join("&");
}

type OrderDirection = "asc" | "desc";
type OrderOpts = { ascending?: boolean; nullsFirst?: boolean };

// ── The unified builder ──────────────────────────────────────────────────────

/**
 * One builder class for all PostgREST operations. Method state is tracked:
 *   - `mode = "read"` (default) — `.select(...)` makes it a GET
 *   - `mode = "insert"` — `.insert(...)` makes it a POST
 *   - `mode = "update"` — `.update(...)` makes it a PATCH
 *   - `mode = "delete"` — `.delete()` makes it a DELETE
 *   - `mode = "upsert"` — `.upsert(...)` makes it a POST with prefer headers
 * Filters compose across all modes (you can filter an update/delete with .eq()).
 *
 * The second generic `TResult` is narrowed by `.single()` and `.maybeSingle()`
 * to `TRow | null` so TypeScript can give call sites precise return types
 * (instead of the loose `TRow[] | TRow | null` union).
 */
export class PostgrestBuilder<TRow, TResult = TRow[]> {
  protected filters: Filter[] = [];
  protected selectColumns = "*";
  protected countOption: "exact" | "planned" | "estimated" | null = null;
  protected headOnly = false;
  protected orderBy: { col: string; dir: OrderDirection; nullsFirst: boolean }[] = [];
  protected limitValue: number | null = null;
  protected rangeValue: { min: number; max: number } | null = null;
  protected singleMode: false | "single" | "maybeSingle" = false;
  protected mode: "read" | "insert" | "update" | "delete" | "upsert" = "read";
  protected body: unknown = null;
  protected onConflict: string | null = null;
  protected ignoreDuplicates = false;
  /** Override to use service-role headers (see `svc-fetch.ts`). */
  protected getHeaders: () => Record<string, string> = postgrestHeaders;

  constructor(protected readonly table: string) {}

  // ── Filters ──────────────────────────────────────────────────────────────

  select(
    cols = "*",
    options?: { count?: "exact" | "planned" | "estimated"; head?: boolean },
  ): this {
    this.selectColumns = cols;
    if (options?.count || options?.head) {
      this.countOption = options.count ?? "exact";
      this.headOnly = options.head ?? false;
    }
    return this;
  }
  eq(col: string, val: string | number | boolean | null): this {
    this.filters.push(val === null ? { col, op: "is", val: null } : { col, op: "eq", val });
    return this;
  }
  neq(col: string, val: string | number | boolean | null): this {
    this.filters.push(val === null ? { col, op: "is", val: null } : { col, op: "neq", val });
    return this;
  }
  gt(col: string, val: number | string): this {
    this.filters.push({ col, op: "gt", val });
    return this;
  }
  gte(col: string, val: number | string): this {
    this.filters.push({ col, op: "gte", val });
    return this;
  }
  lt(col: string, val: number | string): this {
    this.filters.push({ col, op: "lt", val });
    return this;
  }
  lte(col: string, val: number | string): this {
    this.filters.push({ col, op: "lte", val });
    return this;
  }
  is(col: string, val: null | boolean): this {
    this.filters.push({ col, op: "is", val });
    return this;
  }
  like(col: string, pattern: string): this {
    this.filters.push({ col, op: "like", val: pattern });
    return this;
  }
  ilike(col: string, pattern: string): this {
    this.filters.push({ col, op: "ilike", val: pattern });
    return this;
  }
  in(col: string, values: ReadonlyArray<string | number | boolean>): this {
    this.filters.push({ col, op: "in", val: values });
    return this;
  }
  order(col: string, opts?: OrderOpts): this {
    this.orderBy.push({
      col,
      dir: opts?.ascending === false ? "desc" : "asc",
      nullsFirst: opts?.nullsFirst ?? false,
    });
    return this;
  }
  limit(n: number): this {
    this.limitValue = n;
    return this;
  }
  range(min: number, max: number): this {
    this.rangeValue = { min, max };
    return this;
  }
  /** Returns a builder whose await resolves to `TRow | null` (or `null` for maybeSingle on 406). */
  single(): PostgrestBuilder<TRow, TRow | null> {
    this.singleMode = "single";
    return this as unknown as PostgrestBuilder<TRow, TRow | null>;
  }
  /** Same as `single()` but returns `{ data: null, error: null }` on no rows. */
  maybeSingle(): PostgrestBuilder<TRow, TRow | null> {
    this.singleMode = "maybeSingle";
    return this as unknown as PostgrestBuilder<TRow, TRow | null>;
  }

  // ── Mutations ────────────────────────────────────────────────────────────

  insert(data: Partial<TRow> | Partial<TRow>[]): this {
    this.mode = "insert";
    this.body = data;
    return this;
  }
  update(data: Partial<TRow>): this {
    this.mode = "update";
    this.body = data;
    return this;
  }
  /** Alias used by some SDK patterns. */
  set(data: Partial<TRow>): this {
    return this.update(data);
  }
  delete(): this {
    this.mode = "delete";
    return this;
  }
  upsert(data: TRow | TRow[], opts?: { onConflict?: string; ignoreDuplicates?: boolean }): this {
    this.mode = "upsert";
    this.body = data;
    this.onConflict = opts?.onConflict ?? null;
    this.ignoreDuplicates = opts?.ignoreDuplicates ?? false;
    return this;
  }

  // ── URL + header building ────────────────────────────────────────────────

  protected url(extra: string[] = []): string {
    const parts: string[] = [];
    if (this.selectColumns) parts.push(`select=${this.selectColumns}`);
    const filterStr = buildFilterString(this.filters);
    if (filterStr) parts.push(filterStr);
    for (const o of this.orderBy) {
      parts.push(`order=${o.col}.${o.dir}${o.nullsFirst ? ".nullsfirst" : ""}`);
    }
    if (this.limitValue !== null) parts.push(`limit=${this.limitValue}`);
    parts.push(...extra);
    const qs = parts.length ? `?${parts.join("&")}` : "";
    return `${API_URL}/rest/v1/${this.table}${qs}`;
  }

  protected headers(): Record<string, string> {
    const h = this.getHeaders();
    if (this.headOnly) {
      h["Accept"] = "application/vnd.pgrst.object+json";
    } else if (this.singleMode && (this.mode === "read" || this.mode === "update" || this.mode === "delete")) {
      h["Accept"] = "application/vnd.pgrst.object+json";
    }
    if (this.rangeValue) {
      h["Range-Unit"] = "items";
      h["Range"] = `${this.rangeValue.min}-${this.rangeValue.max}`;
    }
    const prefers: string[] = [];
    if (this.countOption) {
      prefers.push(`count=${this.countOption}`);
    }
    if (this.mode === "insert" || this.mode === "update" || this.mode === "delete" || this.mode === "upsert") {
      prefers.push("return=representation");
    }
    if (this.mode === "upsert") {
      if (this.ignoreDuplicates) {
        prefers.push("resolution=ignore-duplicates");
      } else {
        prefers.push("resolution=merge-duplicates");
      }
    }
    if (prefers.length) h["Prefer"] = prefers.join(",");
    return h;
  }

  protected method(): "GET" | "POST" | "PATCH" | "DELETE" {
    switch (this.mode) {
      case "read":
        return "GET";
      case "insert":
      case "upsert":
        return "POST";
      case "update":
        return "PATCH";
      case "delete":
        return "DELETE";
    }
  }

  // ── Execute ──────────────────────────────────────────────────────────────

  protected async execute(): Promise<QueryResult<TResult>> {
    let url = this.url();
    const headers = this.headers();
    const init: RequestInit = { method: this.method(), headers };
    if (this.mode === "upsert" && this.onConflict) {
      url += (url.includes("?") ? "&" : "?") + `on_conflict=${encodeURIComponent(this.onConflict)}`;
    }
    if (this.mode === "insert" || this.mode === "update" || this.mode === "upsert") {
      init.body = JSON.stringify(this.body);
    }
    let res: Response;
    try {
      res = await fetch(url, init);
    } catch (e) {
      return { data: null, error: { message: (e as Error).message } };
    }
    if (res.status === 406 && this.singleMode === "maybeSingle") {
      return { data: null, error: null } as QueryResult<TResult>;
    }
    if (!res.ok) {
      const text = await res.text().catch(() => "");
      return { data: null, error: { message: text || res.statusText, code: String(res.status) } };
    }
    if (this.countOption) {
      // PostgREST returns count in Content-Range header (e.g. "0-99/1234")
      const contentRange = res.headers.get("Content-Range");
      let count: number | null = null;
      if (contentRange) {
        const match = contentRange.match(/\/(\d+)/);
        if (match) count = parseInt(match[1], 10);
      }
      return { data: { count, data: null } as unknown as TResult, error: null };
    }
    if (this.singleMode || this.headOnly) {
      const obj = (await res.json()) as TRow;
      return { data: obj as unknown as TResult, error: null };
    }
    const arr = (await res.json()) as TResult;
    return { data: arr, error: null };
  }

  /** Awaitable: `const { data, error } = await api.from("x").eq("id", 1)`. */
  then<R1 = QueryResult<TResult>, R2 = never>(
    onfulfilled?: ((value: QueryResult<TResult>) => R1 | PromiseLike<R1>) | null,
    onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,
  ): Promise<R1 | R2> {
    return this.execute().then(onfulfilled, onrejected);
  }
}

// ── The exported client ──────────────────────────────────────────────────────

export const api = {
  from<TTable extends string>(table: TTable): PostgrestBuilder<RowOf<TTable>> {
    return new PostgrestBuilder<RowOf<TTable>>(table);
  },
  async rpc<TReturns = unknown>(fn: string, args: Record<string, unknown> = {}): Promise<QueryResult<TReturns>> {
    let res: Response;
    try {
      res = await fetch(`${API_URL}/rest/v1/rpc/${fn}`, {
        method: "POST",
        headers: postgrestHeaders(),
        body: JSON.stringify(args),
        cache: "no-store",
      });
    } catch (e) {
      return { data: null, error: { message: (e as Error).message } };
    }
    if (!res.ok) {
      const text = await res.text().catch(() => "");
      return { data: null, error: { message: text || res.statusText, code: String(res.status) } };
    }
    const data = (await res.json()) as TReturns;
    return { data, error: null };
  },
};
