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 { errorSchema } from '../../../shared/swagger.js';
import { parseJson } from '../../../shared/http-input.js';
import {
  CreateCategory,
  DeleteCategory,
  GetCategoryBySlug,
  ListCategoryTree,
  UpdateCategory,
} from '../application/category-use-cases.js';
import type { Category, CategoryTreeNode } from '../domain/category.js';
import {
  CategoryParentNotContainerError,
  CategoryParentNotFoundError,
  CategorySlugAlreadyExistsError,
  CategoryTreeCycleError,
} from '../domain/errors.js';
import { PgCategoryRepository } from '../infrastructure/pg-category-repository.js';
import { generateWithModel, loadAiSettings, requireAiConfigured } from '../../../shared/ai-content.js';

export interface CategoriesRoutesDeps {
  pool: pg.Pool;
  authenticate: Authenticate;
}

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 newCategorySchema = z.object({
  parentId: z.uuid().optional().nullable(),
  name: z.string().min(1).max(200),
  slug: slugSchema,
  seoTitle: z.string().min(1).max(200).optional().nullable(),
  seoDescription: z.string().min(1).max(500).optional().nullable(),
  description: z.string().min(1).max(4000).optional().nullable(),
  isParent: z.boolean().optional(),
  emoji: z.string().max(10).optional().nullable(),
  color: z.string().max(200).optional().nullable(),
  bgColor: z.string().max(20).optional().nullable(),
  textColor: z.string().max(20).optional().nullable(),
});

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

export async function registerCategoriesRoutes(
  app: FastifyInstance,
  deps: CategoriesRoutesDeps,
): Promise<void> {
  const repository = new PgCategoryRepository(deps.pool);
  const getBySlug = new GetCategoryBySlug(repository);
  const listTree = new ListCategoryTree(repository);
  const createCategory = new CreateCategory(repository);
  const updateCategory = new UpdateCategory(repository);
  const deleteCategory = new DeleteCategory(repository);

  const treeSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Category tree (público)',
    description: 'Devuelve el árbol completo de categorías.',
  };
  app.get('/categories/tree', { schema: treeSchema }, async (_request, reply) => {
    const items = await listTree.execute();
    return reply.send({ items: items.map(serializeTreeNode) });
  });

  const publicCatSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Get category by slug (público)',
    params: { type: 'object', required: ['slug'], properties: { slug: { type: 'string' } } },
    response: { 404: errorSchema },
  };
  app.get('/categoria/:slug', { schema: publicCatSchema }, async (request, reply) => {
    const { slug } = parseJson(slugParamSchema, request.params);
    const category = await getBySlug.execute(slug);
    if (!category) {
      throw new AppError(404, 'NOT_FOUND', 'Category not found');
    }
    return reply.send(serializeCategory(category));
  });

  const createCatSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Create category (admin)',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/categories', { schema: createCatSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(newCategorySchema, request.body);
    try {
      const category = await createCategory.execute(input);
      return reply.code(201).send(serializeCategory(category));
    } catch (error) {
      throw mapCategoryError(error);
    }
  });

  const updateCatSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Update category (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch('/categories/:id', { schema: updateCatSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const patch = parseJson(categoryPatchSchema, request.body);
    try {
      const category = await updateCategory.execute(id, patch);
      if (!category) {
        throw new AppError(404, 'NOT_FOUND', 'Category not found');
      }
      return reply.send(serializeCategory(category));
    } catch (error) {
      throw mapCategoryError(error);
    }
  });

  const deleteCatSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Delete category (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.delete('/categories/:id', { schema: deleteCatSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const result = await deleteCategory.execute(id);
    if (result === 'not_found') {
      throw new AppError(404, 'NOT_FOUND', 'Category not found');
    }
    // Children and product-category links cascade at the DB level (F-120).
    return reply.code(204).send();
  });

  const generateCatSeoSchema: FastifySchema = {
    tags: ['Categories'],
    summary: 'Generate category content with AI (admin)',
    description:
      'Completa con IA la descripción, el título SEO y la descripción SEO de la categoría cuando están vacíos.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema, 422: errorSchema },
  };
  app.post('/categories/:id/generate-seo', { schema: generateCatSeoSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(idParamSchema, request.params);
    const existing = await repository.findById(id);
    if (!existing) throw new AppError(404, 'NOT_FOUND', 'Category not found');

    const settings = await loadAiSettings(deps.pool, [
      'ai_category_description_prompt',
      'ai_category_seo_title_prompt',
      'ai_category_seo_description_prompt',
    ]);
    const { baseUrl, model, apiKey } = requireAiConfigured(settings);

    const replacements: Record<string, string> = { name: existing.name, description: existing.description ?? '' };
    const promptFor = (template: string | undefined, fallback: string) =>
      (template || fallback).replace(/\{\{(name|description)\}\}/g, (_, key: string) => replacements[key] ?? '');

    const patch: { description?: string; seoTitle?: string; seoDescription?: string } = {};
    if (!existing.description?.trim()) {
      const raw = await generateWithModel(
        baseUrl,
        model,
        apiKey,
        promptFor(
          settings.ai_category_description_prompt,
          'Escribe una descripción breve y atractiva en español para esta categoría de una tienda de productos naturales y ecológicos: {{name}}. Devuelve solo la descripción, sin formato.',
        ),
      );
      patch.description = raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 1000);
    }
    if (!existing.seoTitle?.trim()) {
      const raw = await generateWithModel(
        baseUrl,
        model,
        apiKey,
        promptFor(
          settings.ai_category_seo_title_prompt,
          'Genera un título SEO breve para esta categoría: {{name}}. Devuelve solo el título.',
        ),
      );
      patch.seoTitle = raw.replace(/<[^>]*>/g, '').trim().slice(0, 200);
    }
    if (!existing.seoDescription?.trim()) {
      const raw = await generateWithModel(
        baseUrl,
        model,
        apiKey,
        promptFor(
          settings.ai_category_seo_description_prompt,
          'Genera una meta descripción SEO en español para esta categoría: {{name}}. Devuelve solo la descripción.',
        ),
      );
      patch.seoDescription = raw.replace(/<[^>]*>/g, '').trim().slice(0, 500);
    }

    if (Object.keys(patch).length === 0) {
      return reply.send(serializeCategory(existing));
    }
    const updated = await repository.update(id, patch);
    return reply.send(serializeCategory(updated ?? existing));
  });
}

