import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import type { Authenticate } from '../../../shared/auth.js';
import { requireRole } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { parseJson } from '../../../shared/http-input.js';
import { errorSchema } from '../../../shared/swagger.js';
import { OrderService } from '../application/order-service.js';
import { OrderNotFoundError, OrderStateTransitionError } from '../domain/errors.js';
import type { OrderState } from '../domain/order.js';
import { PgOrderRepository } from '../infrastructure/pg-order-repository.js';
import { NoOpOrderEventPublisher } from '../infrastructure/no-op-event-publisher.js';
import { sendOrderStatusEmail, ORDER_STATE_LABELS } from '../infrastructure/order-status-mailer.js';

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

const orderItemSchema = z.object({
  productId: z.uuid(),
  variantId: z.uuid(),
  sku: z.string().min(1).max(120),
  ean: z.string().min(1).max(40).nullable().optional(),
  name: z.string().min(1).max(200),
  unitPriceCents: z.number().int().min(0),
  discountCents: z.number().int().min(0),
  taxCents: z.number().int().min(0),
  quantity: z.number().int().positive(),
});

const createOrderSchema = z
  .object({
    idempotencyKey: z.string().min(1).max(120).optional().nullable(),
    items: z.array(orderItemSchema).min(1),
    totals: z.object({
      subtotalCents: z.number().int().min(0),
      discountCents: z.number().int().min(0),
      taxCents: z.number().int().min(0),
      totalCents: z.number().int().min(0),
    }),
  })
  .strip();

const transitionSchema = z.object({
  state: z.enum([
    'PENDING',
    'AWAITING_PAYMENT',
    'PAID',
    'PROCESSING',
    'SHIPPED',
    'DELIVERED',
    'CANCELLED',
    'REFUNDED',
    'PARTIALLY_REFUNDED',
  ]),
});

const orderIdParamSchema = z.object({ id: z.uuid() });

