import { z } from "zod";

/* ------------------------------------------------------------------ */
/* Primitives                                                          */
/* ------------------------------------------------------------------ */

export const idSchema = z.string().uuid();
export type Id = z.infer<typeof idSchema>;

export const emailSchema = z.string().email().max(320);

export const emojiSchema = z.string().min(1).max(16);

/* ------------------------------------------------------------------ */
/* Rôles & permissions                                                 */
/* ------------------------------------------------------------------ */

/** Rôle dans la famille — source unique consommée par serveur et mobile. */
export const roleSchema = z.enum(["admin", "coadmin", "viewer"]);
export type Role = z.infer<typeof roleSchema>;

/**
 * Droits d'un viewer : la visu est toujours acquise (« view »), les options
 * + Réactions / + Téléchargement sont cumulables (choix multiple).
 * `full` = les deux options (ancien statut plein). Réglé uniquement sur les
 * viewers — les gestionnaires sont toujours en `full` implicitement.
 */
export const permissionSchema = z.enum(["full", "download", "reactions", "view"]);
export type Permission = z.infer<typeof permissionSchema>;

const ROLE_RANK = { viewer: 0, coadmin: 1, admin: 2 } as const;

/** `true` si `r` vaut au moins `min` dans la hiérarchie viewer < coadmin < admin. */
export const hasRole = (r: Role, min: Role): boolean =>
  ROLE_RANK[r] >= ROLE_RANK[min];

/** admin OU coadmin — gère la famille (réglages, invitations, médias…). */
export const canManage = (r: Role): boolean => hasRole(r, "coadmin");

/**
 * Peut télécharger / partager un média. Les gestionnaires oui, les viewers
 * uniquement si l'option « + Téléchargement » est active (`download`/`full`).
 */
export const canDownload = (r: Role, p: Permission): boolean =>
  canManage(r) || p === "download" || p === "full";

/**
 * Peut réagir / commenter. Les gestionnaires oui, les viewers uniquement si
 * l'option « + Réactions » est active (`reactions`/`full`).
 */
export const canSocial = (r: Role, p: Permission): boolean =>
  canManage(r) || p === "reactions" || p === "full";

/* ------------------------------------------------------------------ */
/* Auth                                                               */
/* ------------------------------------------------------------------ */

export const authTokenPairSchema = z.object({
  accessToken: z.string(),
  refreshToken: z.string(),
});
export type AuthTokenPair = z.infer<typeof authTokenPairSchema>;

export const userSchema = z.object({
  id: idSchema,
  email: emailSchema.nullable(),
  displayName: z.string(),
  createdAt: z.string(),
});
export type User = z.infer<typeof userSchema>;

export const familySchema = z.object({
  id: idSchema,
  name: z.string().min(1).max(120),
  createdAt: z.string(),
  role: roleSchema,
  /** Statut du membre courant dans cette famille (réglé sur les viewers). */
  permission: permissionSchema,
  /** Réglages famille (posés par admin/coadmin) — réactions & commentaires. */
  reactionsEnabled: z.boolean(),
  commentsEnabled: z.boolean(),
  /** Créateur de la famille (permet de dériver isOwner côté client). */
  createdBy: idSchema.nullable(),
});
export type Family = z.infer<typeof familySchema>;

export const registerRequestSchema = z.object({
  email: emailSchema,
  password: z.string().min(8).max(200),
  displayName: z.string().min(1).max(100),
  familyName: z.string().min(1).max(120),
});
export type RegisterRequest = z.infer<typeof registerRequestSchema>;

export const loginRequestSchema = z.object({
  email: emailSchema,
  password: z.string().max(200),
});
export type LoginRequest = z.infer<typeof loginRequestSchema>;

/* --- Mot de passe oublié (preuve par Login Flow v2 Nextcloud) --- */

export const forgotPasswordRequestSchema = z.object({
  email: emailSchema,
});
export type ForgotPasswordRequest = z.infer<typeof forgotPasswordRequestSchema>;

/** Renvoie l'URL du Nextcloud familial à prouver — 404 générique sinon. */
export const forgotPasswordResponseSchema = z.object({
  baseUrl: z.string().url(),
});
export type ForgotPasswordResponse = z.infer<typeof forgotPasswordResponseSchema>;

