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 { ShippingService } from '../application/shipping-service.js';
import { InvalidShippingAddressError, ShippingZoneNotFoundError } from '../domain/errors.js';
import type { ShippingQuote } from '../domain/shipping.js';
import { PgShippingRepository } from '../infrastructure/pg-shipping-repository.js';

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

const zoneBodySchema = z.object({
  name: z.string().min(1).max(120),
  country: z.string().min(2).max(80),
  postalCodePrefix: z.string().min(1).max(20).optional().nullable(),
  active: z.boolean().optional(),
});

const methodBodySchema = z.object({
  zoneId: z.uuid(),
  name: z.string().min(1).max(120),
  baseCostCents: z.number().int().min(0),
  freeShippingThresholdCents: z.number().int().min(0).optional().nullable(),
  description: z.string().max(500).optional().nullable(),
  maxWeightKg: z.number().positive().max(100000).optional().nullable(),
  freeShippingMaxWeightKg: z.number().positive().max(100000).optional().nullable(),
  active: z.boolean().optional(),
});

const calculateBodySchema = z
  .object({
    cartTotalCents: z.number().int().min(0),
    country: z.string().min(2).max(80),
    postalCode: z.string().min(1).max(20),
    cartWeightKg: z.number().min(0).max(100000).optional(),
  })
  .strip();

