// Types du SDK caisse.bzh 1.4.0 — https://caisse.bzh/developpeurs
// À placer à côté de caisse-bzh.mjs (même nom de base) pour l'autocomplétion TypeScript.

export declare const SDK_VERSION: string;
export declare const PROTOCOL_VERSION: 1;
export declare const DEFAULT_BASE_URL: string;

export declare class CaisseApiError extends Error {
  status: number;
  /** Code d'erreur renvoyé par l'API (`unknown_shop`, `no_items`, `bridge_auth`…). */
  code: string;
  body: unknown;
}

type FetchLike = (input: string, init?: { method?: string; headers?: Record<string, string>; body?: string }) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;

export interface ClientOptions {
  /** Par défaut https://caisse.bzh */
  baseUrl?: string;
  fetch?: FetchLike;
}

// ── API publique ───────────────────────────────────────────────────────────

export type DayKey = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
export interface WeekHours {
  tz: string;
  days: Record<DayKey, { closed: boolean; ranges: [string, string][] }>;
}

export interface Carte {
  ok: true;
  shop: { slug: string; name: string; tagline: string; address: string; phone: string; currency: string };
  hours: WeekHours;
  /** Ouvert à l'instant, dans le fuseau de l'établissement. */
  openNow: boolean;
  categories: { id: string; name: string; icon: string }[];
  items: { id: string; categoryId: string; name: string; /** € TTC */ price: number; description?: string; tags?: string[] }[];
}

export interface OrderInput {
  slug: string;
  customerName: string;
  phone?: string;
  /** "HH:MM" ou un libellé court ; vide = « Dès que possible ». */
  pickupAt?: string;
  note?: string;
  /** 40 lignes au plus, quantité 1 à 20. Un id inconnu est ignoré. */
  items: { itemId: string; qty?: number; note?: string }[];
}

export interface PlacedOrder {
  id: string;
  /** Numéro du jour, affiché en caisse. */
  no: number;
  pickupAt: string;
  /** € TTC, recalculé par la caisse. */
  total: number;
  status: "nouveau";
  payment: { method: "onsite"; status: "pending" };
}

/** Même clé pour un nouvel essai : la première réponse est rendue, sans doublon (24 h). */
export interface IdempotencyOptions { idempotencyKey?: string }

/** Offre de la billetterie en ligne (GET /api/webtix/catalog). */
export interface TicketOffer {
  /** À reprendre dans bookingUrl({ offerId }) et dans params.offer. */
  offerId: string;
  label: string;
  desc?: string;
  kind?: "perm" | "expo" | "activite" | "event";
  code?: string;
  /** Présent quand l'offre se vend par séance. */
  act?: string;
  tariffs: { tariffId: string; label: string; unitCents: number; needsProof?: boolean }[];
}

/** Séance d'une offre. */
export interface TicketSession {
  id: string;
  /** AAAA-MM-JJ */
  day: string;
  start: string;
  end: string;
  label: string;
  level: "open" | "last" | "full" | "paused";
  /** Places restantes, donné seulement quand il en reste peu. */
  free: number | null;
  waitlist: boolean;
}

export interface TicketCatalog {
  ok: true;
  shop?: { name: string; slug: string };
  open?: boolean;
  today?: string;
  lang?: string;
  offers: TicketOffer[];
  /** Avec params.offer. */
  sessions?: TicketSession[];
}

export interface TicketCatalogParams { lang?: "fr" | "en" | "de" | "it" | "es"; offer?: string; from?: string; days?: number }

/** Article de la boutique en ligne (GET /api/boutique/catalog). */
export interface ShopCatalogItem {
  id: string;
  name: string;
  desc: string;
  /** https, ou null. */
  image: string | null;
  /** € TTC, le plus bas des déclinaisons. */
  price: number;
  priceMax: number;
  soldOut: boolean;
  /** Ce qu'il reste (article sans déclinaison) ; null quand il n'est pas compté. */
  left: number | null;
  /** Page de l'article dans la boutique. */
  url: string;
  variants: { id: string; name: string; price: number; left: number | null }[];
}

