import { performance } from 'node:perf_hooks';
import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import { requireRole, type Authenticate } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { parseJson } from '../../../shared/http-input.js';
import { errorSchema } from '../../../shared/swagger.js';
import { aiTextToHtml, generateWithModel } from '../../../shared/ai-content.js';
import {
  AttachProductImage,
  DetachProductImage,
  ListProductImages,
  ReorderProductImages,
} from '../application/image-use-cases.js';
import {
  CreateProduct,
  GetActiveProductBySlug,
  SearchProducts,
  SuggestCorrections,
  UpdateProduct,
} from '../application/product-use-cases.js';
import {
  CreateProductVariant,
  ListProductVariants,
  UpdateProductVariant,
  UpsertProductRichData,
} from '../application/variant-use-cases.js';
import {
  ProductBrandNotFoundError,
  ProductCategoryNotFoundError,
  ProductImageMainAlreadyExistsError,
  ProductImageVariantMismatchError,
  ProductSlugAlreadyExistsError,
  ProductVariantCodeAlreadyExistsError,
} from '../domain/errors.js';
import type { ProductImage } from '../domain/image.js';
import { PRODUCT_IMAGE_ROLES } from '../domain/image.js';
import type { Product } from '../domain/product.js';
import { PRODUCT_ATTRIBUTES, PRODUCT_STATES } from '../domain/product.js';
import type { ProductRichData, ProductVariant } from '../domain/variant.js';
import { NUTRITION_SOURCES } from '../domain/variant.js';
import type { PricingServicePort } from '../../pricing/index.js';
import { LocalProductImageStorage } from '../infrastructure/local-product-image-storage.js';
import { PgProductImageRepository } from '../infrastructure/pg-product-image-repository.js';
import { PgProductRepository } from '../infrastructure/pg-product-repository.js';
import { PgProductSearchRepository } from '../infrastructure/pg-product-search-repository.js';
import { PgProductRichDataRepository } from '../infrastructure/pg-rich-data-repository.js';
import { PgProductVariantRepository } from '../infrastructure/pg-variant-repository.js';

interface CatalogSearchLogger {
  info(payload: Record<string, unknown>, message: string): void;
}

export interface CatalogRoutesDeps {
  pool: pg.Pool;
  authenticate: Authenticate;
  logger?: CatalogSearchLogger;
  pricing: PricingServicePort;
}

const slugSchema = z
  .string()
  .min(1)
  .max(160)
  .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
const slugParamSchema = z.object({ slug: slugSchema });
const idParamSchema = z.object({ id: z.uuid() });
const variantParamSchema = z.object({ id: z.uuid(), variantId: z.uuid() });
const imageParamSchema = z.object({ id: z.uuid(), imageId: z.uuid() });
const searchQuerySchema = z.object({
  q: z.string().min(1).max(200).optional(),
  limit: z.coerce.number().int().min(1).max(100).optional(),
  offset: z.coerce.number().int().min(0).max(10_000).optional(),
  brandSlug: slugSchema.optional(),
  categorySlug: slugSchema.optional(),
});

const jsonRecordSchema = z
  .record(z.string(), z.unknown())
  .refine((value) => JSON.stringify(value).length <= 10_000, {
    message: 'JSON payload is too large',
  });

const newProductSchema = z.object({
  name: z.string().min(1).max(200),
  slug: slugSchema,
  description: z.string().min(1).max(2_000).optional().nullable(),
  state: z.enum(PRODUCT_STATES).optional(),
  channels: z.enum(['online', 'offline', 'all']).optional(),
  featured: z.boolean().optional(),
  attributes: z.array(z.enum(PRODUCT_ATTRIBUTES)).optional(),
  seoTitle: z.string().min(1).max(200).optional().nullable(),
  seoDescription: z.string().min(1).max(500).optional().nullable(),
  categoryIds: z.array(z.uuid()).max(50).optional(),
  brandId: z.uuid().optional().nullable(),
  expirationDate: z.iso.date().optional().nullable(),
  unitWeightKg: z.number().positive().max(1000).optional(),
  minPurchaseQty: z.number().int().min(1).max(999).optional(),
  aiAssisted: z.boolean().optional(),
});