export async function registerShippingRoutes(
  app: FastifyInstance,
  deps: ShippingRoutesDeps,
): Promise<void> {
  const service = new ShippingService(new PgShippingRepository(deps.pool));

  const createZoneSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Create shipping zone (admin)',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/shipping/zones', { schema: createZoneSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(zoneBodySchema, request.body);
    const result = await deps.pool.query<{ id: string }>(
      `INSERT INTO shipping_zones (name, country, postal_code_prefix, active)
       VALUES ($1, $2, $3, $4) RETURNING id`,
      [input.name, input.country, input.postalCodePrefix ?? null, input.active ?? true],
    );
    return reply.code(201).send({ id: result.rows[0]?.id });
  });

  const createMethodSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Create shipping method (admin)',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/shipping/methods', { schema: createMethodSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(methodBodySchema, request.body);
    const result = await deps.pool.query<{ id: string }>(
      `INSERT INTO shipping_methods (zone_id, name, base_cost_cents, free_shipping_threshold_cents, description, max_weight_kg, free_shipping_max_weight_kg, active)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
      [
        input.zoneId,
        input.name,
        input.baseCostCents,
        input.freeShippingThresholdCents ?? null,
        input.description ?? null,
        input.maxWeightKg ?? null,
        input.freeShippingMaxWeightKg ?? null,
        input.active ?? true,
      ],
    );
    return reply.code(201).send({ id: result.rows[0]?.id });
  });

  // ── Admin management ───────────────────────────────────────────────────────
  const listZonesSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'List zones (admin)',
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/shipping/zones', { schema: listZonesSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const result = await deps.pool.query<{
      id: string;
      name: string;
      country: string;
      postal_code_prefix: string | null;
      active: boolean;
    }>('SELECT * FROM shipping_zones ORDER BY created_at DESC');
    return reply.send({
      items: result.rows.map((r) => ({
        id: r.id,
        name: r.name,
        country: r.country,
        postalCodePrefix: r.postal_code_prefix,
        active: r.active,
      })),
    });
  });

  const patchZoneSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Update zone (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.patch('/admin/shipping/zones/:id', { schema: patchZoneSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const id = (request.params as { id: string }).id;
    const patch = parseJson(
      z.object({
        name: z.string().min(1).max(120).optional(),
        country: z.string().min(2).max(80).optional(),
        postalCodePrefix: z.string().max(20).optional().nullable(),
        active: z.boolean().optional(),
      }),
      request.body,
    );
    const sets: string[] = [];
    const values: unknown[] = [];
    let i = 1;
    if (patch.name !== undefined) {
      sets.push(`name = $${i++}`);
      values.push(patch.name);
    }
    if (patch.country !== undefined) {
      sets.push(`country = $${i++}`);
      values.push(patch.country);
    }
    if (patch.postalCodePrefix !== undefined) {
      sets.push(`postal_code_prefix = $${i++}`);
      values.push(patch.postalCodePrefix);
    }
    if (patch.active !== undefined) {
      sets.push(`active = $${i++}`);
      values.push(patch.active);
    }
    if (!sets.length) {
      return reply.send({ ok: true });
    }
    values.push(id);
    await deps.pool.query(`UPDATE shipping_zones SET ${sets.join(', ')} WHERE id = $${i}`, values);
    return reply.send({ ok: true });
  });

  const deleteZoneSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Delete zone (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema },
  };
  app.delete('/admin/shipping/zones/:id', { schema: deleteZoneSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const id = (request.params as { id: string }).id;
    await deps.pool.query('DELETE FROM shipping_methods WHERE zone_id = $1', [id]);
    await deps.pool.query('DELETE FROM shipping_zones WHERE id = $1', [id]);
    return reply.code(204).send();
  });

  const listMethodsSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'List methods (admin)',
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/shipping/methods', { schema: listMethodsSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const result = await deps.pool.query<{
      id: string;
      zone_id: string;
      zone_name: string;
      name: string;
      base_cost_cents: number;
      free_shipping_threshold_cents: number | null;
      description: string | null;
      max_weight_kg: string | number | null;
      free_shipping_max_weight_kg: string | number | null;
      active: boolean;
    }>(
      `SELECT sm.*, sz.name as zone_name FROM shipping_methods sm
       JOIN shipping_zones sz ON sz.id = sm.zone_id
       ORDER BY sm.created_at DESC`,
    );
    return reply.send({
      items: result.rows.map((r) => ({
        id: r.id,
        zoneId: r.zone_id,
        zoneName: r.zone_name,
        name: r.name,
        baseCostCents: r.base_cost_cents,
        freeShippingThresholdCents: r.free_shipping_threshold_cents,
        description: r.description,
        maxWeightKg: r.max_weight_kg === null ? null : Number(r.max_weight_kg),
        freeShippingMaxWeightKg:
          r.free_shipping_max_weight_kg === null ? null : Number(r.free_shipping_max_weight_kg),
        active: r.active,
      })),
    });
  });

  app.post('/admin/shipping/methods', async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(methodBodySchema, request.body);
    const result = await deps.pool.query<{ id: string }>(
      `INSERT INTO shipping_methods (zone_id, name, base_cost_cents, free_shipping_threshold_cents, max_weight_kg, free_shipping_max_weight_kg, active)
       VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
      [
        input.zoneId,
        input.name,
        input.baseCostCents,
        input.freeShippingThresholdCents ?? null,
        input.maxWeightKg ?? null,
        input.freeShippingMaxWeightKg ?? null,
        input.active ?? true,
      ],
    );
    return reply.code(201).send({ id: result.rows[0]?.id });
  });

  const patchMethodSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Update method (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.patch(
    '/admin/shipping/methods/:id',
    { schema: patchMethodSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const id = (request.params as { id: string }).id;
      const patch = parseJson(
        z.object({
          name: z.string().min(1).max(120).optional(),
          baseCostCents: z.number().int().min(0).optional(),
          freeShippingThresholdCents: z.number().int().min(0).optional().nullable(),
          description: z.string().max(500).optional().nullable(),
          maxWeightKg: z.number().positive().max(100000).optional().nullable(),
          freeShippingMaxWeightKg: z.number().positive().max(100000).optional().nullable(),
          active: z.boolean().optional(),
        }),
        request.body,
      );
      const sets: string[] = [];
      const values: unknown[] = [];
      let i = 1;
      if (patch.name !== undefined) {
        sets.push(`name = $${i++}`);
        values.push(patch.name);
      }
      if (patch.baseCostCents !== undefined) {
        sets.push(`base_cost_cents = $${i++}`);
        values.push(patch.baseCostCents);
      }
      if (patch.freeShippingThresholdCents !== undefined) {
        sets.push(`free_shipping_threshold_cents = $${i++}`);
        values.push(patch.freeShippingThresholdCents);
      }
      if (patch.description !== undefined) {
        sets.push(`description = $${i++}`);
        values.push(patch.description);
      }
      if (patch.maxWeightKg !== undefined) {
        sets.push(`max_weight_kg = $${i++}`);
        values.push(patch.maxWeightKg);
      }
      if (patch.freeShippingMaxWeightKg !== undefined) {
        sets.push(`free_shipping_max_weight_kg = $${i++}`);
        values.push(patch.freeShippingMaxWeightKg);
      }
      if (patch.active !== undefined) {
        sets.push(`active = $${i++}`);
        values.push(patch.active);
      }
      if (!sets.length) {
        return reply.send({ ok: true });
      }
      values.push(id);
      await deps.pool.query(
        `UPDATE shipping_methods SET ${sets.join(', ')} WHERE id = $${i}`,
        values,
      );
      return reply.send({ ok: true });
    },
  );

  const deleteMethodSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Delete method (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema },
  };
  app.delete(
    '/admin/shipping/methods/:id',
    { schema: deleteMethodSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const id = (request.params as { id: string }).id;
      await deps.pool.query('DELETE FROM shipping_methods WHERE id = $1', [id]);
      return reply.code(204).send();
    },
  );

  // ── Customer-facing ───────────────────────────────────────────────────────
  const calcShippingSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'Calculate shipping (público)',
    body: {
      type: 'object',
      required: ['cartTotalCents', 'country', 'postalCode'],
      properties: {
        cartTotalCents: { type: 'integer', minimum: 0 },
        country: { type: 'string' },
        postalCode: { type: 'string' },
      },
    },
  };
  app.post('/shipping/calculate', { schema: calcShippingSchema }, async (request, reply) => {
    const input = parseJson(calculateBodySchema, request.body);
    try {
      const quote = await service.calculate(
        input.cartTotalCents,
        {
          country: input.country,
          postalCode: input.postalCode,
        },
        input.cartWeightKg ?? 0,
      );
      return reply.send(serializeQuote(quote));
    } catch (error) {
      throw mapShippingError(error);
    }
  });

  // ── Public list of active shipping methods ───────────────────────────────────────────────────
  const publicMethodsSchema: FastifySchema = {
    tags: ['Shipping'],
    summary: 'List active shipping methods (public)',
    querystring: {
      type: 'object',
      properties: {
        country: { type: 'string', description: 'Filter by country (defaults to ES)' },
        postalCode: { type: 'string', description: 'Filter by postal code prefix' },
      },
    },
    response: { 200: { type: 'object' } },
  };
  app.get('/shipping/methods', { schema: publicMethodsSchema }, async (request, reply) => {
    const query = request.query as { country?: string; postalCode?: string };
    const country = query.country ?? 'ES';
    const result = await deps.pool.query<{
      id: string;
      zone_id: string;
      name: string;
      base_cost_cents: number;
      free_shipping_threshold_cents: number | null;
      description: string | null;
      max_weight_kg: string | number | null;
      free_shipping_max_weight_kg: string | number | null;
    }>(
      `SELECT sm.id, sm.zone_id, sm.name, sm.base_cost_cents,
              sm.free_shipping_threshold_cents, sm.description,
              sm.max_weight_kg, sm.free_shipping_max_weight_kg
       FROM shipping_methods sm
       JOIN shipping_zones sz ON sz.id = sm.zone_id
       WHERE sm.active = true
         AND sz.active = true
         AND sz.country = $1
         AND ($2::text IS NULL OR sz.postal_code_prefix IS NULL OR $2 LIKE sz.postal_code_prefix || '%')
       ORDER BY sm.base_cost_cents ASC`,
      [country, query.postalCode ?? null],
    );
    return reply.send({
      items: result.rows.map((r) => ({
        id: r.id,
        zoneId: r.zone_id,
        name: r.name,
        baseCostCents: r.base_cost_cents,
        freeShippingThresholdCents: r.free_shipping_threshold_cents,
        description: r.description,
        maxWeightKg: r.max_weight_kg === null ? null : Number(r.max_weight_kg),
        freeShippingMaxWeightKg:
          r.free_shipping_max_weight_kg === null ? null : Number(r.free_shipping_max_weight_kg),
      })),
    });
  });
}

function mapShippingError(error: unknown): Error {
  if (error instanceof ShippingZoneNotFoundError)
    return new AppError(422, 'SHIPPING_ZONE_NOT_FOUND', error.message);
  if (error instanceof InvalidShippingAddressError)
    return new AppError(400, 'INVALID_SHIPPING_ADDRESS', error.message);
  return error instanceof Error ? error : new Error('Unknown shipping error');
}

function serializeQuote(quote: ShippingQuote) {
  return {
    zoneId: quote.zoneId,
    methodId: quote.methodId,
    methodName: quote.methodName,
    costCents: quote.costCents,
    freeApplied: quote.freeApplied,
  };
}