export interface ShopCatalog {
  ok: true;
  v: 1;
  shop: { slug: string; name: string; tagline: string; address: string; phone: string; origin: string; url: string; demo: boolean };
  /** false : boutique fermée, aucune catégorie. */
  open: boolean;
  /** pickup : paiement au retrait. */
  payment: "pickup" | "online";
  training: boolean;
  currency: "EUR";
  symbol: string;
  pickup: string;
  stockKnown: boolean;
  categories: { id: string; name: string; icon: string; items: ShopCatalogItem[] }[];
  at: string;
}

/** Ce que le lien de réservation ouvre ; chaque valeur est vérifiée par la page. */
export interface BookingPick { offerId?: string; /** AAAA-MM-JJ */ day?: string; sessionId?: string; lang?: "fr" | "en" | "de" | "it" | "es" }

export interface Client {
  getCarte(slug: string): Promise<Carte>;
  placeOrder(input: OrderInput, options?: IdempotencyOptions): Promise<PlacedOrder>;
  getTicketCatalog(slug: string, params?: TicketCatalogParams): Promise<TicketCatalog>;
  getShopCatalog(slug: string): Promise<ShopCatalog>;
  bookingUrl(slug: string, pick?: BookingPick): string;
}

export declare function bookingUrl(slug: string, pick?: BookingPick, baseUrl?: string): string;

export declare function createClient(options?: ClientOptions): Client;

// ── API avec clé ───────────────────────────────────────────────────────────

export interface ApiClientOptions extends ClientOptions {
  /** cbz_live_…, créée dans la console, page « Accès API ». Côté serveur uniquement. */
  apiKey: string;
}

export interface ApiShop {
  ok: true;
  shop: { id: string; slug: string; name: string; kind: "restaurant" | "bar" | "boutique" | "alimentaire" | "musee"; city: string; address: string; phone: string; siret: string; vatNumber: string };
  features: { onlineOrders: boolean; stock: boolean; slots?: boolean; accessControl?: boolean; attendance?: boolean; groups?: boolean; passes?: boolean; onlineTickets?: boolean };
  key: { id: string; name: string; scope: "read" | "write" | "export" };
}

export interface Category { id: string; name: string; icon: string; course: "starter" | "main" | "dessert" | "drink"; /** 0.2 pour 20 % */ vatRate: number }

export interface Item {
  id: string;
  categoryId: string;
  name: string;
  /** € TTC */
  price: number;
  description?: string;
  tags?: string[];
  sku?: string;
  barcode?: string;
  variants?: { id: string; name: string; price?: number; sku?: string; barcode?: string; syncKey?: string }[];
  modifiers?: { id: string; name: string; required: boolean; multiple: boolean; options: { id: string; name: string; price: number }[] }[];
  syncKey?: string;
  /** "sync" : l'article vient de la boutique en ligne et ne se modifie que là-bas. */
  managedBy: "caisse" | "sync";
}

export interface ItemInput {
  name: string;
  price: number;
  categoryId: string;
  description?: string;
  tags?: string[];
  sku?: string;
  barcode?: string;
}

export interface StockLine { syncKey: RecordKey; quantity: number | null; itemId?: string; variantId?: string; name?: string; sku?: string }

export interface Sale {
  id: string;
  number: number;
  fiscalNumber: number | null;
  day: string;
  settledAt: string;
  settledBy: string;
  type: "table" | "counter";
  label: string;
  covers: number;
  subtotal: number;
  discount: number;
  total: number;
  tip: number;
  loyaltyRedemption: number;
  vat: { rate: number; gross: number; net: number; vat: number }[];
  /** tx et card : paiement carte piloté par le terminal (jamais le numéro complet de la carte). */
  payments: { method: string; amount: number; tx?: string; card?: { brand?: string; last4?: string; auth?: string; mode?: string; app?: string } }[];
  items: { name: string; qty: number; unitPrice: number; total?: number; vatRate?: number; itemId?: string; variantId?: string; sku?: string }[];
  /** Vente du mode formation, hors chiffre d'affaires. */
  training: boolean;
}

