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 { InventoryService } from '../application/inventory-service.js';
import {
  InsufficientReservedStockError,
  InsufficientStockError,
  InvalidStockQuantityError,
} from '../domain/errors.js';
import type { SetAvailableStockCommand, StockItem } from '../domain/stock.js';
import { DEFAULT_STORE_ID } from '../domain/stock.js';
import { PgInventoryRepository } from '../infrastructure/pg-inventory-repository.js';

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

interface InventoryOverviewRow {
  product_id: string;
  product_name: string;
  product_slug: string;
  product_active: boolean;
  product_created_at: Date;
  expiration_date: string | null;
  variant_id: string;
  sku: string;
  ean: string | null;
  available_quantity: number;
  net_unit_amount_cents: number | null;
  offer_cents: number | null;
  cost_cents: number | null;
  vat_rate: 'general' | 'reduced' | 'super-reduced' | null;
  gross_unit_amount_cents: number | null;
  margin_percent: number | null;
}

interface InventoryOverviewStatsRow {
  total: number;
  in_stock: number;
  low_stock: number;
  out_of_stock: number;
}

const variantParamSchema = z.object({ variantId: z.uuid() });
const productParamSchema = z.object({ productId: z.uuid() });
const availabilityQuerySchema = z.object({
  quantity: z.coerce.number().int().positive().default(1),
  storeId: z.uuid().optional(),
});
const stockBodySchema = z.object({
  quantity: z.number().int().min(0),
  storeId: z.uuid().optional(),
});
const stockCommandBodySchema = z.object({
  quantity: z.number().int().positive(),
  storeId: z.uuid().optional(),
});

const bulkAdjustItemSchema = z.object({
  variantId: z.uuid(),
  quantity: z.number().int().min(0),
});
const bulkAdjustBodySchema = z.object({
  items: z.array(bulkAdjustItemSchema).min(1).max(100),
});

const inventoryOverviewFilterSchema = z.enum([
  'all',
  'in_stock',
  'low_stock',
  'out_of_stock',
  'expiring',
  'low_margin',
]);
const inventoryOverviewQuerySchema = z.object({
  q: z.string().trim().max(120).optional(),
  filter: inventoryOverviewFilterSchema.default('all'),
  limit: z.coerce.number().int().min(1).max(200).default(50),
  offset: z.coerce.number().int().min(0).max(100_000).default(0),
});