/** Étape 1 du relais : l'URL doit correspondre à la connexion stockée (anti-SSRF). */
export const forgotPasswordLoginFlowStartRequestSchema = z.object({
  email: emailSchema,
  baseUrl: z.string().url().regex(/^https:\/\//i, "HTTPS requis"),
});
export type ForgotPasswordLoginFlowStartRequest = z.infer<
  typeof forgotPasswordLoginFlowStartRequestSchema
>;

/** Étape 2 du relais : à la validation, renvoie un jeton de reset à usage unique. */
export const forgotPasswordLoginFlowPollRequestSchema = z.object({
  email: emailSchema,
  baseUrl: z.string().url().regex(/^https:\/\//i, "HTTPS requis"),
  endpoint: z.string().url(),
  token: z.string().min(1),
});
export type ForgotPasswordLoginFlowPollRequest = z.infer<
  typeof forgotPasswordLoginFlowPollRequestSchema
>;

export const resetPasswordRequestSchema = z.object({
  resetToken: z.string().min(1),
  password: z.string().min(8).max(200),
});
export type ResetPasswordRequest = z.infer<typeof resetPasswordRequestSchema>;

export const refreshRequestSchema = z.object({
  refreshToken: z.string(),
});
export type RefreshRequest = z.infer<typeof refreshRequestSchema>;

export const logoutRequestSchema = z.object({
  refreshToken: z.string(),
});
export type LogoutRequest = z.infer<typeof logoutRequestSchema>;

export const authResponseSchema = z.object({
  user: userSchema,
  families: z.array(familySchema),
  tokens: authTokenPairSchema,
});
export type AuthResponse = z.infer<typeof authResponseSchema>;

export const meResponseSchema = z.object({
  user: userSchema,
  families: z.array(familySchema),
});
export type MeResponse = z.infer<typeof meResponseSchema>;

export const familyMemberSchema = z.object({
  userId: idSchema,
  displayName: z.string(),
  role: roleSchema,
  permission: permissionSchema,
  /** Le membre est le créateur de la famille (intouchable). */
  isOwner: z.boolean(),
  joinedAt: z.string(),
});
export type FamilyMember = z.infer<typeof familyMemberSchema>;

/** Corps de PATCH /families/:familyId/members/:userId. */
export const updateMemberRequestSchema = z.object({
  role: roleSchema.optional(),
  permission: permissionSchema.optional(),
});
export type UpdateMemberRequest = z.infer<typeof updateMemberRequestSchema>;

/** Corps de PATCH /families/:familyId/social (réglages famille). */
export const updateSocialSettingsRequestSchema = z.object({
  reactionsEnabled: z.boolean().optional(),
  commentsEnabled: z.boolean().optional(),
});
export type UpdateSocialSettingsRequest = z.infer<typeof updateSocialSettingsRequestSchema>;

/** Préférences de notification du membre courant. */
export const notificationSettingsSchema = z.object({
  newMedia: z.boolean(),
  comments: z.boolean(),
});
export type NotificationSettings = z.infer<typeof notificationSettingsSchema>;

export const updateNotificationSettingsRequestSchema = z.object({
  newMedia: z.boolean().optional(),
  comments: z.boolean().optional(),
});
export type UpdateNotificationSettingsRequest = z.infer<
  typeof updateNotificationSettingsRequestSchema
>;

export const createFamilyRequestSchema = z.object({
  name: z.string().min(1).max(120),
});
export type CreateFamilyRequest = z.infer<typeof createFamilyRequestSchema>;

/* ------------------------------------------------------------------ */
/* Invitations                                                        */
/* ------------------------------------------------------------------ */

export const invitationCodeSchema = z
  .string()
  .regex(
    /^[A-Za-z0-9]{4}-?[A-Za-z0-9]{4}$/,
    "Format invalide (8 caractères, ex: H4TK9RQ2)",
  );

export const createInvitationRequestSchema = z.object({
  role: roleSchema.default("viewer"),
  maxUses: z.number().int().min(1).max(1000).default(1),
  expiresInDays: z.number().int().min(1).max(365).default(30),
});
export type CreateInvitationRequest = z.infer<typeof createInvitationRequestSchema>;

export const invitationSchema = z.object({
  id: idSchema,
  familyId: idSchema,
  role: roleSchema,
  code: invitationCodeSchema,
  expiresAt: z.string(),
  maxUses: z.number().int(),
  usedCount: z.number().int(),
  revokedAt: z.string().nullable(),
});
export type Invitation = z.infer<typeof invitationSchema>;

export const joinRequestSchema = z.object({
  code: invitationCodeSchema,
  displayName: z.string().min(1).max(100),
});
export type JoinRequest = z.infer<typeof joinRequestSchema>;

export const joinResponseSchema = z.object({
  user: userSchema,
  family: familySchema,
  tokens: authTokenPairSchema,
});
export type JoinResponse = z.infer<typeof joinResponseSchema>;

/* ------------------------------------------------------------------ */
/* Nextcloud connection                                               */
/* ------------------------------------------------------------------ */

export const connectionStatusSchema = z.enum([
  "active",
  "needs_reauth",
  "error",
]);
export type ConnectionStatus = z.infer<typeof connectionStatusSchema>;

export const nextcloudLoginV2StartSchema = z.object({
  baseUrl: z.string().url().regex(/^https:\/\//i, "HTTPS requis"),
});
export type NextcloudLoginV2Start = z.infer<typeof nextcloudLoginV2StartSchema>;

export const nextcloudLoginV2ResponseSchema = z.object({
  login: z.string().url(),
  poll: z.object({
    token: z.string(),
    endpoint: z.string().url(),
  }),
});
export type NextcloudLoginV2Response = z.infer<typeof nextcloudLoginV2ResponseSchema>;

export const nextcloudLoginV2PollSchema = z.object({
  server: z.string().url(),
  loginName: z.string(),
  appPassword: z.string(),
});
export type NextcloudLoginV2Poll = z.infer<typeof nextcloudLoginV2PollSchema>;

/** Relaie du poll : l'endpoint vient de la réponse start (même origine). */
export const nextcloudLoginV2PollRequestSchema = z.object({
  baseUrl: z.string().url().regex(/^https:\/\//i, "HTTPS requis"),
  endpoint: z.string().url(),
  token: z.string().min(1),
});
export type NextcloudLoginV2PollRequest = z.infer<typeof nextcloudLoginV2PollRequestSchema>;

export const connectRequestSchema = z.object({
  baseUrl: z.string().url().regex(/^https?:\/\//i, "URL invalide"),
  loginName: z.string().min(1).max(190),
  appPassword: z.string().min(1),
  folderName: z.preprocess(
    // Champ « optionnel » : une chaîne vide (ou espaces) vaut non renseigné
    // et laissera le serveur choisir le dossier par défaut.
    (v) => (typeof v === "string" && v.trim() === "" ? undefined : v),
    z
      .string()
      .regex(/^[A-Za-z0-9._ -]+$/, "Caractères interdits dans le nom de dossier")
      .max(100)
      .optional(),
  ),
});
export type ConnectRequest = z.infer<typeof connectRequestSchema>;

export const connectionSchema = z.object({
  id: idSchema,
  familyId: idSchema,
  baseUrl: z.string().url(),
  loginName: z.string(),
  status: connectionStatusSchema,
  rootFolder: z.string().nullable(),  rootFolderFileId: z.number().int().nullable(),
  lastSyncAt: z.string().nullable(),
});
export type Connection = z.infer<typeof connectionSchema>;

/** Credentials Nextcloud renvoyés à l'admin pour l'upload direct téléphone → serveur. */
export const nextcloudCredentialsResponseSchema = z.object({
  baseUrl: z.string().url(),
  loginName: z.string(),
  appPassword: z.string().min(1),
  rootFolder: z.string().nullable(),
});
export type NextcloudCredentialsResponse = z.infer<typeof nextcloudCredentialsResponseSchema>;

export const probeRequestSchema = z.object({
  baseUrl: z.string().url().regex(/^https?:\/\//i, "URL invalide"),
  loginName: z.string().min(1).max(190),
  appPassword: z.string().min(1),
});
export type ProbeRequest = z.infer<typeof probeRequestSchema>;

export const capabilitiesSchema = z.object({
  version: z.string(),
  product: z.string(),
  chunking: z.boolean(),
  bigFileChunking: z.boolean(),
  supportsSyncCollection: z.boolean(),
  webhookListeners: z.boolean(),
});
export type Capabilities = z.infer<typeof capabilitiesSchema>;

export const probeResponseSchema = z.object({
  server: z.object({
    baseUrl: z.string().url(),
    loginName: z.string(),
  }),
  capabilities: capabilitiesSchema,
});
export type ProbeResponse = z.infer<typeof probeResponseSchema>;

export const nextcloudFolderSchema = z.object({
  name: z.string().min(1).max(100),
  fileId: z.number().int().positive(),
  path: z.string(),
});
export type NextcloudFolder = z.infer<typeof nextcloudFolderSchema>;

export const connectResponseSchema = z.object({
  connection: connectionSchema,
  folder: nextcloudFolderSchema,
});
export type ConnectResponse = z.infer<typeof connectResponseSchema>;

/* ------------------------------------------------------------------ */
/* Media                                                              */
/* ------------------------------------------------------------------ */

export const capturedAtSourceSchema = z.enum([
  "filename",
  "mtime",
  "exif",
  "client",
]);
export type CapturedAtSource = z.infer<typeof capturedAtSourceSchema>;

export const mediaSchema = z.object({
  id: idSchema,
  familyId: idSchema,
  ncFileId: z.number().int(),
  path: z.string(),
  name: z.string(),
  etag: z.string(),
  size: z.number().int().nonnegative(),
  mime: z.string(),
  capturedAt: z.string(),
  capturedAtSource: capturedAtSourceSchema,
  width: z.number().int().positive().nullable(),
  height: z.number().int().positive().nullable(),
  durationMs: z.number().int().nonnegative().nullable(),
  hasPreview: z.boolean(),
  uploadedBy: idSchema.nullable(),
  caption: z.string().max(2000).nullable(),
  /** Jeton de cache-busting après édition photo (`&v=<updatedAt>`). */
  updatedAt: z.string(),
});
export type Media = z.infer<typeof mediaSchema>;

/** Corps de PATCH /media/:mediaId/caption — null ou chaîne vide = efface. */
export const mediaCaptionUpdateSchema = z.object({
  caption: z.string().max(2000).nullable(),
});
export type MediaCaptionUpdate = z.infer<typeof mediaCaptionUpdateSchema>;

export const mediaListQuerySchema = z.object({
  before: z.string().datetime().optional(),
  limit: z.number().int().min(1).max(200).default(50),
});
export type MediaListQuery = z.infer<typeof mediaListQuerySchema>;

export const mediaListResponseSchema = z.object({
  items: z.array(mediaSchema),
  nextCursor: z.string().datetime().nullable(),
});
export type MediaListResponse = z.infer<typeof mediaListResponseSchema>;

/** Mois disponibles de la timeline (« 2026-08 ») + nombre de médias, tri DESC. */
export const mediaMonthsResponseSchema = z.array(
  z.object({ month: z.string().regex(/^\d{4}-\d{2}$/), count: z.number().int().positive() }),
);
export type MediaMonths = z.infer<typeof mediaMonthsResponseSchema>;

export const mediaNotifyRequestSchema = z.object({
  paths: z.array(z.string().min(1).max(500)).max(50),
});
export type MediaNotifyRequest = z.infer<typeof mediaNotifyRequestSchema>;

/* ------------------------------------------------------------------ */
/* Comments / reactions / views                                       */
/* ------------------------------------------------------------------ */

export const commentSchema = z.object({
  id: idSchema,
  mediaId: idSchema,
  authorId: idSchema,
  authorName: z.string(),
  body: z.string().min(1).max(2000),
  createdAt: z.string(),
});
export type Comment = z.infer<typeof commentSchema>;

export const createCommentRequestSchema = z.object({
  mediaId: idSchema.optional(),
  body: z.string().min(1).max(2000),
});
export type CreateCommentRequest = z.infer<typeof createCommentRequestSchema>;

export const reactionSchema = z.object({
  mediaId: idSchema,
  userId: idSchema,
  emoji: emojiSchema,
});
export type Reaction = z.infer<typeof reactionSchema>;

export const setReactionRequestSchema = z.object({
  mediaId: idSchema.optional(),
  emoji: emojiSchema,
  remove: z.boolean().default(false),
});
export type SetReactionRequest = z.infer<typeof setReactionRequestSchema>;

export const markViewedRequestSchema = z.object({
  mediaId: idSchema.optional(),
});
export type MarkViewedRequest = z.infer<typeof markViewedRequestSchema>;

export const viewerSchema = z.object({
  userId: idSchema,
  displayName: z.string(),
  firstViewedAt: z.string(),
});
export type Viewer = z.infer<typeof viewerSchema>;

export const mediaDetailSchema = mediaSchema.extend({
  comments: z.array(commentSchema),
  reactions: z.record(emojiSchema, z.array(idSchema)),
  viewers: z.array(viewerSchema),
});
export type MediaDetail = z.infer<typeof mediaDetailSchema>;

/* ------------------------------------------------------------------ */
/* Albums                                                             */
/* ------------------------------------------------------------------ */

export const albumSchema = z.object({
  id: idSchema,
  familyId: idSchema,
  name: z.string().min(1).max(120),
  createdAt: z.string(),
  coverMediaId: idSchema.nullable(),
});
export type Album = z.infer<typeof albumSchema>;

export const createAlbumRequestSchema = z.object({
  familyId: idSchema.optional(),
  name: z.string().min(1).max(120),
});
export type CreateAlbumRequest = z.infer<typeof createAlbumRequestSchema>;

export const albumDetailSchema = albumSchema.extend({
  mediaIds: z.array(idSchema),
});
export type AlbumDetail = z.infer<typeof albumDetailSchema>;

export const addMediaToAlbumRequestSchema = z.object({
  mediaIds: z.array(idSchema).min(1).max(500),
});
export type AddMediaToAlbumRequest = z.infer<typeof addMediaToAlbumRequestSchema>;

/* ------------------------------------------------------------------ */
/* Children & growth                                                  */
/* ------------------------------------------------------------------ */

export const childSchema = z.object({
  id: idSchema,
  familyId: idSchema,
  name: z.string().min(1).max(100),
  birthDate: z.string(),
  avatarMediaId: idSchema.nullable(),
});
export type Child = z.infer<typeof childSchema>;

export const createChildRequestSchema = z.object({
  familyId: idSchema.optional(),
  name: z.string().min(1).max(100),
  birthDate: z.string().date(),
});
export type CreateChildRequest = z.infer<typeof createChildRequestSchema>;

export const updateChildRequestSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  birthDate: z.string().date().optional(),
  avatarMediaId: idSchema.nullable().optional(),
});
export type UpdateChildRequest = z.infer<typeof updateChildRequestSchema>;

export const growthEntrySchema = z.object({
  id: idSchema,
  childId: idSchema,
  measuredOn: z.string(),
  heightCm: z.number().positive().nullable(),
  weightKg: z.number().positive().nullable(),
  note: z.string().max(500).nullable(),
});
export type GrowthEntry = z.infer<typeof growthEntrySchema>;

export const createGrowthEntryRequestSchema = z.object({
  childId: idSchema.optional(),
  measuredOn: z.string().date(),
  heightCm: z.number().positive().max(250).optional(),
  weightKg: z.number().positive().max(200).optional(),
  note: z.string().max(500).optional(),
});
export type CreateGrowthEntryRequest = z.infer<typeof createGrowthEntryRequestSchema>;

export const milestoneSchema = z.object({
  id: idSchema,
  childId: idSchema,
  happenedOn: z.string(),
  title: z.string().min(1).max(200),
  note: z.string().max(1000).nullable(),
});
export type Milestone = z.infer<typeof milestoneSchema>;

export const createMilestoneRequestSchema = z.object({
  childId: idSchema.optional(),
  happenedOn: z.string().date(),
  title: z.string().min(1).max(200),
  note: z.string().max(1000).optional(),
});
export type CreateMilestoneRequest = z.infer<typeof createMilestoneRequestSchema>;

/* ------------------------------------------------------------------ */
/* Devices / push                                                     */
/* ------------------------------------------------------------------ */

export const registerDeviceRequestSchema = z.object({
  expoPushToken: z.string().min(1).max(300),
  platform: z.enum(["ios", "android"]).optional(),
  /** BCP 47 du device (ex. "fr", "pt-BR") — sert au push localisé. */
  locale: z.string().regex(/^[a-z]{2}([_-][A-Za-z0-9]{2,8})?$/).max(16).optional(),
});
export type RegisterDeviceRequest = z.infer<typeof registerDeviceRequestSchema>;

/* ------------------------------------------------------------------ */
/* Errors                                                             */
/* ------------------------------------------------------------------ */

export * from "./uploader.js";
export * from "./age.js";

export const apiErrorSchema = z.object({
  error: z.object({
    code: z.string(),
    message: z.string(),
  }),
});
export type ApiError = z.infer<typeof apiErrorSchema>;
