import "server-only";

import { cookies } from "next/headers";

import { SESSION_COOKIE } from "@/lib/auth/constants";
import { env } from "@/lib/env";
import {
  createLaravelCms,
  LaravelCmsError,
  type RequestOptions,
} from "@/lib/laravel-cms/client";

export { LaravelCmsError as CmsError };
export type { RequestOptions };

/**
 * Admin API paths on the Laravel CMS, relative to LARAVEL_API_URL. Auth,
 * blogs, import and media are what staging serves today
 * (docs/laravel-blog-api.md §18.1, §18.4); the rest follow §8.2 under the
 * same `admin/` prefix. Change a path here only.
 */
export const cmsPaths = {
  login: "admin/auth/login",
  logout: "admin/auth/logout",
  me: "admin/auth/me",
  password: "admin/auth/me/password",

  blogs: "admin/blogs",
  blog: (id: string) => `admin/blogs/${encodeURIComponent(id)}`,
  blogAction: (id: string, action: string) =>
    `admin/blogs/${encodeURIComponent(id)}/${action}`,
  revisions: (id: string) => `admin/blogs/${encodeURIComponent(id)}/revisions`,
  revision: (id: string, revisionId: string) =>
    `admin/blogs/${encodeURIComponent(id)}/revisions/${encodeURIComponent(revisionId)}`,
  slugCheck: "admin/blogs/slug-check",
  relatedPreview: "admin/blogs/related-preview",
  importJson: "blog/admin/blogs/import-json",
  tagSuggestions: "admin/tags/suggestions",
  dashboard: "admin/dashboard",

  categories: "admin/categories",
  category: (id: string) => `admin/categories/${encodeURIComponent(id)}`,
  authors: "admin/authors",
  author: (id: string) => `admin/authors/${encodeURIComponent(id)}`,
  // Staging serves GET (list), POST (Base64 upload) and DELETE {id} here;
  // PATCH {id} and /usage aren't built yet (docs/laravel-blog-api.md §18.4).
  media: "blog/admin/media",
  mediaItem: (id: string) => `blog/admin/media/${encodeURIComponent(id)}`,
  mediaUsage: (id: string) =>
    `blog/admin/media/${encodeURIComponent(id)}/usage`,
  redirects: "admin/redirects",
  redirect: (id: string) => `admin/redirects/${encodeURIComponent(id)}`,
  users: "admin/users",
  user: (id: string) => `admin/users/${encodeURIComponent(id)}`,
  userAction: (id: string, action: string) =>
    `admin/users/${encodeURIComponent(id)}/${action}`,
  blogDefaults: "admin/settings/blog-defaults",
} as const;

/** The signed-in admin's API token (set at login, see lib/auth/session.ts). */
export async function sessionToken() {
  return (await cookies()).get(SESSION_COOKIE)?.value ?? null;
}

/** A client for this request: the signed-in admin's token, or none. */
export async function cmsClient({ auth = true } = {}) {
  return createLaravelCms({
    apiUrl: env().LARAVEL_API_URL,
    token: auth ? await sessionToken() : null,
  });
}

/** Calls the CMS as the signed-in admin and returns the whole JSON body. */
export async function cmsRequest<T = Record<string, unknown>>(
  path: string,
  init?: RequestOptions,
) {
  return (await cmsClient()).request<T>(path, init);
}

/** Calls the CMS and returns `data` from `{ok, data}` (or the body itself when there is no `data`). */
export async function cmsData<T>(
  path: string,
  init?: RequestOptions,
): Promise<T> {
  const body = await cmsRequest<{ data?: T }>(path, init);
  return (
    body && typeof body === "object" && "data" in body ? body.data : body
  ) as T;
}

/** The success message the CMS sent with a write, if any. */
export function cmsMessage(body: unknown, fallback: string) {
  const message = (body as { message?: unknown } | null)?.message;
  return typeof message === "string" && message ? message : fallback;
}

export const isMissingRoute = (error: unknown): error is LaravelCmsError =>
  error instanceof LaravelCmsError && error.missingRoute;

