import type { z } from 'zod';
import {
  AuthResponseSchema,
  DemoAccountsResponseSchema,
  DemoLoginRequestSchema,
  LoginRequestSchema,
} from './schemas/auth';
import { MeResponseSchema } from './schemas/users';
import { HealthResponseSchema } from './schemas/health';
import {
  AssetCreateSchema,
  AssetComponentCreateSchema,
  AssetComponentSchema,
  AssetDetailSchema,
  AssetOptionsResponseSchema,
  AssetsResponseSchema,
  AssetUpdateSchema,
  CategoriesResponseSchema,
  CategoryCreateSchema,
  CategorySchema,
  CategoryUpdateSchema,
  LocationCreateSchema,
  LocationSchema,
  LocationsResponseSchema,
  LocationUpdateSchema,
  TeamCreateSchema,
  TeamSchema,
  TeamsResponseSchema,
  TeamUpdateSchema,
} from './schemas/referentiel';
import {
  ActivateRequestSchema,
  ActivateResponseSchema,
  InvitationCreateSchema,
  InvitationResponseSchema,
  RolesResponseSchema,
  UserAdminSchema,
  UsersResponseSchema,
  UserUpdateSchema,
} from './schemas/users-admin';
import {
  PortalAssetSchema,
  PortalRequestCreatedSchema,
  PortalRequestCreateSchema,
  PortalRequestStatusSchema,
} from './schemas/portail';
import {
  AnalyticsSummarySchema,
  DocumentCorpusUpdateSchema,
  DocumentSchema,
  DocumentsResponseSchema,
} from './schemas/documents';
import { SearchResponseSchema } from './schemas/search';
import {
  AssistantAnswerSchema,
  AssistantAskSchema,
  BilanSuggestionsResponseSchema,
  ReindexResultSchema,
  SuggestBilanSchema,
  TranscriptionResultSchema,
} from './schemas/assistant';
import {
  ConsumePartSchema,
  LaborTimeCreateSchema,
  PartCreateSchema,
  PartDetailSchema,
  PartnerCreateSchema,
  PartnerSchema,
  PartnersResponseSchema,
  PartnerUpdateSchema,
  PartsResponseSchema,
  PartUpdateSchema,
  PurchaseOrderCreateSchema,
  PurchaseOrderSchema,
  PurchaseOrdersResponseSchema,
  PurchaseOrderTransitionSchema,
  StockMovementCreateSchema,
} from './schemas/gestion';
import {
  MeterReadingCreateSchema,
  MetersResponseSchema,
  PreventiveGenerateSchema,
  PreventiveGenerationResultSchema,
  PreventiveStatusSchema,
  TaskTemplateCreateSchema,
  TaskTemplateSchema,
  TaskTemplatesResponseSchema,
  TaskTemplateUpdateSchema,
} from './schemas/preventif';
import {
  AssigneesUpdateSchema,
  ChecklistItemSchema,
  ChecklistPatchSchema,
  CommentCreateSchema,
  ReferenceValueCreateSchema,
  ReferenceValueSchema,
  ReferenceValuesResponseSchema,
  ReferenceValueUpdateSchema,
  ReportUpsertSchema,
  RequestApproveSchema,
  RequestCreateSchema,
  RequestRejectSchema,
  RequestsResponseSchema,
  RequestSummarySchema,
  TransitionRequestSchema,
  WorkOrderCreateSchema,
  WorkOrderDetailSchema,
  WorkOrdersResponseSchema,
} from './schemas/exploitation';

/**
 * Contrat d'API R0 — source unique de vérité (règle d'or ADR-001).
 * `scripts/generate-openapi.ts` en dérive `docs/openapi.json` (committée) ;
 * les clients web/mobile sont générés depuis cette spec, dans le même commit.
 */
export interface ApiOperation {
  operationId: string;
  method: 'get' | 'post' | 'put' | 'patch' | 'delete';
  path: string;
  summary: string;
  tags: string[];
  /** Route annotée @Public() côté API (pas de JWT requis). */
  isPublic?: boolean;
  /** ADR-002 : la route N'EXISTE PAS (404) si DEMO_MODE n'est pas actif. */
  demoOnly?: boolean;
  /** Paramètres de chemin (`{id}` dans path) — `id`/`…Id` = UUID. */
  pathParams?: string[];
  /** Paramètres de requête (?a=…&b=…). */
  queryParams?: { name: string; required?: boolean; description?: string }[];
  /** Upload multipart/form-data : champs déclarés ('file' = binaire). */
  multipartFields?: Record<string, 'file' | 'string'>;
  /** Réponse 200 binaire (téléchargement streamé). */
  binaryResponse?: boolean;
  request?: { name: string; schema: z.ZodType };
  responses: Record<
    number,
    { description: string; name?: string; schema?: z.ZodType }
  >;
}