const productPatchSchema = newProductSchema
  .partial()
  .refine((value) => Object.values(value).some((field) => field !== undefined), {
    message: 'At least one product field is required',
  });

const newVariantSchema = z.object({
  sku: z.string().min(1).max(100),
  ean: z.string().min(1).max(32).optional().nullable(),
  attributes: jsonRecordSchema.optional(),
});

const variantPatchSchema = newVariantSchema
  .partial()
  .refine((value) => Object.values(value).some((field) => field !== undefined), {
    message: 'At least one variant field is required',
  });

const imageUrlSchema = z
  .string()
  .trim()
  .min(1)
  .max(2_000)
  .refine((value) => value.startsWith('/') || URL.canParse(value), {
    message: 'Image URL must be an absolute local path or a valid URL',
  });

const newImageSchema = z.object({
  url: imageUrlSchema,
  altText: z.string().trim().max(300).optional().default(''),
  role: z.enum(PRODUCT_IMAGE_ROLES),
  variantId: z.uuid().optional().nullable(),
  position: z.number().int().min(0).optional(),
});

const reorderImagesSchema = z.object({
  items: z
    .array(z.object({ imageId: z.uuid(), position: z.number().int().min(0) }))
    .min(1)
    .max(100),
});

const richDataSchema = z
  .object({
    ingredients: z.string().min(1).max(5_000).optional().nullable(),
    allergens: z.array(z.string().min(1).max(100)).max(100).optional(),
    nutrition: jsonRecordSchema.optional().nullable(),
    nutritionSource: z.enum(NUTRITION_SOURCES).optional(),
    isOrganic: z.boolean().optional(),
    organicCertification: z.string().min(1).max(300).optional().nullable(),
  })
  .refine(
    (value) =>
      value.nutrition === undefined ||
      value.nutrition === null ||
      value.nutritionSource !== undefined,
    {
      message: 'nutritionSource is required when nutrition is provided',
    },
  )
  .refine((value) => Object.values(value).some((field) => field !== undefined), {
    message: 'At least one rich data field is required',
  });

