'use client'

/**
 * Browser-side API client.
 *
 * Every mutating call carries the CSRF token from the readable cookie, so no component
 * has to remember to attach it. Errors come back as a typed object rather than a thrown
 * string, because the UI needs the machine-readable code to decide between "show a
 * field error", "ask them to sign in again" and "offer a retry".
 */

export interface ApiFailure {
  code: string
  message: string
  messageAr: string
  details?: Record<string, string>
}

export class ApiClientError extends Error {
  readonly failure: ApiFailure
  readonly status: number

  constructor(failure: ApiFailure, status: number) {
    super(failure.message)
    this.name = 'ApiClientError'
    this.failure = failure
    this.status = status
  }
}

function readCookie(name: string): string | null {
  if (typeof document === 'undefined') return null
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
  return match ? decodeURIComponent(match[1]) : null
}

async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
  const method = (init.method ?? 'GET').toUpperCase()
  const headers = new Headers(init.headers)
  headers.set('Accept', 'application/json')

  if (method !== 'GET' && method !== 'HEAD') {
    headers.set('Content-Type', 'application/json')
    const csrf = readCookie('nabdh_csrf')
    if (csrf) headers.set('x-csrf-token', csrf)
  }

  let response: Response
  try {
    response = await fetch(path, { ...init, headers, credentials: 'same-origin' })
  } catch {
    throw new ApiClientError(
      {
        code: 'NETWORK_ERROR',
        message: 'The connection to the server was lost.',
        messageAr: 'انقطع الاتصال بالخادم.',
      },
      0,
    )
  }

  let payload: unknown = null
  try {
    payload = await response.json()
  } catch {
    payload = null
  }

  const envelope = payload as { ok?: boolean; data?: T; error?: ApiFailure } | null

  if (!response.ok || !envelope?.ok) {
    throw new ApiClientError(
      envelope?.error ?? {
        code: 'SERVER_ERROR',
        message: 'Something went wrong on our side.',
        messageAr: 'حدث خطأ من جانبنا.',
      },
      response.status,
    )
  }

  return envelope.data as T
}

export const api = {
  get: <T>(path: string, init?: RequestInit) => request<T>(path, { ...init, method: 'GET' }),
  post: <T>(path: string, body?: unknown, init?: RequestInit) =>
    request<T>(path, { ...init, method: 'POST', body: body === undefined ? undefined : JSON.stringify(body) }),
  patch: <T>(path: string, body?: unknown, init?: RequestInit) =>
    request<T>(path, { ...init, method: 'PATCH', body: body === undefined ? undefined : JSON.stringify(body) }),
  put: <T>(path: string, body?: unknown, init?: RequestInit) =>
    request<T>(path, { ...init, method: 'PUT', body: body === undefined ? undefined : JSON.stringify(body) }),
  delete: <T>(path: string, init?: RequestInit) => request<T>(path, { ...init, method: 'DELETE' }),
}

/** Picks the message matching the active locale from an API failure. */
export function failureMessage(failure: ApiFailure, locale: 'ar' | 'en'): string {
  return locale === 'ar' ? failure.messageAr : failure.message
}

/** The codes a validation failure can name per field. */
export const FIELD_ERROR_CODES = [
  'required',
  'tooSmall',
  'tooLarge',
  'invalidType',
  'invalidFormat',
  'invalidValue',
] as const

/**
 * Turn one field's error code into a sentence in the reader's language.
 *
 * A code that predates this mapping — or one from a future schema — falls back to the
 * generic sentence rather than being printed raw. A form must never show a reader a token
 * that was written for a developer.
 */
export function fieldErrorMessage(
  code: string | undefined,
  t: (key: string) => string,
): string | undefined {
  if (!code) return undefined
  return (FIELD_ERROR_CODES as readonly string[]).includes(code)
    ? t(`validation.${code}`)
    : t('validation.invalidValue')
}
