import type {ApiResponse, RangeKey} from '@/shared/types/api';
import {isFailure} from '@/shared/types/api';

/**
 * The backend-swap seam. Nothing above this file knows a URL.
 *
 * Point NEXT_PUBLIC_API_BASE at a real host and every request bypasses the
 * local route handlers. Node, Nest, Spring, Laravel — as long as responses
 * match the envelope in shared/types/api, no feature file changes.
 *
 * Two shapes live here and they are not interchangeable:
 *
 *   Endpoint<T> + fetchEndpoint   declarative GETs, consumed by useResource,
 *                                 keyed by URL so a scope change refetches
 *   getJson / postJson            imperative calls (login, logout, saves),
 *                                 where the caller needs the status back
 *
 * Credentials ride on every request: the session is an httpOnly cookie, so
 * omitting them would authenticate nothing.
 */

const BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';

// ---------------------------------------------------------------------------
// Declarative endpoints — what useResource consumes
// ---------------------------------------------------------------------------

export interface Endpoint<T> {
  path: string;
  params: Record<string, string>;
  /** Phantom field — carries the payload type through to useResource. */
  __type?: T;
}

/** The query scope every analytics endpoint is filtered by. */
export interface Scope {
  range: RangeKey;
  storeId: string;
}

/** Build a scope-filtered endpoint. Feature repositories only. */
export function scopedEndpoint<T>(
  path: string,
  scope: Scope,
  extra: Record<string, string> = {},
): Endpoint<T> {
  return {path, params: {range: scope.range, storeId: scope.storeId, ...extra}};
}

export function endpointUrl(e: Endpoint<unknown>): string {
  const qs = new URLSearchParams(e.params).toString();
  return `${BASE}${e.path}${qs ? `?${qs}` : ''}`;
}

export async function fetchEndpoint<T>(
  e: Endpoint<T>,
  signal?: AbortSignal,
): Promise<ApiResponse<T>> {
  const res = await fetch(endpointUrl(e), {
    signal,
    cache: 'no-store',
    credentials: 'same-origin',
  });
  // A non-JSON body (proxy error page, 502 HTML) must surface as a typed
  // failure rather than throwing a SyntaxError deep inside the hook.
  let body: unknown;
  try {
    body = await res.json();
  } catch {
    return {
      error: {code: 'internal', message: `Unexpected response (${res.status})`},
    };
  }
  return body as ApiResponse<T>;
}

// ---------------------------------------------------------------------------
// Imperative calls — mutations and one-shot reads
// ---------------------------------------------------------------------------

/**
 * A TRANSPORT outcome, not a domain one. `ok` means the request completed with
 * a 2xx and a parseable envelope; turning `message`/`field` into something a
 * user should read is the service layer's job.
 */
export interface HttpResult<T> {
  ok: boolean;
  status: number;
  data?: T;
  message?: string;
  /** Server-attributed input name, when a failure belongs to one field. */
  field?: string;
}

async function request<T>(
  path: string,
  init?: RequestInit,
): Promise<HttpResult<T>> {
  let res: Response;
  try {
    res = await fetch(`${BASE}${path}`, {
      cache: 'no-store',
      credentials: 'same-origin',
      ...init,
    });
  } catch (err) {
    // Offline, DNS, CORS — there is no status to report, so 0 stands for
    // "never reached the server", which callers distinguish from a 4xx.
    return {
      ok: false,
      status: 0,
      message: (err as Error)?.message ?? 'Network request failed',
    };
  }

  let body: unknown;
  try {
    body = await res.json();
  } catch {
    return {
      ok: false,
      status: res.status,
      message: `Unexpected response (${res.status})`,
    };
  }

  const envelope = body as ApiResponse<T> & {field?: string};

  if (!res.ok || isFailure(envelope)) {
    return {
      ok: false,
      status: res.status,
      message: isFailure(envelope) ? envelope.error.message : undefined,
      field: envelope.field,
    };
  }
  return {ok: true, status: res.status, data: envelope.data};
}

export function getJson<T>(path: string): Promise<HttpResult<T>> {
  return request<T>(path);
}

export function postJson<T>(
  path: string,
  body: unknown,
): Promise<HttpResult<T>> {
  return request<T>(path, {
    method: 'POST',
    headers: {'content-type': 'application/json'},
    body: JSON.stringify(body),
  });
}

export function patchJson<T>(
  path: string,
  body: unknown,
): Promise<HttpResult<T>> {
  return request<T>(path, {
    method: 'PATCH',
    headers: {'content-type': 'application/json'},
    body: JSON.stringify(body),
  });
}