export async function registerCatalogRoutes(
  app: FastifyInstance,
  deps: CatalogRoutesDeps,
): Promise<void> {
  const repository = new PgProductRepository(deps.pool);
  const searchRepository = new PgProductSearchRepository(deps.pool);
  const variants = new PgProductVariantRepository(deps.pool);
  const richData = new PgProductRichDataRepository(deps.pool);
  const images = new PgProductImageRepository(deps.pool, new LocalProductImageStorage());
  const getBySlug = new GetActiveProductBySlug(repository);
  const searchProducts = new SearchProducts(searchRepository);
  const suggestCorrections = new SuggestCorrections(searchRepository);
  const createProduct = new CreateProduct(repository);
  const updateProduct = new UpdateProduct(repository);
  const listVariants = new ListProductVariants(variants);
  const createVariant = new CreateProductVariant(repository, variants, deps.pricing);
  const updateVariant = new UpdateProductVariant(variants);
  const upsertRichData = new UpsertProductRichData(repository, richData);
  const listImages = new ListProductImages(images);
  const attachImage = new AttachProductImage(repository, images);
  const detachImage = new DetachProductImage(images);
  const reorderImages = new ReorderProductImages(images);

  // Admin: list all products (any state)
  const catalogListSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'List all products (admin)',
    description: 'Lista todos los productos incluyendo los no activos. Requiere rol admin.',
    querystring: {
      type: 'object',
      properties: {
        q: { type: 'string', description: 'Búsqueda por nombre' },
        limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
        offset: { type: 'integer', minimum: 0, default: 0 },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/catalog/products', { schema: catalogListSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const q = (request.query as { q?: string }).q;
    const limit = parseInt((request.query as { limit?: string }).limit ?? '20', 10);
    const offset = parseInt((request.query as { offset?: string }).offset ?? '0', 10);
    const result = await repository.listAll({ limit, offset, q });
    const productIds = result.items.map((p) => p.id);
    const allImages = await images.listByProductIds(productIds);
    const imagesByProductId = groupImagesByProductId(allImages);
    return reply.send({
      items: result.items.map((p) => serializeProduct(p, imagesByProductId.get(p.id) ?? [])),
      total: result.total,
    });
  });

  const publicProductSchema: FastifySchema = {
    tags: ['Catalog'],
    summary: 'Get product by slug (público)',
    description: 'Devuelve un producto activo con todas sus imágenes.',
    params: {
      type: 'object',
      required: ['slug'],
      properties: { slug: { type: 'string', description: 'URL slug del producto' } },
    },
    response: { 404: errorSchema },
  };
  app.get('/productos/:slug', { schema: publicProductSchema }, async (request, reply) => {
    const { slug } = parseJson(slugParamSchema, request.params);
    const product = await getBySlug.execute(slug);
    if (!product) {
      throw new AppError(404, 'NOT_FOUND', 'Product not found');
    }
    const productImages = await listImages.execute(product.id);
    return reply.send(serializeProduct(product, productImages));
  });

  const adminGetProductSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Get product by ID (admin)',
    description: 'Obtiene un producto por su UUID (incluye no activos). Requiere admin.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.get('/products/:id', { schema: adminGetProductSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const product = await repository.findById(id);
    if (!product) {
      throw new AppError(404, 'NOT_FOUND', 'Product not found');
    }
    const productImages = await listImages.execute(product.id);
    return reply.send(serializeProduct(product, productImages));
  });

  app.post('/products/:id/generate-seo', async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const product = await repository.findById(id);
    if (!product) throw new AppError(404, 'NOT_FOUND', 'Product not found');

    const settingsResult = await deps.pool.query<{ key: string; value: string }>(
      `SELECT key, value FROM store_settings WHERE key = ANY($1::text[])`,
      [['ai_base_url', 'ai_model', 'ai_api_key', 'ai_seo_title_prompt', 'ai_seo_description_prompt', 'ai_product_description_prompt']],
    );
    const settings = Object.fromEntries(settingsResult.rows.map((row) => [row.key, row.value]));
    const baseUrl = settings.ai_base_url?.trim();
    const model = settings.ai_model?.trim();
    const apiKey = settings.ai_api_key?.trim();
    if (!baseUrl || !model || !apiKey) {
      throw new AppError(422, 'AI_NOT_CONFIGURED', 'Configura proveedor, URL base, modelo y API key en Ajustes → IA para SEO');
    }

    const replacements: Record<string, string> = {
      name: product.name,
      description: product.description ?? '',
      brand: product.brand?.name ?? '',
    };
    const promptFor = (template: string | undefined, fallback: string) =>
      (template || fallback).replace(/\{\{(name|description|brand)\}\}/g, (_, key: string) => replacements[key] ?? '');
    const patch: { description?: string; seoTitle?: string; seoDescription?: string; aiAssisted?: boolean } = {};
    if (!product.description?.trim()) {
      const raw = await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_product_description_prompt, 'Escribe una descripción comercial clara y útil en español para este producto: {{name}}. Devuelve SOLO HTML válido y seguro (párrafos <p>, y si procede listas <ul><li> y <strong> para resaltar). No uses títulos ni markdown.'));
      patch.description = aiTextToHtml(raw).slice(0, 4_000);
    }
    if (!product.seoTitle?.trim()) {
      patch.seoTitle = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_title_prompt, 'Genera un título SEO breve para {{name}}. Devuelve solo el título.'))).replace(/<[^>]*>/g, '').trim().slice(0, 200);
    }
    if (!product.seoDescription?.trim()) {
      patch.seoDescription = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_description_prompt, 'Genera una meta descripción SEO en español para {{name}}. Devuelve solo la descripción.'))).replace(/<[^>]*>/g, '').trim().slice(0, 500);
    }
    if (Object.keys(patch).length > 0) {
      // F-112: cualquier generación automática marca la ficha como asistida por IA.
      patch.aiAssisted = true;
    }
    const updated = await repository.update(id, patch);
    return reply.send(serializeProduct(updated ?? product));
  });

  const searchSchema: FastifySchema = {
    tags: ['Catalog'],
    summary: 'Search products (público)',
    description: 'Búsqueda full-text con filtros opcionales por marca y categoría.',
    querystring: {
      type: 'object',
      properties: {
        q: { type: 'string', maxLength: 200, description: 'Consulta de búsqueda' },
        brandSlug: { type: 'string', description: 'Slug de marca' },
        categorySlug: { type: 'string', description: 'Slug de categoría' },
        limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
        offset: { type: 'integer', minimum: 0, default: 0 },
      },
    },
    response: { 200: { type: 'object', properties: { items: { type: 'array' } } } },
  };
  app.get('/products/search', { schema: searchSchema }, async (request, reply) => {
    const input = parseJson(searchQuerySchema, request.query);
    const startedAt = performance.now();
    const items = await searchProducts.execute(input);
    const productIds = items.map((p) => p.id);
    const allImages = await images.listByProductIds(productIds);
    const imagesByProductId = groupImagesByProductId(allImages);
    deps.logger?.info(
      {
        event: 'catalog_search',
        queryPresent: input.q !== undefined,
        query: sanitizeSearchTelemetryQuery(input.q),
        brandSlug: input.brandSlug,
        categorySlug: input.categorySlug,
        limit: input.limit ?? 20,
        offset: input.offset ?? 0,
        durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
        resultCount: items.length,
      },
      'catalog search completed',
    );
    return reply.send({
      items: items.map((product) =>
        serializeProduct(product, imagesByProductId.get(product.id) ?? []),
      ),
    });
  });

  const suggestSchema: FastifySchema = {
    tags: ['Catalog'],
    summary: 'Search suggestions',
    description: 'Devuelve correcciones sugeridas para la consulta de búsqueda.',
    querystring: {
      type: 'object',
      required: ['q'],
      properties: { q: { type: 'string', minLength: 2, maxLength: 200 } },
    },
    response: {
      200: {
        type: 'object',
        properties: { suggestions: { type: 'array', items: { type: 'string' } } },
      },
    },
  };
  app.get('/products/suggest', { schema: suggestSchema }, async (request, reply) => {
    const { q } = parseJson(z.object({ q: z.string().min(2).max(200) }), request.query);
    const suggestions = await suggestCorrections.execute(q);
    return reply.send({ suggestions });
  });

  const createProductSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Create product',
    description: 'Crea un nuevo producto. Requiere rol admin.',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/products', { schema: createProductSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(newProductSchema, request.body);
    try {
      const product = await createProduct.execute(input);
      // Modelo sin variantes visibles: cada producto lleva una única variante
      // interna autogenerada (SKU interno, nunca editable).
      try {
        await createVariant.execute(product.id, { sku: `SKU-MV-${product.id}` });
      } catch (variantError) {
        request.log.warn({ err: variantError, productId: product.id }, 'default_variant_create_failed');
      }
      return reply.code(201).send(serializeProduct(product));
    } catch (error) {
      throw mapProductError(error);
    }
  });

  const variantsSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'List product variants',
    description: 'Lista las variantes de un producto.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
  };
  app.get('/products/:id/variants', { schema: variantsSchema }, async (request, reply) => {
    const { id } = parseJson(idParamSchema, request.params);
    let items = await listVariants.execute(id);
    if (items.length === 0) {
      // Lazy migration: productos legacy sin variante reciben la variante
      // default solo si quien consulta tiene sesión admin.
      try {
        const user = await deps.authenticate(request);
        if (user.role === 'admin') {
          const created = await createVariant.execute(id, { sku: `SKU-MV-${id}` });
          if (created) items = [created];
        }
      } catch {
        // Sin sesión admin: se devuelve la lista vacía sin crear nada.
      }
    }
    return reply.send({ items: items.map(serializeVariant) });
  });

  const imagesSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'List product images',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
  };
  app.get('/products/:id/images', { schema: imagesSchema }, async (request, reply) => {
    const { id } = parseJson(idParamSchema, request.params);
    const items = await listImages.execute(id);
    return reply.send({ items: items.map(serializeImage) });
  });

  const attachImageSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Attach image to product',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.post('/products/:id/images', { schema: attachImageSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const input = parseJson(newImageSchema, request.body);
    try {
      const image = await attachImage.execute(id, input);
      if (!image) {
        throw new AppError(404, 'NOT_FOUND', 'Product not found');
      }
      return reply.code(201).send(serializeImage(image));
    } catch (error) {
      throw mapProductError(error);
    }
  });

  const deleteImageSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Delete product image',
    params: {
      type: 'object',
      required: ['id', 'imageId'],
      properties: {
        id: { type: 'string', format: 'uuid' },
        imageId: { type: 'string', format: 'uuid' },
      },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.delete(
    '/products/:id/images/:imageId',
    { schema: deleteImageSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { id, imageId } = parseJson(imageParamSchema, request.params);
      const deleted = await detachImage.execute(id, imageId);
      if (!deleted) {
        throw new AppError(404, 'NOT_FOUND', 'Image not found');
      }
      return reply.code(204).send();
    },
  );

  const reorderImagesSwaggerSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Reorder product images',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.patch(
    '/products/:id/images/reorder',
    { schema: reorderImagesSwaggerSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { id } = parseJson(idParamSchema, request.params);
      const { items } = parseJson(reorderImagesSchema, request.body);
      const ordered = await reorderImages.execute(id, items);
      return reply.send({ items: ordered.map(serializeImage) });
    },
  );

  const createVariantSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Create product variant',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/products/:id/variants', { schema: createVariantSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const input = parseJson(newVariantSchema, request.body);
    try {
      const variant = await createVariant.execute(id, input);
      if (!variant) {
        throw new AppError(404, 'NOT_FOUND', 'Product not found');
      }
      return reply.code(201).send(serializeVariant(variant));
    } catch (error) {
      throw mapProductError(error);
    }
  });

  const updateVariantSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Update product variant',
    params: {
      type: 'object',
      required: ['id', 'variantId'],
      properties: {
        id: { type: 'string', format: 'uuid' },
        variantId: { type: 'string', format: 'uuid' },
      },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch(
    '/products/:id/variants/:variantId',
    { schema: updateVariantSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { id, variantId } = parseJson(variantParamSchema, request.params);
      const patch = parseJson(variantPatchSchema, request.body);
      try {
        const variant = await updateVariant.execute(id, variantId, patch);
        if (!variant) {
          throw new AppError(404, 'NOT_FOUND', 'Variant not found');
        }
        return reply.send(serializeVariant(variant));
      } catch (error) {
        throw mapProductError(error);
      }
    },
  );

  const richDataSchema2: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Set product rich data',
    description: 'Establece datos ricos: ingredientes, alérgenos, nutrición, orgánico.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch('/products/:id/rich-data', { schema: richDataSchema2 }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const patch = parseJson(richDataSchema, request.body);
    const data = await upsertRichData.execute(id, patch);
    if (!data) {
      throw new AppError(404, 'NOT_FOUND', 'Product not found');
    }
    return reply.send(serializeRichData(data));
  });

  const updateProductSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Update product',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch('/products/:id', { schema: updateProductSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const patch = parseJson(productPatchSchema, request.body);
    try {
      const product = await updateProduct.execute(id, patch);
      if (!product) {
        throw new AppError(404, 'NOT_FOUND', 'Product not found');
      }
      return reply.send(serializeProduct(product));
    } catch (error) {
      throw mapProductError(error);
    }
  });

  const setStateSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Change product state',
    description: 'Cambia el estado del producto (draft, active, archived).',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['state'],
      properties: {
        state: { type: 'string', enum: ['draft', 'active', 'archived', 'pending_review'] },
      },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch('/products/:id/state', { schema: setStateSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const { state } = parseJson(z.object({ state: z.enum(PRODUCT_STATES) }), request.body);
    const product = await updateProduct.execute(id, { state });
    if (!product) {
      throw new AppError(404, 'NOT_FOUND', 'Product not found');
    }
    return reply.send(serializeProduct(product));
  });

  const deleteProductSchema: FastifySchema = {
    tags: ['Admin Products'],
    summary: 'Delete product',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema },
  };
  app.delete('/products/:id', { schema: deleteProductSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    await repository.delete(id);
    return reply.code(204).send();
  });
}