function mapCategoryError(error: unknown): Error {
  if (error instanceof CategorySlugAlreadyExistsError) {
    return new AppError(409, 'CATEGORY_SLUG_EXISTS', error.message);
  }
  if (error instanceof CategoryParentNotFoundError) {
    return new AppError(422, 'CATEGORY_PARENT_NOT_FOUND', error.message);
  }
  if (error instanceof CategoryTreeCycleError) {
    return new AppError(422, 'CATEGORY_TREE_CYCLE', error.message);
  }
  if (error instanceof CategoryParentNotContainerError) {
    return new AppError(422, 'CATEGORY_PARENT_NOT_CONTAINER', error.message);
  }
  return error instanceof Error ? error : new Error('Unknown category error');
}

function serializeCategory(category: Category) {
  return {
    id: category.id,
    parentId: category.parentId,
    name: category.name,
    slug: category.slug,
    url: `/categoria/${category.slug}`,
    seoTitle: category.seoTitle,
    seoDescription: category.seoDescription,
    description: category.description,
    isParent: category.isParent,
    emoji: category.emoji,
    color: category.color,
    bgColor: category.bgColor,
    textColor: category.textColor,
    createdAt: category.createdAt.toISOString(),
    updatedAt: category.updatedAt.toISOString(),
  };
}

function serializeTreeNode(category: CategoryTreeNode): ReturnType<typeof serializeCategory> & {
  children: ReturnType<typeof serializeTreeNode>[];
} {
  return {
    ...serializeCategory(category),
    children: category.children.map(serializeTreeNode),
  };
}
