/**
 * Generic offline durability for the central API client. When a write happens
 * while offline (or fails with a network error), it is enqueued in the outbox
 * and an optimistic value is returned, so no write is ever lost — instead of the
 * mutation throwing. The online path is unchanged apart from an idempotency key.
 *
 * A small set of endpoints are *online-only* (payments, billing, auth, SMS): these
 * must never be queued — they throw {@link OfflineUnavailableError} when offline so
 * the UI can tell the user to reconnect.
 */
import {
  enqueueOutboxOp,
  getOutboxCount,
  getQueueCount,
  type OutboxMethod,
} from "@/lib/offline/offline-db";
import { useSyncQueueStore } from "@/lib/stores/sync-queue.store";

/** Endpoints that require a live connection and must NOT be queued offline. */
const ONLINE_ONLY: RegExp[] = [
  /\/api\/auth\//, // login / refresh / register / OTP
  /\/api\/billing\b/, // checkout / verify / payment gateway
  /\/payments?\b/, // taking payment against an order/shift
  /\/api\/sms\b/, // sending SMS now / campaigns
  /\/send-sms\b/,
  /\/export\b/, // server-computed exports
];

export function isOnlineOnly(url: string): boolean {
  return ONLINE_ONLY.some((re) => re.test(url));
}

export class OfflineUnavailableError extends Error {
  readonly code = "OFFLINE_UNAVAILABLE";
  constructor(message = "This action needs an internet connection.") {
    super(message);
    this.name = "OfflineUnavailableError";
  }
}

export function isNetworkError(err: unknown): boolean {
  if (err instanceof TypeError) {
    const msg = err.message.toLowerCase();
    return (
      msg.includes("failed to fetch") ||
      msg.includes("networkerror") ||
      msg.includes("load failed") ||
      msg.includes("network request failed")
    );
  }
  const ax = err as { isAxiosError?: boolean; response?: unknown };
  return !!ax?.isAxiosError && !ax.response;
}

export function newIdempotencyKey(): string {
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
  return `idem_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}

export function newLocalId(): string {
  return `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

/** Best-effort entity kind from a URL (last non-id path segment). */
export function entityTypeFromUrl(url: string): string {
  const path = (url.split("?")[0] ?? "").replace(/^\/api\//, "");
  const segs = path.split("/").filter(Boolean);
  for (let i = segs.length - 1; i >= 0; i--) {
    const s = segs[i];
    const looksLikeId = /^[0-9a-f]{16,}$/i.test(s) || s.startsWith("local_");
    if (!looksLikeId) return s;
  }
  return segs[0] ?? "entity";
}

async function refreshBadge(): Promise<void> {
  const n = (await getOutboxCount()) + (await getQueueCount());
  useSyncQueueStore.getState().setQueueCount(n);
}

/**
 * Enqueue a write to the outbox and synthesize an optimistic return value.
 * POST → treated as a create (local id, remappable later); PUT/PATCH → echo the
 * body; DELETE → void.
 */
export async function queueWrite(
  method: OutboxMethod,
  url: string,
  body: unknown,
  idempotencyKey: string
): Promise<unknown> {
  let createsClientId: string | undefined;
  let optimistic: unknown;

  if (method === "POST") {
    createsClientId = newLocalId();
    optimistic =
      body && typeof body === "object"
        ? { id: createsClientId, ...(body as Record<string, unknown>) }
        : { id: createsClientId };
  } else if (method === "DELETE") {
    optimistic = undefined;
  } else {
    optimistic = body && typeof body === "object" ? { ...(body as Record<string, unknown>) } : body;
  }

  await enqueueOutboxOp({
    id: newLocalId(),
    idempotencyKey,
    method,
    url,
    body,
    entityType: entityTypeFromUrl(url),
    createsClientId,
    idField: "id",
    createdAt: Date.now(),
  });
  await refreshBadge();
  return optimistic;
}