export type OrderStatus = "new" | "accepted" | "ready" | "collected" | "cancelled";

export interface Order {
  id: string;
  number: number;
  status: OrderStatus;
  customerName: string;
  phone: string;
  pickupAt: string;
  note: string;
  items: { itemId: string; name: string; qty: number; unitPrice: number; note: string }[];
  total: number;
  payment: { method: "onsite" | "card"; status: "pending" | "paid" };
  createdAt: string;
}

export interface AccessGate { id: string; name: string; zone: string; direction: "in" | "out" | "both"; active: boolean; archivedAt: string | null }

export interface AccessValidateInput {
  /** Texte lu dans le QR code du billet. */
  code: string;
  /** Identifiant de la porte (listAccessGates). */
  gate: string;
  /** Identifiant de la clé (key_...), un tiret, puis votre identifiant du scan : le même à chaque nouvel essai. */
  eid: string;
  /** Nombre de personnes d'un billet de groupe, 1 à 500. */
  n?: number;
  dir?: "in" | "out";
  /** Vérifier sans consommer. */
  dryRun?: boolean;
}

export interface AccessVerdict {
  ok: true;
  verdict: "accept" | "refuse" | "ask";
  res: string;
  n: number;
  max: number;
  remaining?: number;
  replayed: boolean;
  logged: boolean;
  dryRun: boolean;
  training: boolean;
  ticket: { tid: string; no: string; product: string; tariff: string; proof: boolean; admits: number; validFrom: number; validTo: number; slot: boolean; holder?: string } | null;
  first?: { at: number; gate: string; gateName: string };
  view: { tone: "ok" | "warn" | "no"; icon: string; title: string; detail: string };
  serverNow: number;
  ms: number;
}

export interface AccessEvent {
  gate: string; seq: number; eid: string; ticket: string | null;
  kind: "entry" | "exit" | "refusal" | "cancellation"; result: string; resultLabel: string;
  zone: string; persons: number; at: string; receivedAt: string | null; device: string;
  mode: "online" | "offline"; input: string; product: string | null; tariff: string | null;
  session: string | null; reason: string | null; cancelledEntryAt: string | null;
}

export interface SaleV1 extends Sale {
  /** Numéro de l'écriture du journal fiscal. */
  seq: number | null;
  refund: boolean;
  refundOf: number | null;
  terminal: string | null;
}

export interface SessionV1 {
  id: string; date: string; start: string; end: string; startsAt: string; endsAt: string;
  activityId: string; activityName: string; label: string; lang: string; status: string; level: string;
  capacity: number; free: number; sold: number; held: number;
}

export interface HoldV1 { id: string; quantity: number; state: "hold" | "confirmed" | "released"; expiresAt: string | null }

export interface AttendanceCount { visitors: number; paid: number; free: number }

export interface AttendanceV1 {
  ok: true;
  kind: "admissions_issued";
  range: { from: string; to: string; group: "day" | "month" };
  total: { visitors: number; paid: number; free: number; admissionRevenue: number };
  sources: { journal: AttendanceCount | null; manual: AttendanceCount | null; imported: AttendanceCount | null };
  categories: { id: string; label: string; audience: string; audienceLabel: string; visitors: number; free: number; revenue: number }[];
  origin: { code: string; label: string; visitors: number }[];
  hours: { hour: number; visitors: number }[];
  series: { period: string; visitors: number; paid: number; free: number; counted: number; admissionRevenue: number; salesTotal: number }[];
  coverage: { reconciledDays: number; liveDays: number; incompleteDays: number };
}

