import { serialize, type SearchState } from "@sheikbras/domain";
import type * as T from "./types";
export * from "./types";

/** Erro da API com código estável (`UNAUTHORIZED`, `VALIDATION`, `RATE_LIMITED`…). `details` traz a mensagem de cada campo. */
export class ApiError extends Error {
  constructor(public code: string, message: string, public status: number, public details?: Record<string, string>, public retryAfterSec?: number) { super(message); this.name = "ApiError"; }
  get isNetwork() { return this.status === 0; }
}
export interface TokenStore { get(): Promise<T.Tokens | null>; set(t: T.Tokens | null): Promise<void> }
export interface ApiOptions {
  baseUrl: string; store: TokenStore; fetch?: typeof fetch; onSessionExpired?: () => void; uuid?: () => string;
  /** Substitui a renovação padrão (ex.: web renova pelo servidor do site, que guarda o refresh token em cookie httpOnly). Devolve o novo access token ou null. */
  refresh?: () => Promise<string | null>;
}
type Q = Record<string, string | number | boolean | undefined>;
interface Req { body?: unknown; query?: Q; headers?: Record<string, string>; auth?: boolean }

/**
 * Cliente da API para web e app.
 * - Anexa o access token e, se a API responder UNAUTHORIZED, renova a sessão UMA vez (várias chamadas simultâneas compartilham a mesma renovação) e repete a chamada.
 * - Se a renovação falhar, limpa os tokens e chama `onSessionExpired` (a tela leva a pessoa ao login).
 * - Erros viram `ApiError`; falha de rede vira `ApiError` com `status` 0.
 */