const warned = new Set<string>();

function warnMissing(error: LaravelCmsError) {
  const key = error.endpoint ?? error.message;
  if (warned.has(key)) return;
  warned.add(key);
  console.warn(
    `[cms] ${key} isn't available on the Laravel CMS yet; using a fallback.`,
  );
}

/**
 * For data a page can do without (pickers, counts): when the CMS hasn't
 * built the endpoint yet, returns `fallback` instead of failing the page.
 */
export async function withFallback<T>(
  load: Promise<T>,
  fallback: T,
): Promise<T> {
  try {
    return await load;
  } catch (error) {
    if (isMissingRoute(error)) {
      warnMissing(error);
      return fallback;
    }
    throw error;
  }
}

export type Loaded<T> =
  { available: true; data: T } | { available: false; endpoint: string };

/**
 * For a page's main data: tells the page whether the CMS serves it, so it
 * can show `<CmsUnavailable>` instead of an error.
 */
export async function loadOrUnavailable<T>(
  load: Promise<T>,
): Promise<Loaded<T>> {
  try {
    return { available: true, data: await load };
  } catch (error) {
    if (isMissingRoute(error)) {
      warnMissing(error);
      return { available: false, endpoint: error.endpoint ?? "this endpoint" };
    }
    throw error;
  }
}

// ---------------------------------------------------------------------------
// Reading Laravel rows
// ---------------------------------------------------------------------------

/**
 * Laravel returns raw rows in snake_case; the spec's shapes are camelCase.
 * Reads either spelling, so mappers keep working if the API changes case.
 */
export function field<T = unknown>(row: unknown, snake: string): T | undefined {
  if (!row || typeof row !== "object") return undefined;
  const record = row as Record<string, unknown>;
  if (snake in record) return record[snake] as T;
  const camel = snake.replace(/_([a-z])/g, (_, letter: string) =>
    letter.toUpperCase(),
  );
  return record[camel] as T | undefined;
}

export const str = (row: unknown, key: string) => {
  const value = field(row, key);
  return value === null || value === undefined ? "" : String(value);
};

export const strOrNull = (row: unknown, key: string) => {
  const value = field(row, key);
  return value === null || value === undefined || value === ""
    ? null
    : String(value);
};

export const num = (row: unknown, key: string, fallback = 0) => {
  const value = Number(field(row, key));
  return Number.isFinite(value) ? value : fallback;
};

export const bool = (row: unknown, key: string, fallback = false) => {
  const value = field(row, key);
  if (value === undefined || value === null) return fallback;
  return value === true || value === 1 || value === "1" || value === "true";
};

/** ISO string (or null) from a Laravel timestamp. */
export const isoOrNull = (row: unknown, key: string) => {
  const value = field(row, key);
  if (!value) return null;
  const date = new Date(String(value));
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
};

export const dateOrNull = (row: unknown, key: string) => {
  const iso = isoOrNull(row, key);
  return iso ? new Date(iso) : null;
};

/** For required timestamps: falls back to the epoch rather than an invalid date. */
export const dateOf = (row: unknown, key: string) =>
  dateOrNull(row, key) ?? new Date(0);

export const idOf = (row: unknown) => str(row, "id");

/** An array field, or []. */
export function list<T = unknown>(row: unknown, key: string): T[] {
  const value = field(row, key);
  return Array.isArray(value) ? (value as T[]) : [];
}

/** A nested object (relation), or null. */
export function relation(
  row: unknown,
  key: string,
): Record<string, unknown> | null {
  const value = field(row, key);
  return value && typeof value === "object" && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}

/** Laravel's LengthAwarePaginator, or a plain array, as `{items, total}`. */
export function paginated<T>(body: unknown, map: (row: unknown) => T) {
  if (Array.isArray(body)) return { items: body.map(map), total: body.length };
  const rows = list(body, "data");
  const total = num(body, "total", rows.length);
  return { items: rows.map(map), total };
}