export interface AttendanceYearV1 {
  ok: true;
  year: string;
  scope: "shop";
  shopId: string;
  /** shops : nombre d'établissements du site ; plus d'un, ces chiffres sont ceux de l'établissement de la clé seulement. */
  site: { id: string; name: string; museofileId: string; shops: number } | null;
  profile: string;
  rows: { id: string; label: string; value: number | null; unit: "" | "%"; sources: string; note: string }[];
  /** null quand le site compte plusieurs établissements. */
  openData: { museofileId: string; name: string; year: string; paid: number; free: number; total: number } | null;
  /** imported : historique importé, jamais compté ; null sans import. */
  sources: { journal: AttendanceCount | null; manual: AttendanceCount | null; imported: AttendanceCount | null };
  warnings: string[];
  note: string;
}

export interface StockLevelV1 {
  unit: string; itemId: string; variantId: string | null; sku: string | null; barcode: string | null; name: string; categoryId: string | null;
  quantity: number; byLocation: Record<string, number>; tracked: boolean; source: "native" | "web";
  min?: number; max?: number; low?: boolean; negative?: boolean; orphan?: boolean;
}

export interface StockMovementV1 {
  seq: number; id: string; kind: string; at: string; postedAt: string; terminal: string | null; reference: string;
  document: string | null; fiscalSeq: number | null; void: boolean;
  lines: { unit: string; location: string; quantity: number; name?: string; reason?: string }[];
}

export interface GroupBookingRow {
  id: string; number: string; status: string; date: string | null; start: string | null; organisation: string; organisationId: string | null;
  persons: number; adults: number; kind: string; optionUntil: string | null; training: boolean; updatedAt: string;
}

export interface DocumentRow {
  id: string; kind: "quote" | "invoice" | "deposit_request" | "deposit" | "credit" | "titre"; number: string | null; status: string;
  organisation: string; organisationId: string | null; bookingId: string | null; bookingNumber: string | null;
  gross: number; due: number; issueDate: string | null; dueDate: string | null; training: boolean;
}