export async function registerOrdersRoutes(
  app: FastifyInstance,
  deps: OrdersRoutesDeps,
): Promise<void> {
  const service = new OrderService(new PgOrderRepository(deps.pool), new NoOpOrderEventPublisher());

  const createOrderSwaggerSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Create order',
    description: 'Crea un pedido. Requiere autenticación.',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema },
  };
  app.post('/orders', { schema: createOrderSwaggerSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const input = parseJson(createOrderSchema, request.body);
    try {
      const order = await service.create({
        userId: user.id,
        idempotencyKey: input.idempotencyKey ?? null,
        items: input.items.map((item) => ({ ...item, ean: item.ean ?? null })),
        totals: input.totals,
      });
      return reply.code(201).send(serializeOrder(order));
    } catch (error) {
      throw mapOrderError(error);
    }
  });

  const transitionOrderSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Transition order state (customer)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['state'],
      properties: {
        state: {
          type: 'string',
          enum: [
            'PENDING',
            'AWAITING_PAYMENT',
            'PAID',
            'PROCESSING',
            'SHIPPED',
            'DELIVERED',
            'CANCELLED',
            'REFUNDED',
            'PARTIALLY_REFUNDED',
          ],
        },
      },
    },
    response: { 401: errorSchema, 404: errorSchema },
  };
  app.post('/orders/:id/transitions', { schema: transitionOrderSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(orderIdParamSchema, request.params);
    const { state } = parseJson(transitionSchema, request.body);
    try {
      const order = await service.transition(id, state as OrderState, user.id);
      return reply.send(serializeOrder(order));
    } catch (error) {
      throw mapOrderError(error);
    }
  });

  const getOrderSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Get order',
    description: 'Obtiene un pedido. El cliente solo ve los suyos; admin ve cualquiera.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 404: errorSchema },
  };
  app.get('/orders/:id', { schema: getOrderSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(orderIdParamSchema, request.params);
    const order = await service.getOrder(id, user.id);
    if (!order) throw new AppError(404, 'ORDER_NOT_FOUND', 'Order not found');
    return reply.send(serializeOrder(order));
  });

  // Admin-only routes
  const listOrdersSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'List orders (admin)',
    querystring: {
      type: 'object',
      properties: {
        status: { type: 'string', description: 'Filtro por estado del pedido' },
        q: { type: 'string', maxLength: 120, description: 'Búsqueda por ID o email del cliente' },
        limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
        offset: { type: 'integer', minimum: 0, default: 0 },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/orders', { schema: listOrdersSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const query = request.query as { status?: string; q?: string; limit?: string; offset?: string };
    const limit = Math.min(Math.max(Number(query.limit ?? 20) || 20, 1), 100);
    const offset = Math.max(Number(query.offset ?? 0) || 0, 0);
    const { items, total } = await service.searchOrders({
      state: query.status?.trim() || undefined,
      q: query.q?.trim() || undefined,
      limit,
      offset,
    });
    return reply.send({ items: items.map(serializeOrder), total });
  });

  const getOrderAdminSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Get order (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.get('/orders/:id/admin', { schema: getOrderAdminSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(orderIdParamSchema, request.params);
    const order = await service.getOrderAdmin(id);
    if (!order) throw new AppError(404, 'ORDER_NOT_FOUND', 'Order not found');
    return reply.send(serializeOrder(order));
  });

  const adminTransitionSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Force order transition (admin)',
    description: 'Fuerza una transición de estado de pedido.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['state'],
      properties: {
        state: {
          type: 'string',
          enum: [
            'PENDING',
            'AWAITING_PAYMENT',
            'PAID',
            'PROCESSING',
            'SHIPPED',
            'DELIVERED',
            'CANCELLED',
            'REFUNDED',
            'PARTIALLY_REFUNDED',
          ],
        },
        trackingNumber: { type: 'string', maxLength: 120, description: 'Número de seguimiento del envío (obligatorio al marcar como Enviado)' },
        courier: { type: 'string', maxLength: 120, description: 'Transportista del envío, de la lista editable en Ajustes (obligatorio al marcar como Enviado)' },
      },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.post(
    '/orders/:id/transitions/admin',
    { schema: adminTransitionSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { id } = parseJson(orderIdParamSchema, request.params);
      const { state, trackingNumber, courier } = parseJson(
        transitionSchema.extend({
          trackingNumber: z.string().min(1).max(120).optional(),
          courier: z.string().min(1).max(120).optional(),
        }),
        request.body,
      );
      if (state === 'SHIPPED' && !trackingNumber?.trim()) {
        throw new AppError(422, 'TRACKING_NUMBER_REQUIRED', 'Indica el número de seguimiento para marcar el pedido como enviado');
      }
      if (state === 'SHIPPED' && !courier?.trim()) {
        throw new AppError(422, 'COURIER_REQUIRED', 'Indica el transportista para marcar el pedido como enviado');
      }
      let order;
      try {
        order = await service.transitionAdmin(id, state as OrderState, trackingNumber?.trim() || undefined, courier?.trim() || undefined);
      } catch (error) {
        throw mapOrderError(error);
      }
      // Notificar al cliente el cambio de estado (sin bloquear la transición).
      let notified = false;
      let notificationError = '';
      try {
        // F-153: email is now associated on the order view via a repository join.
        const to = order.email;
        if (to) {
          await sendOrderStatusEmail(deps.pool, {
            to,
            orderId: order.id,
            state: order.state,
            trackingNumber: order.trackingNumber,
            courier: order.courier,
          });
          notified = true;
        } else {
          notificationError = 'El cliente no tiene email asociado';
        }
      } catch (error) {
        notificationError = error instanceof Error ? error.message : 'No se pudo enviar el email';
        request.log.warn({ err: error, orderId: order.id }, 'order_status_email_failed');
      }
      await recordOrderHistory(
        deps.pool,
        order.id,
        'STATE_CHANGE',
        `Estado cambiado a ${ORDER_STATE_LABELS[order.state] ?? order.state}${order.courier ? ` · Transportista: ${order.courier}` : ''}${order.trackingNumber ? ` · Tracking: ${order.trackingNumber}` : ''}`,
        order.trackingNumber ?? null,
        user.email,
      );
      return reply.send({ ...serializeOrder(order), notified, notificationError: notificationError || undefined });
    },
  );

  // ── Gestión de envío (admin): tracking + historial ────────────────────────
  const shippingSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Update order shipping (admin)',
    description: 'Actualiza el número de seguimiento del pedido y registra el cambio en el historial.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.post('/orders/:id/shipping', { schema: shippingSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(orderIdParamSchema, request.params);
    const { trackingNumber, courier, note } = parseJson(
      z.object({
        trackingNumber: z.string().trim().min(1).max(120),
        courier: z.string().trim().min(1).max(120).optional(),
        note: z.string().trim().max(500).optional(),
      }),
      request.body,
    );
    const existing = await service.getOrderAdmin(id);
    if (!existing) throw new AppError(404, 'ORDER_NOT_FOUND', 'Order not found');
    await deps.pool.query(
      `UPDATE orders_orders SET tracking_number = $2, courier = COALESCE($3, courier), updated_at = now() WHERE id = $1`,
      [id, trackingNumber, courier ?? null],
    );
    const message = `Seguimiento actualizado: ${trackingNumber}${courier ? ` · Transportista: ${courier}` : ''}${note ? ` · Nota: ${note}` : ''}`;
    await recordOrderHistory(deps.pool, id, 'SHIPPING_UPDATE', message, trackingNumber, user.email);
    const updated = await service.getOrderAdmin(id);
    return reply.send(serializeOrder(updated ?? { ...existing, trackingNumber, courier: courier ?? existing.courier }));
  });

  const historySchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Order history (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/orders/:id/history', { schema: historySchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(orderIdParamSchema, request.params);
    const result = await deps.pool.query<{
      id: string;
      event_type: string;
      message: string;
      tracking_number: string | null;
      actor_email: string | null;
      created_at: Date;
    }>(
      `SELECT id, event_type, message, tracking_number, actor_email, created_at
       FROM orders_order_history WHERE order_id = $1 ORDER BY created_at ASC, id ASC`,
      [id],
    );
    return reply.send({
      items: result.rows.map((row) => ({
        id: row.id,
        eventType: row.event_type,
        message: row.message,
        trackingNumber: row.tracking_number,
        actorEmail: row.actor_email,
        createdAt: row.created_at,
      })),
    });
  });

  const editItemsSchema: FastifySchema = {
    tags: ['Orders'],
    summary: 'Edit order items (admin)',
    description:
      'Reemplaza los artículos del pedido (agregar/quitar productos y ajustar cantidades). Los totales se recalculan con los precios e IVA actuales.',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema, 422: errorSchema },
  };
  app.put('/orders/:id/items', { schema: editItemsSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { id } = parseJson(orderIdParamSchema, request.params);
    const body = parseJson(
      z.object({
        items: z
          .array(
            z.object({
              variantId: z.uuid(),
              quantity: z.number().int().positive().max(999),
            }),
          )
          .min(1),
      }),
      request.body,
    );

    const existing = await service.getOrderAdmin(id);
    if (!existing) throw new AppError(404, 'ORDER_NOT_FOUND', 'Order not found');

    // Consolidar cantidades por variante.
    const byVariant = new Map<string, number>();
    for (const item of body.items) {
      byVariant.set(item.variantId, (byVariant.get(item.variantId) ?? 0) + item.quantity);
    }
    const variantIds = [...byVariant.keys()];

    // Resolver datos de catálogo y precio vigente de cada variante.
    const variantResult = await deps.pool.query<{
      variant_id: string;
      sku: string;
      ean: string | null;
      product_id: string;
      product_name: string;
      net_unit_amount_cents: number | null;
      vat_rate: string | null;
    }>(
      `SELECT v.id AS variant_id, v.sku, v.ean, v.product_id, p.name AS product_name,
              pr.net_unit_amount_cents, pr.vat_rate
       FROM catalog_product_variants v
       JOIN catalog_products p ON p.id = v.product_id
       LEFT JOIN pricing_variant_prices pr ON pr.variant_id = v.id
       WHERE v.id = ANY($1::uuid[])`,
      [variantIds],
    );
    const found = new Map(variantResult.rows.map((row) => [row.variant_id, row]));
    for (const variantId of variantIds) {
      if (!found.has(variantId)) {
        throw new AppError(422, 'VARIANT_NOT_FOUND', `La variante ${variantId.slice(0, 8)} no existe en el catálogo`);
      }
      const row = found.get(variantId);
      if (row?.net_unit_amount_cents == null) {
        throw new AppError(422, 'PRICE_MISSING', `Falta el precio de la variante ${row?.sku ?? variantId.slice(0, 8)}`);
      }
    }

    const VAT_POINTS: Record<string, number> = { general: 2100, reduced: 1000, 'super-reduced': 400 };
    const itemInputs = variantIds.map((variantId) => {
      const row = found.get(variantId)!;
      const quantity = byVariant.get(variantId)!;
      const unitPriceCents = row.net_unit_amount_cents!;
      const points = VAT_POINTS[row.vat_rate ?? 'general'] ?? 2100;
      const taxCents = Math.round((unitPriceCents * quantity * points) / 10_000);
      return {
        productId: row.product_id,
        variantId,
        sku: row.sku,
        ean: row.ean,
        name: row.product_name,
        unitPriceCents,
        discountCents: 0,
        taxCents,
        quantity,
      };
    });

    const netSubtotalCents = itemInputs.reduce((acc, item) => acc + item.unitPriceCents * item.quantity, 0);
    const taxCents = itemInputs.reduce((acc, item) => acc + item.taxCents, 0);
    const subtotalCents = netSubtotalCents + taxCents;
    const discountCents = Math.min(existing.discountCents, subtotalCents);
    // El coste de envío original se conserva: total - (subtotal - descuento).
    const shippingCents = Math.max(0, existing.totalCents - Math.max(0, existing.subtotalCents - existing.discountCents));
    const totalCents = Math.max(0, subtotalCents - discountCents) + shippingCents;

    try {
      const updated = await service.editItems(id, itemInputs, {
        subtotalCents,
        discountCents,
        taxCents,
        totalCents,
      });
      return reply.send(serializeOrder(updated));
    } catch (error) {
      throw mapOrderError(error);
    }
  });
}