function mapProductError(error: unknown): Error {
  if (error instanceof ProductSlugAlreadyExistsError) {
    return new AppError(409, 'PRODUCT_SLUG_EXISTS', error.message);
  }
  if (error instanceof ProductCategoryNotFoundError) {
    return new AppError(422, 'PRODUCT_CATEGORY_NOT_FOUND', error.message);
  }
  if (error instanceof ProductBrandNotFoundError) {
    return new AppError(422, 'PRODUCT_BRAND_NOT_FOUND', error.message);
  }
  if (error instanceof ProductVariantCodeAlreadyExistsError) {
    return new AppError(409, 'PRODUCT_VARIANT_CODE_EXISTS', error.message);
  }
  if (error instanceof ProductImageVariantMismatchError) {
    return new AppError(422, 'PRODUCT_IMAGE_VARIANT_MISMATCH', error.message);
  }
  if (error instanceof ProductImageMainAlreadyExistsError) {
    return new AppError(409, 'PRODUCT_IMAGE_MAIN_EXISTS', error.message);
  }
  return error instanceof Error ? error : new Error('Unknown product error');
}

function sanitizeSearchTelemetryQuery(query: string | undefined): string | undefined {
  if (query === undefined) {
    return undefined;
  }
  return query
    .trim()
    .slice(0, 200)
    .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '[redacted-email]')
    .replace(/\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9_]+\b/g, '[redacted-key]')
    .replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[redacted-token]');
}