export interface ApiClient {
  getShop(): Promise<ApiShop>;
  getCatalogue(): Promise<{ ok: true; categories: Category[]; items: Item[] }>;
  getItem(id: string): Promise<Item>;
  createItem(item: ItemInput, options?: IdempotencyOptions): Promise<Item>;
  updateItem(id: string, patch: Partial<ItemInput>): Promise<Item>;
  deleteItem(id: string): Promise<{ ok: true; deleted: string }>;
  getStock(): Promise<StockLine[]>;
  listSales(params?: { from?: string; to?: string; limit?: number }): Promise<{ ok: true; count: number; hasMore: boolean; sales: Sale[] }>;
  listOrders(params?: { status?: OrderStatus }): Promise<Order[]>;
  updateOrder(id: string, action: "accept" | "ready" | "collected" | "cancel"): Promise<Order>;
  listAccessGates(): Promise<{ ok: true; enabled: boolean; training: boolean; zones: { id: string; name: string; index: number }[]; gates: AccessGate[] }>;
  getAccessKeys(): Promise<{ ok: true; algorithm: "Ed25519"; format: "CBZ1"; site: string; current: number | null; keys: { kid: number; publicKey: string; scope: "tenant" | "device"; createdAt: string | null; notAfter: string | null; retiredAt: string | null; revokedAt: string | null }[] }>;
  validateTicket(input: AccessValidateInput): Promise<AccessVerdict>;
  getAccessAttendance(params?: { day?: string }): Promise<{ ok: true; kind: "validated_entries"; day: string; training: boolean; sealed: { revision: number; journalSeq: number; at: string; revisionPending: boolean } | null; counters: Record<string, number>; byHour: { hour: number; entries: number; visitors: number; exits: number }[]; byGate: { gate: string; name: string; entries: number; visitors: number; exits: number; refused: number }[]; byZone: { zone: string; name: string; index: number; entries: number }[]; byTariff: { product: string; productName: string | null; tariff: string; tariffName: string | null; entries: number }[]; refusals: { code: string; label: string; count: number }[]; present: { now: number; estimated: boolean; capacity: number; level: "ok" | "near" | "full" } | null }>;
  listAccessEvents(params?: { day?: string; gate?: string; since?: string; limit?: number }): Promise<{ ok: true; day: string; training: boolean; count: number; more: boolean; next: string; events: AccessEvent[] }>;
  listSalesPage(params?: { cursor?: string; from?: string; to?: string; include?: "refunds" }): Promise<{ ok: true; order: "oldest_first"; count: number; sales: SaleV1[]; nextCursor: string | null }>;
  getDayReports(params: { from: string; to?: string }): Promise<{ ok: true; range: Record<string, unknown>; totals: Record<string, unknown>; refunds: Record<string, unknown>; byMethod: Record<string, unknown>; vat: Record<string, unknown>[]; days: { day: string; sales: number; refunds: number; revenue: number; tips: number; total: number; covers: number; reconciled: boolean }[] }>;
  listFiscalEntries(params?: { after?: number; limit?: number }): Promise<{ ok: true; headSeq: number; entries: Record<string, unknown>[]; next: number | null }>;
  startExport(): Promise<{ ok: true; export: { id: string; startedAt: string; watermark: { seq: number; hash: string }; parts: { id: string; format: string; years: string[] | null; files: string[] }[] } }>;
  getExportPart(exportId: string, partId: string, params?: { year?: string; cursor?: string }): Promise<{ ok: true; text: string; count: number; nextCursor: string | null }>;
  listActivities(): Promise<{ id: string; name: string; kind: string; durationMin: number; capacity: number; maxPerBooking: number; public: boolean }[]>;
  listSessions(params?: { from?: string; to?: string; activity?: string }): Promise<SessionV1[]>;
  getSession(id: string): Promise<SessionV1>;
  holdSeats(sessionId: string, hold: { id: string; quantity: number; ttlMinutes?: number }, options?: IdempotencyOptions): Promise<HoldV1>;
  confirmHold(sessionId: string, holdId: string, reference: string): Promise<HoldV1>;
  releaseHold(sessionId: string, holdId: string, options?: { confirmed?: boolean }): Promise<{ ok: true }>;
  getAttendance(params?: { from?: string; to?: string; group?: "day" | "month" }): Promise<AttendanceV1>;
  /** Musées : rubriques de l'enquête annuelle de l'établissement de la clé (entrées émises). */
  getAttendanceYear(params?: { year?: string }): Promise<AttendanceYearV1>;
  getStockLevels(params?: { location?: string }): Promise<{ ok: true; active: boolean; untracked?: number; stale?: boolean; webUnavailable?: boolean; data: StockLevelV1[] }>;
  listStockLocations(): Promise<{ ok: true; active: boolean; sellLocation?: string; locations: { id: string; name: string; sells: boolean; archived: boolean }[] }>;
  listStockMovements(params?: { after?: number; limit?: number }): Promise<{ ok: true; headSeq: number; count: number; next: number | null; movements: StockMovementV1[] }>;
  listGroupBookings(params?: { month?: string }): Promise<GroupBookingRow[]>;
  getGroupBooking(id: string): Promise<Record<string, unknown>>;
  listDocuments(params?: { month?: string }): Promise<DocumentRow[]>;
  getDocument(id: string): Promise<Record<string, unknown>>;
  getFacturX(id: string): Promise<{ ok: true; filename: string; sha256: string; xml: string }>;
  listOrganisations(params?: { q?: string }): Promise<{ id: string; name: string; siret: string; city: string; kind: string; updatedAt: string }[]>;
  getOrganisation(id: string): Promise<Record<string, unknown>>;
  listPasses(params?: { q?: string; status?: string; product?: string; page?: number }): Promise<{ ok: true; training: boolean; total: number; page: number; pages: number; count: number; passes: { id: string; productId: string; holder: string; status: string; end: string | null }[] }>;
  getPass(id: string): Promise<Record<string, unknown>>;
  listWebOrders(params?: { day?: string; page?: number }): Promise<{ ok: true; day: string; total: number; page: number; more: boolean; count: number; orders: Record<string, unknown>[] }>;
  getWebOrder(ref: string): Promise<Record<string, unknown>>;
}