export const API_CONTRACT: ApiOperation[] = [
  {
    operationId: 'login',
    method: 'post',
    path: '/auth/login',
    summary: 'Connexion par e-mail et mot de passe',
    tags: ['auth'],
    isPublic: true,
    request: { name: 'LoginRequest', schema: LoginRequestSchema },
    responses: {
      200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
      401: { description: 'Identifiants invalides ou compte inactif' },
    },
  },
  {
    operationId: 'listDemoAccounts',
    method: 'get',
    path: '/auth/demo-accounts',
    summary: 'Comptes de démonstration (ADR-002 — jamais de secret)',
    tags: ['auth', 'demo'],
    isPublic: true,
    demoOnly: true,
    responses: {
      200: {
        description: 'Comptes isDemo actifs',
        name: 'DemoAccountsResponse',
        schema: DemoAccountsResponseSchema,
      },
    },
  },
  {
    operationId: 'demoLogin',
    method: 'post',
    path: '/auth/demo-login',
    summary: 'Connexion 1 clic sur un compte de démonstration (ADR-002)',
    tags: ['auth', 'demo'],
    isPublic: true,
    demoOnly: true,
    request: { name: 'DemoLoginRequest', schema: DemoLoginRequestSchema },
    responses: {
      200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
      403: { description: 'Le compte n’est pas un compte de démonstration' },
      404: { description: 'Compte inconnu' },
    },
  },
  {
    operationId: 'getMe',
    method: 'get',
    path: '/users/me',
    summary: 'Profil courant + matrice de permissions du rôle',
    tags: ['users'],
    responses: {
      200: { description: 'Profil', name: 'MeResponse', schema: MeResponseSchema },
      401: { description: 'Non authentifié' },
    },
  },
  {
    operationId: 'activateAccount',
    method: 'post',
    path: '/auth/activate',
    summary: 'Activer un compte invité (lien 7 jours) — connecte directement',
    tags: ['auth'],
    isPublic: true,
    request: { name: 'ActivateRequest', schema: ActivateRequestSchema },
    responses: {
      200: { description: 'Compte activé et connecté', name: 'AuthResponse', schema: ActivateResponseSchema },
      400: { description: 'Lien invalide ou expiré' },
    },
  },

  // ————— R1 · Référentiel —————
  {
    operationId: 'listCategories',
    method: 'get',
    path: '/categories',
    summary: 'Référentiels administrables (catégories d’équipement, types d’organes)',
    tags: ['categories'],
    responses: {
      200: { description: 'Liste', name: 'CategoriesResponse', schema: CategoriesResponseSchema },
    },
  },
  {
    operationId: 'createCategory',
    method: 'post',
    path: '/categories',
    summary: 'Ajouter une catégorie',
    tags: ['categories'],
    request: { name: 'CategoryCreate', schema: CategoryCreateSchema },
    responses: {
      201: { description: 'Créée', name: 'Category', schema: CategorySchema },
      409: { description: 'Nom déjà utilisé pour ce type' },
    },
  },
  {
    operationId: 'updateCategory',
    method: 'patch',
    path: '/categories/{id}',
    summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée',
    tags: ['categories'],
    pathParams: ['id'],
    request: { name: 'CategoryUpdate', schema: CategoryUpdateSchema },
    responses: {
      200: { description: 'Mise à jour', name: 'Category', schema: CategorySchema },
      404: { description: 'Inconnue' },
    },
  },
  {
    operationId: 'listLocations',
    method: 'get',
    path: '/locations',
    summary: 'Sites et zones (liste plate, le client construit l’arbre)',
    tags: ['locations'],
    responses: {
      200: { description: 'Liste', name: 'LocationsResponse', schema: LocationsResponseSchema },
    },
  },
  {
    operationId: 'createLocation',
    method: 'post',
    path: '/locations',
    summary: 'Créer un site (sans parent) ou une zone (profondeur max 2)',
    tags: ['locations'],
    request: { name: 'LocationCreate', schema: LocationCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'Location', schema: LocationSchema },
      400: { description: 'Hiérarchie trop profonde' },
    },
  },
  {
    operationId: 'updateLocation',
    method: 'patch',
    path: '/locations/{id}',
    summary: 'Modifier un emplacement',
    tags: ['locations'],
    pathParams: ['id'],
    request: { name: 'LocationUpdate', schema: LocationUpdateSchema },
    responses: {
      200: { description: 'Mis à jour', name: 'Location', schema: LocationSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'listAssets',
    method: 'get',
    path: '/assets',
    summary: 'Inventaire des appareils',
    tags: ['assets'],
    responses: {
      200: { description: 'Liste', name: 'AssetsResponse', schema: AssetsResponseSchema },
    },
  },
  {
    operationId: 'listAssetOptions',
    method: 'get',
    path: '/assets/options',
    summary: 'Options d’appareils pour le signalement (tout rôle authentifié)',
    tags: ['assets'],
    responses: {
      200: {
        description: 'Options',
        name: 'AssetOptionsResponse',
        schema: AssetOptionsResponseSchema,
      },
    },
  },
  {
    operationId: 'getAsset',
    method: 'get',
    path: '/assets/{id}',
    summary: 'Fiche appareil (identité + organes)',
    tags: ['assets'],
    pathParams: ['id'],
    responses: {
      200: { description: 'Fiche', name: 'AssetDetail', schema: AssetDetailSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'createAsset',
    method: 'post',
    path: '/assets',
    summary: 'Créer un appareil (avec ses organes) — le QR découle de la référence',
    tags: ['assets'],
    request: { name: 'AssetCreate', schema: AssetCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'AssetDetail', schema: AssetDetailSchema },
      409: { description: 'Référence déjà utilisée' },
    },
  },
  {
    operationId: 'updateAsset',
    method: 'patch',
    path: '/assets/{id}',
    summary: 'Modifier un appareil (dont son statut d’équipement)',
    tags: ['assets'],
    pathParams: ['id'],
    request: { name: 'AssetUpdate', schema: AssetUpdateSchema },
    responses: {
      200: { description: 'Mis à jour', name: 'AssetDetail', schema: AssetDetailSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'addAssetComponent',
    method: 'post',
    path: '/assets/{id}/components',
    summary: 'Ajouter un organe (jamais d’emplacement propre)',
    tags: ['assets'],
    pathParams: ['id'],
    request: { name: 'AssetComponentCreate', schema: AssetComponentCreateSchema },
    responses: {
      201: { description: 'Ajouté', name: 'AssetComponent', schema: AssetComponentSchema },
      400: { description: 'Le type choisi n’est pas un type d’organe' },
    },
  },
  {
    operationId: 'removeAssetComponent',
    method: 'delete',
    path: '/assets/{id}/components/{componentId}',
    summary: 'Retirer un organe',
    tags: ['assets'],
    pathParams: ['id', 'componentId'],
    responses: {
      204: { description: 'Retiré' },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'listTeams',
    method: 'get',
    path: '/teams',
    summary: 'Équipes et leurs membres',
    tags: ['teams'],
    responses: {
      200: { description: 'Liste', name: 'TeamsResponse', schema: TeamsResponseSchema },
    },
  },
  {
    operationId: 'createTeam',
    method: 'post',
    path: '/teams',
    summary: 'Créer une équipe',
    tags: ['teams'],
    request: { name: 'TeamCreate', schema: TeamCreateSchema },
    responses: {
      201: { description: 'Créée', name: 'Team', schema: TeamSchema },
      409: { description: 'Nom déjà utilisé' },
    },
  },
  {
    operationId: 'updateTeam',
    method: 'patch',
    path: '/teams/{id}',
    summary: 'Modifier une équipe (nom, description, membres)',
    tags: ['teams'],
    pathParams: ['id'],
    request: { name: 'TeamUpdate', schema: TeamUpdateSchema },
    responses: {
      200: { description: 'Mise à jour', name: 'Team', schema: TeamSchema },
      404: { description: 'Inconnue' },
    },
  },
  {
    operationId: 'listUsers',
    method: 'get',
    path: '/users',
    summary: 'Personnes (statut dérivé : actif / invité / désactivé)',
    tags: ['users'],
    responses: {
      200: { description: 'Liste', name: 'UsersResponse', schema: UsersResponseSchema },
    },
  },
  {
    operationId: 'listRoles',
    method: 'get',
    path: '/roles',
    summary: 'Les 7 rôles (pour l’invitation)',
    tags: ['users'],
    responses: {
      200: { description: 'Liste', name: 'RolesResponse', schema: RolesResponseSchema },
    },
  },
  {
    operationId: 'inviteUser',
    method: 'post',
    path: '/users/invitations',
    summary: 'Inviter — crée le compte inactif et émet le lien d’activation (7 j)',
    tags: ['users'],
    request: { name: 'InvitationCreate', schema: InvitationCreateSchema },
    responses: {
      201: { description: 'Invitation émise', name: 'InvitationResponse', schema: InvitationResponseSchema },
      409: { description: 'Email déjà utilisé' },
    },
  },
  {
    operationId: 'resendInvitation',
    method: 'post',
    path: '/users/{id}/invitation',
    summary: 'Régénérer le lien d’activation d’un compte non activé',
    tags: ['users'],
    pathParams: ['id'],
    responses: {
      201: { description: 'Nouveau lien', name: 'InvitationResponse', schema: InvitationResponseSchema },
      409: { description: 'Compte déjà activé' },
    },
  },
  {
    operationId: 'updateUser',
    method: 'patch',
    path: '/users/{id}',
    summary: 'Modifier une personne (rôle, équipes, activation du compte)',
    tags: ['users'],
    pathParams: ['id'],
    request: { name: 'UserUpdate', schema: UserUpdateSchema },
    responses: {
      200: { description: 'Mise à jour', name: 'UserAdmin', schema: UserAdminSchema },
      404: { description: 'Inconnue' },
    },
  },
  // ————— R3 · Bibliothèque & analytics —————
  {
    operationId: 'listDocuments',
    method: 'get',
    path: '/documents',
    summary: 'Bibliothèque (filtrable par appareil ou OT)',
    tags: ['documents'],
    queryParams: [{ name: 'assetId' }, { name: 'workOrderId' }, { name: 'kind' }],
    responses: {
      200: { description: 'Liste', name: 'DocumentsResponse', schema: DocumentsResponseSchema },
    },
  },
  {
    operationId: 'uploadDocument',
    method: 'post',
    path: '/documents',
    summary: 'Téléverser (PDF/JPG/PNG, 20 Mo max, rattachement appareil OU OT requis)',
    tags: ['documents'],
    multipartFields: { file: 'file', kind: 'string', assetId: 'string', workOrderId: 'string' },
    responses: {
      201: { description: 'Document rangé', name: 'Document', schema: DocumentSchema },
      400: { description: 'Type/taille refusé ou rattachement manquant' },
    },
  },
  {
    operationId: 'downloadDocument',
    method: 'get',
    path: '/documents/{id}/download',
    summary: 'Télécharger — streamé par l’API (MinIO jamais exposé)',
    tags: ['documents'],
    pathParams: ['id'],
    binaryResponse: true,
    responses: {
      200: { description: 'Fichier' },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'updateDocumentCorpus',
    method: 'patch',
    path: '/documents/{id}/corpus',
    summary: 'Inclure/exclure du corpus IA (D3 — réversible, effectif à la prochaine réindexation)',
    tags: ['documents'],
    pathParams: ['id'],
    request: { name: 'DocumentCorpusUpdate', schema: DocumentCorpusUpdateSchema },
    responses: {
      200: { description: 'Document mis à jour', name: 'Document', schema: DocumentSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'deleteDocument',
    method: 'delete',
    path: '/documents/{id}',
    summary: 'Supprimer (permission d’édition sur la cible)',
    tags: ['documents'],
    pathParams: ['id'],
    responses: {
      204: { description: 'Supprimé' },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'getAnalyticsSummary',
    method: 'get',
    path: '/analytics/summary',
    summary: 'Le tableau de la direction : coûts, pannes par organe (bilans), préventif, top équipements',
    tags: ['analytics'],
    queryParams: [{ name: 'months', description: 'Période en mois : 3, 6 ou 12 (défaut)' }],
    responses: {
      200: { description: 'Synthèse', name: 'AnalyticsSummary', schema: AnalyticsSummarySchema },
    },
  },
  {
    operationId: 'askAssistant',
    method: 'post',
    path: '/assistant/ask',
    summary: 'Assistant R5 — sourcé ou silencieux : extraits cités ou refus honnête (D2)',
    tags: ['assistant'],
    request: { name: 'AssistantAsk', schema: AssistantAskSchema },
    responses: {
      200: { description: 'Réponse sourcée ou refus', name: 'AssistantAnswer', schema: AssistantAnswerSchema },
      503: { description: 'Service IA indisponible' },
    },
  },
  {
    operationId: 'suggestBilan',
    method: 'post',
    path: '/assistant/suggest-bilan',
    summary: 'Suggérer des codes de bilan depuis une description libre (D1 — l’humain valide)',
    tags: ['assistant'],
    request: { name: 'SuggestBilan', schema: SuggestBilanSchema },
    responses: {
      200: { description: 'Suggestions (codes existants seulement)', name: 'BilanSuggestions', schema: BilanSuggestionsResponseSchema },
      503: { description: 'Service IA indisponible' },
    },
  },
  {
    operationId: 'reindexAssistant',
    method: 'post',
    path: '/assistant/reindex',
    summary: 'Réindexer le corpus (bibliothèque PDF + bilans clôturés, anonymisés à l’ingestion — D4)',
    tags: ['assistant'],
    responses: {
      200: { description: 'Bilan d’indexation', name: 'ReindexResult', schema: ReindexResultSchema },
      503: { description: 'Service IA indisponible' },
    },
  },
  {
    operationId: 'transcribeAudio',
    method: 'post',
    path: '/assistant/transcribe',
    summary: 'Dictée (R5 D5, opt-in) — l’audio est transcrit puis JAMAIS conservé, à relire avant tout usage',
    tags: ['assistant'],
    multipartFields: { file: 'file' },
    responses: {
      200: { description: 'Texte transcrit — à relire (D1)', name: 'TranscriptionResult', schema: TranscriptionResultSchema },
      503: { description: 'Dictée non activée ou service IA indisponible' },
    },
  },
  {
    operationId: 'globalSearch',
    method: 'get',
    path: '/search',
    summary: 'Recherche globale (OT, ascenseurs, sites) — chaque famille filtrée par la matrice',
    tags: ['search'],
    queryParams: [{ name: 'q', required: true, description: '2 caractères minimum' }],
    responses: {
      200: { description: 'Résultats (5 max par famille)', name: 'SearchResponse', schema: SearchResponseSchema },
    },
  },

  // ————— R3 · Gestion (stock, achats, tiers, coûts) —————
  {
    operationId: 'listPartners',
    method: 'get',
    path: '/partners',
    summary: 'Tiers (fournisseurs, syndics) — permission PURCHASE_ORDERS',
    tags: ['partners'],
    responses: {
      200: { description: 'Liste', name: 'PartnersResponse', schema: PartnersResponseSchema },
    },
  },
  {
    operationId: 'createPartner',
    method: 'post',
    path: '/partners',
    summary: 'Créer un tiers',
    tags: ['partners'],
    request: { name: 'PartnerCreate', schema: PartnerCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'Partner', schema: PartnerSchema },
      409: { description: 'Nom déjà utilisé' },
    },
  },
  {
    operationId: 'updatePartner',
    method: 'patch',
    path: '/partners/{id}',
    summary: 'Modifier / (dés)activer un tiers',
    tags: ['partners'],
    pathParams: ['id'],
    request: { name: 'PartnerUpdate', schema: PartnerUpdateSchema },
    responses: {
      200: { description: 'Mis à jour', name: 'Partner', schema: PartnerSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'listParts',
    method: 'get',
    path: '/parts',
    summary: 'Stock de pièces — quantités DÉRIVÉES des mouvements',
    tags: ['parts'],
    responses: {
      200: { description: 'Liste', name: 'PartsResponse', schema: PartsResponseSchema },
    },
  },
  {
    operationId: 'getPart',
    method: 'get',
    path: '/parts/{id}',
    summary: 'Fiche pièce : les mouvements SONT le stock',
    tags: ['parts'],
    pathParams: ['id'],
    responses: {
      200: { description: 'Fiche', name: 'PartDetail', schema: PartDetailSchema },
      404: { description: 'Inconnue' },
    },
  },
  {
    operationId: 'createPart',
    method: 'post',
    path: '/parts',
    summary: 'Créer une pièce (référence P-#### générée)',
    tags: ['parts'],
    request: { name: 'PartCreate', schema: PartCreateSchema },
    responses: {
      201: { description: 'Créée', name: 'PartDetail', schema: PartDetailSchema },
    },
  },
  {
    operationId: 'updatePart',
    method: 'patch',
    path: '/parts/{id}',
    summary: 'Modifier une pièce (désignation, seuil, fournisseur…)',
    tags: ['parts'],
    pathParams: ['id'],
    request: { name: 'PartUpdate', schema: PartUpdateSchema },
    responses: {
      200: { description: 'Mise à jour', name: 'PartDetail', schema: PartDetailSchema },
      404: { description: 'Inconnue' },
    },
  },
  {
    operationId: 'addStockMovement',
    method: 'post',
    path: '/parts/{id}/movements',
    summary: 'Entrée manuelle (+) ou ajustement (± motif REQUIS) — jamais de saisie de stock',
    tags: ['parts'],
    pathParams: ['id'],
    request: { name: 'StockMovementCreate', schema: StockMovementCreateSchema },
    responses: {
      201: { description: 'Mouvement tracé', name: 'PartDetail', schema: PartDetailSchema },
      409: { description: 'Le stock ne peut pas devenir négatif' },
    },
  },
  {
    operationId: 'listPurchaseOrders',
    method: 'get',
    path: '/purchase-orders',
    summary: 'Bons de commande',
    tags: ['purchase-orders'],
    responses: {
      200: {
        description: 'Liste',
        name: 'PurchaseOrdersResponse',
        schema: PurchaseOrdersResponseSchema,
      },
    },
  },
  {
    operationId: 'getPurchaseOrder',
    method: 'get',
    path: '/purchase-orders/{id}',
    summary: 'Fiche BC (lignes, total)',
    tags: ['purchase-orders'],
    pathParams: ['id'],
    responses: {
      200: { description: 'Fiche', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'createPurchaseOrder',
    method: 'post',
    path: '/purchase-orders',
    summary: 'Créer un BC (brouillon)',
    tags: ['purchase-orders'],
    request: { name: 'PurchaseOrderCreate', schema: PurchaseOrderCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
      400: { description: 'Fournisseur ou pièce invalide' },
    },
  },
  {
    operationId: 'transitionPurchaseOrder',
    method: 'post',
    path: '/purchase-orders/{id}/transition',
    summary: 'Envoyer / réceptionner (→ entrées de stock, PU figés) / annuler',
    tags: ['purchase-orders'],
    pathParams: ['id'],
    request: { name: 'PurchaseOrderTransition', schema: PurchaseOrderTransitionSchema },
    responses: {
      200: { description: 'État changé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
      409: { description: 'Transition interdite' },
    },
  },
  {
    operationId: 'consumePart',
    method: 'post',
    path: '/work-orders/{id}/consume-part',
    summary: 'Consommer une pièce sur l’OT — stock décrémenté, PRIX FIGÉ',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'ConsumePart', schema: ConsumePartSchema },
    responses: {
      201: { description: 'Consommée', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      409: { description: 'Stock insuffisant ou prix inconnu' },
    },
  },
  {
    operationId: 'addLaborTime',
    method: 'post',
    path: '/work-orders/{id}/labor',
    summary: 'Saisir de la main-d’œuvre — TAUX FIGÉ à la saisie',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'LaborTimeCreate', schema: LaborTimeCreateSchema },
    responses: {
      201: { description: 'Saisie', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      409: { description: 'Taux horaire non défini pour cette personne' },
    },
  },

  // ————— R2 · Portail public (QR cabine — aucun compte) —————
  {
    operationId: 'getPortalAsset',
    method: 'get',
    path: '/portal/assets/{reference}',
    summary: 'Résoudre le QR scanné (référence → appareil, préremplissage)',
    tags: ['portal'],
    isPublic: true,
    pathParams: ['reference'],
    responses: {
      200: { description: 'Appareil', name: 'PortalAsset', schema: PortalAssetSchema },
      404: { description: 'Référence inconnue' },
    },
  },
  {
    operationId: 'createPortalRequest',
    method: 'post',
    path: '/portal/requests',
    summary: 'Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone)',
    tags: ['portal'],
    isPublic: true,
    request: { name: 'PortalRequestCreate', schema: PortalRequestCreateSchema },
    responses: {
      201: {
        description: 'Signalement enregistré',
        name: 'PortalRequestCreated',
        schema: PortalRequestCreatedSchema,
      },
      404: { description: 'Référence inconnue' },
      429: { description: 'Trop de signalements — réessayez dans une minute' },
    },
  },
  {
    operationId: 'getPortalRequestStatus',
    method: 'get',
    path: '/portal/requests/{reference}/{token}',
    summary: 'Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis',
    tags: ['portal'],
    isPublic: true,
    pathParams: ['reference', 'token'],
    responses: {
      200: { description: 'Avancement', name: 'PortalRequestStatus', schema: PortalRequestStatusSchema },
      404: { description: 'Signalement inconnu ou jeton invalide' },
    },
  },

  // ————— R2 · Préventif & compteurs —————
  {
    operationId: 'listTaskTemplates',
    method: 'get',
    path: '/preventive/templates',
    summary: 'Gabarits de tâches du préventif (périodicité en mois)',
    tags: ['preventive'],
    responses: {
      200: { description: 'Liste', name: 'TaskTemplatesResponse', schema: TaskTemplatesResponseSchema },
    },
  },
  {
    operationId: 'createTaskTemplate',
    method: 'post',
    path: '/preventive/templates',
    summary: 'Ajouter un gabarit',
    tags: ['preventive'],
    request: { name: 'TaskTemplateCreate', schema: TaskTemplateCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'TaskTemplate', schema: TaskTemplateSchema },
      409: { description: 'Libellé déjà utilisé' },
    },
  },
  {
    operationId: 'updateTaskTemplate',
    method: 'patch',
    path: '/preventive/templates/{id}',
    summary: 'Modifier / (dés)activer un gabarit — jamais de suppression si utilisé',
    tags: ['preventive'],
    pathParams: ['id'],
    request: { name: 'TaskTemplateUpdate', schema: TaskTemplateUpdateSchema },
    responses: {
      200: { description: 'Mis à jour', name: 'TaskTemplate', schema: TaskTemplateSchema },
      404: { description: 'Inconnu' },
    },
  },
  {
    operationId: 'generatePreventive',
    method: 'post',
    path: '/preventive/generate',
    summary: 'Générer la grille du mois — IDEMPOTENT (regénérer ne double rien)',
    tags: ['preventive'],
    request: { name: 'PreventiveGenerate', schema: PreventiveGenerateSchema },
    responses: {
      200: {
        description: 'Résumé de génération',
        name: 'PreventiveGenerationResult',
        schema: PreventiveGenerationResultSchema,
      },
    },
  },
  {
    operationId: 'getPreventiveStatus',
    method: 'get',
    path: '/preventive/status',
    summary: 'État du mois courant (générées, terminées, en retard, premiers contrôles)',
    tags: ['preventive'],
    responses: {
      200: { description: 'État', name: 'PreventiveStatus', schema: PreventiveStatusSchema },
    },
  },
  {
    operationId: 'getAssetMeters',
    method: 'get',
    path: '/assets/{id}/meters',
    summary: 'Compteurs de l’appareil (relevés récents d’abord)',
    tags: ['meters'],
    pathParams: ['id'],
    responses: {
      200: { description: 'Compteurs', name: 'MetersResponse', schema: MetersResponseSchema },
      404: { description: 'Appareil inconnu' },
    },
  },
  {
    operationId: 'addMeterReading',
    method: 'post',
    path: '/assets/{id}/meter-readings',
    summary: 'Enregistrer un relevé — strictement croissant',
    tags: ['meters'],
    pathParams: ['id'],
    request: { name: 'MeterReadingCreate', schema: MeterReadingCreateSchema },
    responses: {
      201: { description: 'Relevé enregistré', name: 'MetersResponse', schema: MetersResponseSchema },
      400: { description: 'Relevé inférieur ou égal au précédent' },
    },
  },

  // ————— R2 · Exploitation —————
  {
    operationId: 'listWorkOrders',
    method: 'get',
    path: '/work-orders',
    summary: 'Ordres de travail (sans « voir autre » : seulement les siens)',
    tags: ['work-orders'],
    responses: {
      200: { description: 'Liste', name: 'WorkOrdersResponse', schema: WorkOrdersResponseSchema },
    },
  },
  {
    operationId: 'getWorkOrder',
    method: 'get',
    path: '/work-orders/{id}',
    summary: 'Fiche OT (activité, checklist, bilan, transitions autorisées, blocages de clôture)',
    tags: ['work-orders'],
    pathParams: ['id'],
    responses: {
      200: { description: 'Fiche', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      404: { description: 'Inconnu (ou hors de son périmètre)' },
    },
  },
  {
    operationId: 'createWorkOrder',
    method: 'post',
    path: '/work-orders',
    summary: 'Créer un OT (statut Ouvert)',
    tags: ['work-orders'],
    request: { name: 'WorkOrderCreate', schema: WorkOrderCreateSchema },
    responses: {
      201: { description: 'Créé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
    },
  },
  {
    operationId: 'transitionWorkOrder',
    method: 'post',
    path: '/work-orders/{id}/transition',
    summary: 'Changer l’état (machine à états stricte ; DONE exige bilan + checklist)',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'TransitionRequest', schema: TransitionRequestSchema },
    responses: {
      200: { description: 'État changé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      409: { description: 'Transition interdite ou clôture bloquée (garde)' },
    },
  },
  {
    operationId: 'commentWorkOrder',
    method: 'post',
    path: '/work-orders/{id}/comments',
    summary: 'Commenter (activité chronologique)',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'CommentCreate', schema: CommentCreateSchema },
    responses: {
      201: { description: 'Commentaire ajouté', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
    },
  },
  {
    operationId: 'setWorkOrderAssignees',
    method: 'put',
    path: '/work-orders/{id}/assignees',
    summary: 'Assigner (remplace la liste)',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'AssigneesUpdate', schema: AssigneesUpdateSchema },
    responses: {
      200: { description: 'Assignés', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
    },
  },
  {
    operationId: 'upsertWorkOrderReport',
    method: 'put',
    path: '/work-orders/{id}/report',
    summary: 'Renseigner le bilan codé (null efface un champ)',
    tags: ['work-orders'],
    pathParams: ['id'],
    request: { name: 'ReportUpsert', schema: ReportUpsertSchema },
    responses: {
      200: { description: 'Bilan enregistré', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      400: { description: 'Valeur hors référentiel du champ' },
    },
  },
  {
    operationId: 'patchChecklistItem',
    method: 'patch',
    path: '/work-orders/{id}/checklist/{itemId}',
    summary: 'Régler une tâche de la grille (Fait / N-A / à faire)',
    tags: ['work-orders'],
    pathParams: ['id', 'itemId'],
    request: { name: 'ChecklistPatch', schema: ChecklistPatchSchema },
    responses: {
      200: { description: 'Tâche réglée', name: 'ChecklistItem', schema: ChecklistItemSchema },
      404: { description: 'Tâche inconnue' },
    },
  },
  {
    operationId: 'listRequests',
    method: 'get',
    path: '/requests',
    summary: 'Demandes (sans « voir autre » : seulement les siennes)',
    tags: ['requests'],
    responses: {
      200: { description: 'Liste', name: 'RequestsResponse', schema: RequestsResponseSchema },
    },
  },
  {
    operationId: 'createRequest',
    method: 'post',
    path: '/requests',
    summary: 'Signaler (interne — le portail public QR arrive en R2.4)',
    tags: ['requests'],
    request: { name: 'RequestCreate', schema: RequestCreateSchema },
    responses: {
      201: { description: 'Demande créée', name: 'RequestSummary', schema: RequestSummarySchema },
    },
  },
  {
    operationId: 'approveRequest',
    method: 'post',
    path: '/requests/{id}/approve',
    summary: 'Approuver → crée l’OT lié (1-1, jamais de doublon)',
    tags: ['requests'],
    pathParams: ['id'],
    request: { name: 'RequestApprove', schema: RequestApproveSchema },
    responses: {
      201: { description: 'OT créé et lié', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
      409: { description: 'Demande déjà traitée' },
    },
  },
  {
    operationId: 'rejectRequest',
    method: 'post',
    path: '/requests/{id}/reject',
    summary: 'Rejeter — motif obligatoire, lisible côté demandeur',
    tags: ['requests'],
    pathParams: ['id'],
    request: { name: 'RequestReject', schema: RequestRejectSchema },
    responses: {
      200: { description: 'Rejetée', name: 'RequestSummary', schema: RequestSummarySchema },
      409: { description: 'Demande déjà traitée' },
    },
  },
  {
    operationId: 'listReferenceValues',
    method: 'get',
    path: '/reference-values',
    summary: 'Référentiels du bilan codé (6 champs)',
    tags: ['reference-values'],
    responses: {
      200: {
        description: 'Liste',
        name: 'ReferenceValuesResponse',
        schema: ReferenceValuesResponseSchema,
      },
    },
  },
  {
    operationId: 'createReferenceValue',
    method: 'post',
    path: '/reference-values',
    summary: 'Ajouter une valeur de référentiel',
    tags: ['reference-values'],
    request: { name: 'ReferenceValueCreate', schema: ReferenceValueCreateSchema },
    responses: {
      201: { description: 'Créée', name: 'ReferenceValue', schema: ReferenceValueSchema },
      409: { description: 'Libellé déjà présent pour ce champ' },
    },
  },
  {
    operationId: 'updateReferenceValue',
    method: 'patch',
    path: '/reference-values/{id}',
    summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée',
    tags: ['reference-values'],
    pathParams: ['id'],
    request: { name: 'ReferenceValueUpdate', schema: ReferenceValueUpdateSchema },
    responses: {
      200: { description: 'Mise à jour', name: 'ReferenceValue', schema: ReferenceValueSchema },
      404: { description: 'Inconnue' },
    },
  },
  {
    operationId: 'getHealth',
    method: 'get',
    path: '/health',
    summary: 'État des dépendances (base, Redis, stockage)',
    tags: ['health'],
    isPublic: true,
    responses: {
      200: {
        description: 'État agrégé',
        name: 'HealthResponse',
        schema: HealthResponseSchema,
      },
    },
  },
];