function mapOrderError(error: unknown): Error {
  if (error instanceof OrderStateTransitionError)
    return new AppError(409, 'ORDER_STATE_TRANSITION_INVALID', error.message);
  if (error instanceof OrderNotFoundError)
    return new AppError(404, 'ORDER_NOT_FOUND', error.message);
  return error instanceof Error ? error : new Error('Unknown order error');
}

function serializeOrder(order: {
  id: string;
  state: string;
  currency: string;
  subtotalCents: number;
  discountCents: number;
  taxCents: number;
  totalCents: number;
  trackingNumber?: string | null;
  courier?: string | null;
  customerName?: string | null;
  customerPhone?: string | null;
  createdAt: Date;
  updatedAt: Date;
  idempotencyKey: string | null;
  userId?: string;
  email?: string | null;
  phone?: string | null;
  items: Array<{
    id: string;
    productId: string;
    variantId: string;
    sku: string;
    ean: string | null;
    name: string;
    unitPriceCents: number;
    discountCents: number;
    taxCents: number;
    quantity: number;
    createdAt: Date;
  }>;
  shippingAddress?: { recipientName: string; street: string; city: string; postalCode: string; country: string } | null;
  billingAddress?: { recipientName: string; street: string; city: string; postalCode: string; country: string } | null;
  payment?: { provider: string; status: string; amountCents: number; last4: string | null; createdAt: Date } | null;
}) {
  return {
    id: order.id,
    userId: order.userId,
    email: order.email ?? null,
    phone: order.phone ?? null,
    customerName: order.customerName ?? null,
    customerPhone: order.customerPhone ?? null,
    state: order.state,
    currency: order.currency,
    subtotalCents: order.subtotalCents,
    discountCents: order.discountCents,
    taxCents: order.taxCents,
    totalCents: order.totalCents,
    trackingNumber: order.trackingNumber ?? null,
    courier: order.courier ?? null,
    idempotencyKey: order.idempotencyKey,
    items: order.items.map((item) => ({
      id: item.id,
      productId: item.productId,
      variantId: item.variantId,
      sku: item.sku,
      ean: item.ean,
      name: item.name,
      unitPriceCents: item.unitPriceCents,
      discountCents: item.discountCents,
      taxCents: item.taxCents,
      quantity: item.quantity,
      createdAt: item.createdAt.toISOString(),
    })),
    shippingAddress: order.shippingAddress ?? null,
    billingAddress: order.billingAddress ?? null,
    payment: order.payment ? { ...order.payment, createdAt: order.payment.createdAt.toISOString() } : null,
    createdAt: order.createdAt.toISOString(),
    updatedAt: order.updatedAt.toISOString(),
  };
}

/**
 * Registra un evento en el historial del pedido. Los fallos se loguean pero
 * nunca bloquean la operación principal (transición o actualización de envío).
 */
async function recordOrderHistory(
  pool: pg.Pool,
  orderId: string,
  eventType: string,
  message: string,
  trackingNumber: string | null,
  actorEmail: string,
): Promise<void> {
  try {
    await pool.query(
      `INSERT INTO orders_order_history (order_id, event_type, message, tracking_number, actor_email)
       VALUES ($1, $2, $3, $4, $5)`,
      [orderId, eventType, message, trackingNumber, actorEmail],
    );
  } catch {
    // Historial es best-effort: no debe romper la operación principal.
  }
}