export function createApi(o: ApiOptions) {
  const f = o.fetch ?? globalThis.fetch.bind(globalThis), base = o.baseUrl.replace(/\/$/, "");
  const uuid = o.uuid ?? (() => globalThis.crypto.randomUUID());
  let inflight: Promise<string | null> | null = null;

  async function send(method: string, path: string, r: Req, token: string | null) {
    const qs = r.query ? Object.entries(r.query).filter(([, v]) => v !== undefined && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&") : "";
    let res: Response;
    try { res = await f(`${base}${path}${qs ? (path.includes("?") ? "&" : "?") + qs : ""}`, { method, headers: { Accept: "application/json", ...(r.body !== undefined ? { "Content-Type": "application/json" } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...r.headers }, body: r.body !== undefined ? JSON.stringify(r.body) : undefined }); }
    catch { throw new ApiError("NETWORK", "Sem conexão. Verifique sua internet e tente de novo.", 0); }
    const text = await res.text(); let json: any = null; if (text) { try { json = JSON.parse(text); } catch { /* corpo não-JSON */ } }
    return { res, json };
  }
  const toError = (res: Response, json: any) => {
    const e = json?.error; const ra = Number(res.headers.get("retry-after")) || undefined;
    return new ApiError(e?.code ?? (res.status >= 500 ? "INTERNAL" : "ERROR"), e?.message ?? "Algo deu errado. Tente novamente.", res.status, e?.details, ra);
  };
  /** Renovação única e compartilhada. Devolve o novo access token ou null. */
  function refresh(): Promise<string | null> {
    return (inflight ??= (o.refresh ? o.refresh() : (async () => {
      const t = await o.store.get(); if (!t) return null;
      try {
        const { res, json } = await send("POST", "/auth/refresh", { body: { refreshToken: t.refreshToken } }, null);
        if (!res.ok) { await o.store.set(null); return null; }
        await o.store.set(json as T.Tokens); return (json as T.Tokens).accessToken;
      } catch { return null; } // sem rede: mantém os tokens para tentar depois
    })()).finally(() => { inflight = null; }));
  }
  async function call<R>(method: string, path: string, r: Req = {}): Promise<R> {
    const useAuth = r.auth !== false; let token = useAuth ? (await o.store.get())?.accessToken ?? null : null;
    let { res, json } = await send(method, path, r, token);
    if (res.status === 401 && useAuth && json?.error?.code === "UNAUTHORIZED" && (await o.store.get())) {
      const fresh = await refresh();
      if (fresh) ({ res, json } = await send(method, path, r, fresh)); else { await o.store.set(null); o.onSessionExpired?.(); }
    }
    if (!res.ok) throw toError(res, json); return json as R;
  }
  const get = <R>(p: string, q?: Q, auth = true) => call<R>("GET", p, { query: q, auth }), post = <R>(p: string, body?: unknown, extra: Req = {}) => call<R>("POST", p, { body, ...extra });
  const keep = async (r: T.AuthResult) => { await o.store.set(r.tokens); return r; };

  return {
    auth: {
      register: async (v: { nome: string; email: string; senha: string; aceite: boolean }) => keep(await post<T.AuthResult>("/auth/register", v, { auth: false })),
      login: async (v: { email: string; senha: string; codigo?: string }) => keep(await post<T.AuthResult>("/auth/login", v, { auth: false })),
      logout: async () => { const t = await o.store.get(); await o.store.set(null); if (t) await post("/auth/logout", { refreshToken: t.refreshToken }, { auth: false }).catch(() => {}); },
      logoutAll: async () => { await post("/auth/logout-all"); await o.store.set(null); },
      requestEmailVerification: () => post<void>("/auth/verify-email/request"), verifyEmail: (token: string) => post<T.ApiUser>("/auth/verify-email", { token }, { auth: false }),
      forgotPassword: (email: string) => post<{ message: string }>("/auth/password/forgot", { email }, { auth: false }), resetPassword: (token: string, senha: string) => post<void>("/auth/password/reset", { token, senha }, { auth: false }),
      changePassword: async (atual: string, nova: string) => { await post<void>("/auth/password/change", { atual, nova }); await o.store.set(null); },
      mfaSetup: () => post<{ secret: string; otpauthUrl: string }>("/auth/mfa/setup"), mfaConfirm: (codigo: string) => post<{ recoveryCodes: string[] }>("/auth/mfa/confirm", { codigo }), mfaDisable: (senha: string, codigo: string) => post<void>("/auth/mfa/disable", { senha, codigo }),
    },
    me: { get: () => get<T.ApiUser>("/me"), export: () => get<Record<string, unknown>>("/me/export"), remove: async (senha: string) => { await call<void>("DELETE", "/me", { body: { senha } }); await o.store.set(null); } },
    products: {
      /** Mesmo formato de URL da web: o estado de busca vira a query. */
      search: (s: SearchState, limit = 24) => get<T.SearchResult>("/products", Object.fromEntries([...new URLSearchParams(serialize(s))].concat([["limite", String(limit)]])), false),
      get: (id: number) => get<import("@sheikbras/domain").Product>(`/products/${id}`, undefined, true), reviews: (id: number) => get<T.ReviewDto[]>(`/products/${id}/reviews`, undefined, false),
      mine: () => get<unknown[]>("/products/mine"), create: (v: T.ProductInput) => post<unknown>("/products", v), update: (id: number, v: Partial<T.ProductInput>) => call<unknown>("PATCH", `/products/${id}`, { body: v }),
      publish: (id: number) => post<unknown>(`/products/${id}/publish`), archive: (id: number) => post<unknown>(`/products/${id}/archive`),
    },
    orders: {
      quote: (items: T.PlaceOrderBody["items"], cep: string) => post<T.QuoteGroup[]>("/orders/quote", { items, cep }),
      /** A chave de idempotência evita pedido duplicado se a chamada for repetida (rede instável, duplo toque). Reuse a mesma chave ao repetir. */
      place: (b: T.PlaceOrderBody, idempotencyKey: string = uuid()) => post<{ orders: T.OrderDto[]; payment: T.PaymentDto }>("/orders", b, { headers: { "Idempotency-Key": idempotencyKey } }),
      list: () => get<T.OrderDto[]>("/orders"), get: (id: string) => get<{ order: T.OrderDto; payment?: Omit<T.PaymentDto, "id" | "amountCents"> }>(`/orders/${id}`), cancel: (id: string, motivo?: string) => post<T.OrderDto>(`/orders/${id}/cancel`, { motivo }),
    },
    reviews: { create: (v: { orderId: string; productId: number; rating: number; text: string }) => post<T.ReviewDto>("/reviews", v) },
    seller: {
      apply: (v: T.SellerApplication) => post<unknown>("/sellers/apply", v), me: () => get<unknown>("/sellers/me"), orders: () => get<T.OrderDto[]>("/seller/orders"),
      prepare: (id: string) => post<T.OrderDto>(`/seller/orders/${id}/prepare`), ship: (id: string, carrier: string, code: string) => post<T.OrderDto>(`/seller/orders/${id}/ship`, { carrier, code }), deliver: (id: string) => post<T.OrderDto>(`/seller/orders/${id}/deliver`),
      finance: () => get<T.FinanceSummary>("/seller/finance"), reviews: () => get<(T.ReviewDto & { productId: number })[]>("/seller/reviews"), reply: (id: string, text: string) => post<T.ReviewDto>(`/seller/reviews/${id}/reply`, { text }),
    },
    admin: {
      orders: (status?: string) => get<T.OrderDto[]>("/admin/orders", { status }), audit: (categoria?: string) => get<{ type: string; at: string; userId?: string; ip?: string }[]>("/admin/audit", { categoria }), metrics: () => get<T.Metrics>("/admin/metrics"), disputes: () => get<T.ReturnDto[]>("/admin/disputes"),
      resolveReturn: (id: string, decision: "approve" | "reject", motivo: string) => post<T.ReturnDto>(`/admin/returns/${id}/resolve`, { decision, motivo }),
      banners: { list: () => get<unknown[]>("/admin/banners"), create: (v: T.BannerInput) => post<unknown>("/admin/banners", v), update: (id: string, v: Partial<T.BannerInput>) => call<unknown>("PATCH", `/admin/banners/${id}`, { body: v }), setStatus: (id: string, status: "active" | "paused") => post<unknown>(`/admin/banners/${id}/status`, { status }) },
      coupons: { list: () => get<unknown[]>("/admin/coupons"), create: (v: T.CouponInput) => post<unknown>("/admin/coupons", v), setActive: (code: string, active: boolean) => post<unknown>(`/admin/coupons/${code}/active`, { active }) },
      sellers: (status?: string) => get<unknown[]>("/admin/sellers", { status }), approve: (id: string) => post<unknown>(`/admin/sellers/${id}/approve`), reject: (id: string, motivo: string) => post<unknown>(`/admin/sellers/${id}/reject`, { motivo }), suspend: (id: string, motivo: string) => post<unknown>(`/admin/sellers/${id}/suspend`, { motivo }),
    },
    /** Vitrine pública (home composta e banners no ar para o dispositivo). */
    home: (device: T.Device = "desktop") => get<T.HomeDto>("/home", { device }, false), banners: (device: T.Device = "desktop", slot = "hero") => get<T.BannerDto[]>("/banners", { device, slot }, false),
    coupons: { check: (code: string) => post<{ code: string; pct: number }>("/coupons/check", { code }) },
    returns: {
      request: (v: { orderId: string; reason: T.ReturnReason; description: string; items?: { productId: number; qty: number }[] }) => post<T.ReturnDto>("/returns", v), list: () => get<T.ReturnDto[]>("/returns"), dispute: (id: string, text: string) => post<T.ReturnDto>(`/returns/${id}/dispute`, { text }),
      sellerList: () => get<T.ReturnDto[]>("/seller/returns"), approve: (id: string) => post<T.ReturnDto>(`/seller/returns/${id}/approve`), reject: (id: string, motivo: string) => post<T.ReturnDto>(`/seller/returns/${id}/reject`, { motivo }), received: (id: string) => post<T.ReturnDto>(`/seller/returns/${id}/received`),
    },
    notifications: { list: () => get<{ unread: number; items: T.NotificationDto[] }>("/notifications"), read: (id: string) => post<void>(`/notifications/${id}/read`), readAll: () => post<void>("/notifications/read-all") },
    uploads: {
      presign: (contentType: string, sizeBytes: number) => post<{ upload: { url: string; method: "PUT"; headers: Record<string, string>; expiresAt: string }; imageUrl: string }>("/seller/uploads/product-image", { contentType, sizeBytes }),
      /** Envia a foto direto para o armazenamento (a API só autoriza) e devolve o endereço público para usar em `imageUrl`. */
      uploadProductImage: async (file: { type: string; size: number; body: unknown }) => {
        const { upload, imageUrl } = await post<{ upload: { url: string; method: "PUT"; headers: Record<string, string> }; imageUrl: string }>("/seller/uploads/product-image", { contentType: file.type, sizeBytes: file.size });
        let res: Response; try { res = await f(upload.url, { method: upload.method, headers: upload.headers, body: file.body as BodyInit }); } catch { throw new ApiError("NETWORK", "Sem conexão. Verifique sua internet e tente de novo.", 0); }
        if (!res.ok) throw new ApiError("UPLOAD_FAILED", "Não foi possível enviar a imagem. Tente de novo.", res.status); return imageUrl;
      },
    },
    /** Útil para a UI saber se há sessão salva sem chamar a rede. */
    hasSession: async () => !!(await o.store.get()),
  };
}
export type Api = ReturnType<typeof createApi>;
export const memoryStore = (): TokenStore => { let t: T.Tokens | null = null; return { get: async () => t, set: async (v) => { t = v; } }; };