export async function registerInventoryRoutes(
  app: FastifyInstance,
  deps: InventoryRoutesDeps,
): Promise<void> {
  const inventory = new InventoryService(new PgInventoryRepository(deps.pool));

  const inventoryOverviewSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Inventory overview (admin)',
    querystring: {
      type: 'object',
      properties: {
        q: { type: 'string', maxLength: 120 },
        filter: {
          type: 'string',
          enum: ['all', 'in_stock', 'low_stock', 'out_of_stock', 'expiring', 'low_margin'],
          default: 'all',
        },
        limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 },
        offset: { type: 'integer', minimum: 0, default: 0 },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/inventory/admin/overview', { schema: inventoryOverviewSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { q, filter, limit, offset } = parseJson(inventoryOverviewQuerySchema, request.query ?? {});

    const params: unknown[] = [DEFAULT_STORE_ID];
    let nextParam = 2;
    const searchConditions: string[] = [];
    if (q) {
      const placeholder = `$${nextParam++}`;
      params.push(`%${q}%`);
      searchConditions.push(
        `(product_name ILIKE ${placeholder} OR sku ILIKE ${placeholder} OR COALESCE(ean, '') ILIKE ${placeholder})`,
      );
    }
    const filterCondition = inventoryOverviewFilterClause(filter);
    const whereSearch = searchConditions.length > 0 ? `WHERE ${searchConditions.join(' AND ')}` : '';
    const whereItems = [
      ...searchConditions,
      ...(filterCondition ? [filterCondition] : []),
    ];
    const whereItemsSql = whereItems.length > 0 ? `WHERE ${whereItems.join(' AND ')}` : '';
    const orderBy = inventoryOverviewOrderBy(filter);

    const baseSql = `
      WITH raw AS (
        SELECT
          p.id AS product_id,
          p.name AS product_name,
          p.slug AS product_slug,
          p.state = 'active' AS product_active,
          p.created_at AS product_created_at,
          p.expiration_date,
          v.id AS variant_id,
          v.sku AS variant_sku,
          v.ean,
          COALESCE(stock.available, 0) AS available_quantity,
          price.net_unit_amount_cents,
          price.offer_cents,
          price.cost_cents,
          price.vat_rate,
          COALESCE(
            price.offer_cents,
            ROUND(
              price.net_unit_amount_cents * CASE price.vat_rate
                WHEN 'general' THEN 1.21
                WHEN 'reduced' THEN 1.10
                WHEN 'super-reduced' THEN 1.04
                ELSE 1.21
              END
            )::int
          ) AS gross_unit_amount_cents
        FROM catalog_product_variants v
        JOIN catalog_products p ON p.id = v.product_id
        LEFT JOIN inventory_stock stock ON stock.variant_id = v.id AND stock.store_id = $1
        LEFT JOIN pricing_variant_prices price ON price.variant_id = v.id AND price.currency = 'EUR'
      ),
      base AS (
        SELECT
          product_id,
          product_name,
          product_slug,
          product_active,
          product_created_at,
          expiration_date,
          variant_id,
          variant_sku AS sku,
          ean,
          available_quantity,
          net_unit_amount_cents,
          offer_cents,
          cost_cents,
          vat_rate,
          gross_unit_amount_cents,
          CASE
            WHEN cost_cents IS NULL OR gross_unit_amount_cents IS NULL OR gross_unit_amount_cents <= 0 THEN NULL
            ELSE ROUND((((gross_unit_amount_cents - cost_cents)::numeric / gross_unit_amount_cents::numeric) * 100))::int
          END AS margin_percent
        FROM raw
      )
    `;

    const statsResult = await deps.pool.query<InventoryOverviewStatsRow>(
      `${baseSql}
       SELECT
         COUNT(*)::int AS total,
         COUNT(*) FILTER (WHERE available_quantity >= 5)::int AS in_stock,
         COUNT(*) FILTER (WHERE available_quantity > 0 AND available_quantity < 5)::int AS low_stock,
         COUNT(*) FILTER (WHERE available_quantity = 0)::int AS out_of_stock
       FROM base
       ${whereSearch}`,
      params,
    );

    const countParams = [...params, limit, offset];
    const countResult = await deps.pool.query<{ total: string }>(
      `${baseSql}
       SELECT COUNT(*)::int AS total
       FROM base
       ${whereItemsSql}`,
      params,
    );
    const rows = await deps.pool.query<InventoryOverviewRow>(
      `${baseSql}
       SELECT *
       FROM base
       ${whereItemsSql}
       ORDER BY ${orderBy}
       LIMIT $${nextParam++} OFFSET $${nextParam++}`,
      countParams,
    );

    return reply.send({
      items: rows.rows.map(serializeInventoryOverviewRow),
      total: Number(countResult.rows[0]?.total ?? 0),
      stats: serializeInventoryOverviewStats(statsResult.rows[0]),
    });
  });

  const availabilitySchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Check availability (público)',
    params: {
      type: 'object',
      required: ['variantId'],
      properties: { variantId: { type: 'string', format: 'uuid' } },
    },
    querystring: {
      type: 'object',
      properties: {
        quantity: { type: 'integer', default: 1 },
        storeId: { type: 'string', format: 'uuid' },
      },
    },
  };
  app.get(
    '/inventory/:variantId/availability',
    { schema: availabilitySchema },
    async (request, reply) => {
      const { variantId } = parseJson(variantParamSchema, request.params);
      const { quantity, storeId } = parseJson(availabilityQuerySchema, request.query);
      const availability = await inventory.checkAvailability(
        variantId,
        storeId ?? DEFAULT_STORE_ID,
        quantity,
      );
      return reply.send(availability);
    },
  );

  // GET /inventory/product/:productId/availability — lookup first variant and return stock
  const productAvailabilitySchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Check availability by productId (público)',
    params: {
      type: 'object',
      required: ['productId'],
      properties: { productId: { type: 'string', format: 'uuid' } },
    },
    querystring: {
      type: 'object',
      properties: {
        quantity: { type: 'integer', default: 1 },
        storeId: { type: 'string', format: 'uuid' },
      },
    },
  };
  app.get(
    '/inventory/product/:productId/availability',
    { schema: productAvailabilitySchema },
    async (request, reply) => {
      const { productId } = parseJson(productParamSchema, request.params);
      const { quantity, storeId } = parseJson(availabilityQuerySchema, request.query);

      // Lookup the first variant for this product
      const variantResult = await deps.pool.query(
        `SELECT id FROM product_variants WHERE product_id = $1 LIMIT 1`,
        [productId],
      );
      if (variantResult.rows.length === 0) {
        throw new AppError(404, 'VARIANT_NOT_FOUND', 'No variant found for this product');
      }
      const variantId = variantResult.rows[0].id;

      const availability = await inventory.checkAvailability(
        variantId,
        storeId ?? DEFAULT_STORE_ID,
        quantity,
      );
      return reply.send(availability);
    },
  );

  const setStockSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Set available stock (admin)',
    params: {
      type: 'object',
      required: ['variantId'],
      properties: { variantId: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['quantity'],
      properties: { quantity: { type: 'integer', minimum: 0 } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.put('/inventory/:variantId/stock', { schema: setStockSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { variantId } = parseJson(variantParamSchema, request.params);
    const { quantity, storeId } = parseJson(stockBodySchema, request.body);
    try {
      const item = await inventory.setAvailable({
        variantId,
        storeId: storeId ?? DEFAULT_STORE_ID,
        quantity,
      });
      return reply.send(serializeStockItem(item));
    } catch (error) {
      throw mapInventoryError(error);
    }
  });

  const reserveSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Reserve stock (admin)',
    params: {
      type: 'object',
      required: ['variantId'],
      properties: { variantId: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['quantity'],
      properties: { quantity: { type: 'integer', minimum: 1 } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.post(
    '/inventory/:variantId/reservations',
    { schema: reserveSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { variantId } = parseJson(variantParamSchema, request.params);
      const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
      try {
        const item = await inventory.reserve({
          variantId,
          storeId: storeId ?? DEFAULT_STORE_ID,
          quantity,
        });
        return reply.code(201).send(serializeStockItem(item));
      } catch (error) {
        throw mapInventoryError(error);
      }
    },
  );

  const releaseSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Release reservation (admin)',
    params: {
      type: 'object',
      required: ['variantId'],
      properties: { variantId: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['quantity'],
      properties: { quantity: { type: 'integer', minimum: 1 } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.post(
    '/inventory/:variantId/reservations/release',
    { schema: releaseSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { variantId } = parseJson(variantParamSchema, request.params);
      const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
      try {
        const item = await inventory.release({
          variantId,
          storeId: storeId ?? DEFAULT_STORE_ID,
          quantity,
        });
        return reply.send(serializeStockItem(item));
      } catch (error) {
        throw mapInventoryError(error);
      }
    },
  );

  const confirmSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Confirm reservation (admin)',
    params: {
      type: 'object',
      required: ['variantId'],
      properties: { variantId: { type: 'string', format: 'uuid' } },
    },
    body: {
      type: 'object',
      required: ['quantity'],
      properties: { quantity: { type: 'integer', minimum: 1 } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.post(
    '/inventory/:variantId/reservations/confirm',
    { schema: confirmSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const { variantId } = parseJson(variantParamSchema, request.params);
      const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
      try {
        const item = await inventory.confirm({
          variantId,
          storeId: storeId ?? DEFAULT_STORE_ID,
          quantity,
        });
        return reply.send(serializeStockItem(item));
      } catch (error) {
        throw mapInventoryError(error);
      }
    },
  );

  // POST /inventory/bulk-adjust — atomic bulk stock adjustment
  const bulkSchema: FastifySchema = {
    tags: ['Inventory'],
    summary: 'Bulk stock adjustment (admin)',
    description: 'Ajuste atómico de stock para múltiples variantes en una transacción.',
    body: {
      type: 'object',
      required: ['items'],
      properties: {
        items: {
          type: 'array',
          items: {
            type: 'object',
            required: ['variantId', 'quantity'],
            properties: {
              variantId: { type: 'string', format: 'uuid' },
              quantity: { type: 'integer', minimum: 0 },
            },
          },
        },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.post('/inventory/bulk-adjust', { schema: bulkSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { items } = parseJson(bulkAdjustBodySchema, request.body);

    const client = await deps.pool.connect();
    try {
      await client.query('BEGIN');
      const results: StockItem[] = [];

      for (const item of items) {
        const command: SetAvailableStockCommand = {
          variantId: item.variantId,
          storeId: DEFAULT_STORE_ID,
          quantity: item.quantity,
        };

        // Use direct SQL for atomicity within the transaction
        const result = await client.query<{
          id: string;
          variant_id: string;
          store_id: string;
          available: number;
          reserved: number;
          sold: number;
          incoming: number;
          created_at: Date;
          updated_at: Date;
        }>(
          `INSERT INTO inventory_stock (variant_id, store_id, available)
           VALUES ($1, $2, $3)
           ON CONFLICT (variant_id, store_id) DO UPDATE
             SET available = EXCLUDED.available, updated_at = now()
           RETURNING *`,
          [command.variantId, command.storeId, command.quantity],
        );

        // Log the movement
        await client.query(
          `INSERT INTO inventory_movements (variant_id, store_id, operation, quantity)
           VALUES ($1, $2, $3, $4)`,
          [command.variantId, command.storeId, 'bulk_adjust', command.quantity],
        );

        const row = result.rows[0];
        if (!row) {
          throw new Error(`Inventory stock not found for variant ${command.variantId}`);
        }
        results.push({
          id: row.id,
          variantId: row.variant_id,
          storeId: row.store_id,
          available: row.available,
          reserved: row.reserved,
          sold: row.sold,
          incoming: row.incoming,
          createdAt: row.created_at,
          updatedAt: row.updated_at,
        });
      }

      await client.query('COMMIT');
      return reply.code(201).send({ adjusted: results.map(serializeStockItem), errors: [] });
    } catch (error) {
      await client.query('ROLLBACK');
      throw mapInventoryError(error);
    } finally {
      client.release();
    }
  });
}

function mapInventoryError(error: unknown): Error {
  if (error instanceof InsufficientStockError) {
    return new AppError(409, 'INSUFFICIENT_STOCK', error.message);
  }
  if (error instanceof InsufficientReservedStockError) {
    return new AppError(409, 'INSUFFICIENT_RESERVED_STOCK', error.message);
  }
  if (error instanceof InvalidStockQuantityError) {
    return new AppError(422, 'INVALID_STOCK_QUANTITY', error.message);
  }
  return error instanceof Error ? error : new Error('Unknown inventory error');
}

function serializeStockItem(item: StockItem) {
  return {
    id: item.id,
    variantId: item.variantId,
    available: item.available,
    reserved: item.reserved,
    sold: item.sold,
    incoming: item.incoming,
    createdAt: item.createdAt.toISOString(),
    updatedAt: item.updatedAt.toISOString(),
  };
}

function inventoryOverviewFilterClause(
  filter: z.infer<typeof inventoryOverviewFilterSchema>,
): string {
  if (filter === 'in_stock') return 'available_quantity >= 5';
  if (filter === 'low_stock') return 'available_quantity > 0 AND available_quantity < 5';
  if (filter === 'out_of_stock') return 'available_quantity = 0';
  if (filter === 'expiring') {
    return "expiration_date IS NOT NULL AND expiration_date <= current_date + 30";
  }
  if (filter === 'low_margin') return 'margin_percent IS NOT NULL AND margin_percent < 30';
  return '';
}

function inventoryOverviewOrderBy(filter: z.infer<typeof inventoryOverviewFilterSchema>): string {
  if (filter === 'expiring') {
    return 'expiration_date ASC NULLS LAST, product_name ASC, sku ASC';
  }
  if (filter === 'low_margin') {
    return 'margin_percent ASC NULLS LAST, product_name ASC, sku ASC';
  }
  return 'product_created_at DESC, product_name ASC, sku ASC';
}

function serializeInventoryOverviewRow(row: InventoryOverviewRow) {
  return {
    productId: row.product_id,
    productName: row.product_name,
    productSlug: row.product_slug,
    productActive: row.product_active,
    expirationDate: row.expiration_date,
    variant: {
      id: row.variant_id,
      productId: row.product_id,
      sku: row.sku,
      ean: row.ean,
      attributes: {},
    },
    stock: {
      available: row.available_quantity > 0,
      availableQuantity: Number(row.available_quantity),
    },
    price: row.net_unit_amount_cents === null || row.vat_rate === null
      ? null
      : {
          variantId: row.variant_id,
          netUnitAmountCents: Number(row.net_unit_amount_cents),
          offerCents: row.offer_cents === null ? null : Number(row.offer_cents),
          costCents: row.cost_cents === null ? null : Number(row.cost_cents),
          vatRate: row.vat_rate,
          currency: 'EUR',
        },
    marginPercent: row.margin_percent === null ? null : Number(row.margin_percent),
  };
}

function serializeInventoryOverviewStats(row: InventoryOverviewStatsRow | undefined) {
  return {
    total: Number(row?.total ?? 0),
    inStock: Number(row?.in_stock ?? 0),
    lowStock: Number(row?.low_stock ?? 0),
    outOfStock: Number(row?.out_of_stock ?? 0),
  };
}