function serializeProduct(product: Product, images: ProductImage[] = []) {
  return {
    id: product.id,
    name: product.name,
    slug: product.slug,
    url: `/productos/${product.slug}`,
    images: images.map(serializeImage),
    description: product.description,
    state: product.state,
    channels: product.channels,
    featured: product.featured,
    attributes: product.attributes,
    seoTitle: product.seoTitle,
    seoDescription: product.seoDescription,
    categoryIds: product.categoryIds,
    brandId: product.brandId,
    brand: product.brand,
    expirationDate: product.expirationDate,
    unitWeightKg: product.unitWeightKg,
    minPurchaseQty: product.minPurchaseQty,
    aiAssisted: product.aiAssisted,
    createdAt: product.createdAt.toISOString(),
    updatedAt: product.updatedAt.toISOString(),
  };
}

function serializeImage(image: ProductImage) {
  return {
    id: image.id,
    productId: image.productId,
    variantId: image.variantId,
    url: image.url,
    altText: image.altText,
    position: image.position,
    role: image.role,
    createdAt: image.createdAt.toISOString(),
    updatedAt: image.updatedAt.toISOString(),
  };
}

function groupImagesByProductId(allImages: ProductImage[]): Map<string, ProductImage[]> {
  const map = new Map<string, ProductImage[]>();
  for (const image of allImages) {
    const list = map.get(image.productId);
    if (list) {
      list.push(image);
    } else {
      map.set(image.productId, [image]);
    }
  }
  return map;
}

function serializeVariant(variant: ProductVariant) {
  return {
    id: variant.id,
    productId: variant.productId,
    sku: variant.sku,
    ean: variant.ean,
    attributes: variant.attributes,
    createdAt: variant.createdAt.toISOString(),
    updatedAt: variant.updatedAt.toISOString(),
  };
}

function serializeRichData(data: ProductRichData) {
  return {
    productId: data.productId,
    ingredients: data.ingredients,
    allergens: data.allergens,
    nutrition: data.nutrition,
    nutritionSource: data.nutritionSource,
    isOrganic: data.isOrganic,
    organicCertification: data.organicCertification,
    createdAt: data.createdAt.toISOString(),
    updatedAt: data.updatedAt.toISOString(),
  };
}
