/**
 * Petit client HTTP typé pour l'API PHP.
 *
 *  - GET (public) : pas de token nécessaire.
 *  - POST/PATCH/DELETE : ajoute Authorization: Bearer <token> si le token est
 *    stocké en localStorage (cf. composables/useAuth.ts).
 *
 * Lève une ApiError sur tout statut non-2xx. Le payload typé est attaché pour
 * que les écrans puissent afficher fields:{...} sur les 422.
 */

import type { ApiErrorPayload, ErrorCode } from '@/types/api'

const TOKEN_STORAGE_KEY = 'infolab.admin_token'

/** Récupère le token admin depuis localStorage (ou null). */
export function getAdminToken(): string | null {
  try {
    return localStorage.getItem(TOKEN_STORAGE_KEY)
  } catch {
    return null
  }
}

export function setAdminToken(token: string): void {
  localStorage.setItem(TOKEN_STORAGE_KEY, token)
}

export function clearAdminToken(): void {
  localStorage.removeItem(TOKEN_STORAGE_KEY)
}

export class ApiError extends Error {
  // En TS 6 + erasableSyntaxOnly, on ne peut plus déclarer les champs via param
  // properties (`constructor(public readonly x: ...)`). Déclaration explicite.
  readonly status: number
  readonly code: ErrorCode | string
  readonly payload: ApiErrorPayload

  constructor(status: number, code: ErrorCode | string, payload: ApiErrorPayload) {
    super(payload.message ?? code)
    this.status = status
    this.code = code
    this.payload = payload
  }

  /** Erreur de validation côté serveur : champ → code d'erreur. */
  get fields(): Record<string, string> {
    return this.payload.fields ?? {}
  }
}

type Json = Record<string, unknown> | unknown[] | null

interface RequestOptions {
  /** Si true, ajoute le header Authorization même sur un GET. Sinon, ne l'ajoute jamais sur GET. */
  withAuth?: boolean
  /** Paramètres de query string. null/undefined sont sautés. */
  query?: Record<string, string | number | boolean | null | undefined>
}

function buildUrl(path: string, query?: RequestOptions['query']): string {
  if (!query) return path
  const params = new URLSearchParams()
  for (const [k, v] of Object.entries(query)) {
    if (v === undefined || v === null || v === '') continue
    params.append(k, String(v))
  }
  const qs = params.toString()
  return qs ? `${path}?${qs}` : path
}

async function request<T>(
  method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
  path: string,
  body?: Json,
  opts: RequestOptions = {},
): Promise<T> {
  const headers: Record<string, string> = {
    Accept: 'application/json',
  }
  if (body !== undefined) {
    headers['Content-Type'] = 'application/json'
  }
  // Token Bearer sur tous les writes (et optionnellement les reads via withAuth).
  if (method !== 'GET' || opts.withAuth) {
    const token = getAdminToken()
    if (token) headers['Authorization'] = `Bearer ${token}`
  }

  const url = buildUrl(path, opts.query)
  const res = await fetch(url, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  })

  // 204 No Content -> on retourne undefined as T (utile pour DELETE).
  if (res.status === 204) {
    return undefined as T
  }

  const text = await res.text()
  const data = text === '' ? null : (JSON.parse(text) as unknown)

  if (!res.ok) {
    const payload = (data ?? { error: 'internal_server_error' }) as ApiErrorPayload
    throw new ApiError(res.status, payload.error ?? `http_${res.status}`, payload)
  }
  return data as T
}

export const api = {
  get:    <T>(path: string, opts?: RequestOptions) => request<T>('GET',    path, undefined, opts),
  post:   <T>(path: string, body: Json,           opts?: RequestOptions) => request<T>('POST',   path, body, opts),
  patch:  <T>(path: string, body: Json,           opts?: RequestOptions) => request<T>('PATCH',  path, body, opts),
  delete: <T>(path: string,                       opts?: RequestOptions) => request<T>('DELETE', path, undefined, opts),
}

/**
 * Upload multipart/form-data — utilisé par la galerie projet.
 *
 * Le navigateur fixe lui-même le Content-Type avec le bon boundary, on ne le
 * définit donc PAS à la main (sinon le boundary serait absent et le backend ne
 * saurait pas découper le body).
 *
 * @param path   path vers l'endpoint, ex: '/api/projects/nos-ecoles/images'
 * @param fields champs texte additionnels (caption, sort_order, ...)
 * @param file   le fichier (issu d'un <input type="file"> ou drop)
 * @param fileFieldName  nom du champ côté serveur (défaut 'file')
 */
export async function uploadFile<T>(
  path: string,
  file: File,
  fields: Record<string, string | number | undefined | null> = {},
  fileFieldName = 'file',
): Promise<T> {
  const form = new FormData()
  form.append(fileFieldName, file, file.name)
  for (const [k, v] of Object.entries(fields)) {
    if (v !== undefined && v !== null && v !== '') {
      form.append(k, String(v))
    }
  }

  const headers: Record<string, string> = { Accept: 'application/json' }
  const token = getAdminToken()
  if (token) headers['Authorization'] = `Bearer ${token}`

  const res = await fetch(path, { method: 'POST', headers, body: form })

  if (res.status === 204) return undefined as T
  const text = await res.text()
  const data = text === '' ? null : (JSON.parse(text) as unknown)
  if (!res.ok) {
    const payload = (data ?? { error: 'internal_server_error' }) as ApiErrorPayload
    throw new ApiError(res.status, payload.error ?? `http_${res.status}`, payload)
  }
  return data as T
}