export declare function createApiClient(options: ApiClientOptions): ApiClient;

// ── synchronisation ────────────────────────────────────────────────────────

/** "woo:product:<id>" ou "woo:variant:<id>", id entier positif de votre boutique. */
export type RecordKey = string;

export interface Prices {
  regular?: number | string | null;
  sale?: number | string | null;
  /** Taux de TVA en pourcentage (20, 10, 5.5). */
  tax_rate?: number | null;
  /** "gross" (TTC, par défaut) ou "net" (HT, la caisse ajoute la TVA). */
  basis?: "gross" | "net";
}

export interface ProductSnapshot {
  key: RecordKey;
  name?: string;
  /** Seul "publish" garde l'article en caisse ; toute autre valeur le retire. */
  status?: "publish" | string;
  deleted?: boolean;
  archived?: boolean;
  sku?: string;
  /** Chemins de catégories, du plus général au plus précis : [["Vêtements", "Pantalons"]]. */
  categories?: string[][];
  prices?: Prices;
  identifiers?: { ean13?: string; gtin?: string; upc?: string };
  variants?: {
    key: RecordKey;
    status: "publish" | string;
    attributes?: Record<string, string>;
    sku?: string;
    prices?: Prices;
    identifiers?: { ean13?: string; gtin?: string; upc?: string };
  }[];
  /** Quantités de départ (ignorées pour une clé déjà connue : ensuite seuls les événements de stock comptent). */
  inventory?: { key: RecordKey; quantity: number | null }[];
}

export type SyncEvent =
  | { id?: string; kind: "product"; key: RecordKey; payload: { data: ProductSnapshot } }
  | { id?: string; kind: "stock"; key: RecordKey; payload: { delta: number } | { set_mode: true; quantity: number | null } }
  | { id?: string; kind: "order" | "customer"; key?: string; payload: Record<string, unknown> };

export interface SyncHealth {
  ok: true;
  protocol: 1;
  platform: "ps";
  software: "caisse.bzh";
  mode: "live" | "disabled";
  diagnostics: { ok: boolean; mode: string; queue: { direction: "out"; state: "pending"; count: number }[]; issues: unknown[] };
}

export interface SyncClientOptions extends ClientOptions {
  /** Jeton de la boutique (24 hex), page « Boutique en ligne » de la console. */
  token: string;
  /** Secret partagé (64 hex), même page. Côté serveur uniquement. */
  secret: string;
}

export interface SyncClient {
  health(): Promise<SyncHealth>;
  sendEvents(events: SyncEvent[]): Promise<{ ok: true; accepted: number }>;
  upsertProduct(product: ProductSnapshot, id?: string): Promise<{ ok: true; accepted: number }>;
  removeProduct(key: RecordKey, id?: string): Promise<{ ok: true; accepted: number }>;
  adjustStock(key: RecordKey, delta: number, id?: string): Promise<{ ok: true; accepted: number }>;
  setStock(key: RecordKey, quantity: number | null, id?: string): Promise<{ ok: true; accepted: number }>;
  tick(): Promise<{ ok: true }>;
}

export declare function createSyncClient(options: SyncClientOptions): SyncClient;

/** Message signé que la caisse envoie à votre webhook. */
export type TillMessage =
  | { version: 1; source: "ps"; op: "events"; events: { id: string; kind: "stock"; key: RecordKey; payload: { delta: number } }[] }
  | { version: 1; source: "ps"; op: "tick" };

export declare function sign(secret: string, body: string, timestamp: string, nonce: string): Promise<string>;
export declare function signedHeaders(secret: string, body: string): Promise<Record<string, string>>;
export declare function verifySignature(input: {
  secret: string;
  /** Corps brut, avant JSON.parse. */
  body: string;
  headers: Headers | Record<string, string | string[] | undefined>;
  toleranceSeconds?: number;
}): Promise<TillMessage>;
export declare function newEventId(): string;
export declare function randomHex(bytes: number): string;
