import type pg from 'pg';
import { AppError } from '../../../shared/errors.js';
import type { PosReturnInput, PosReturnResult } from '../domain/pos-sale.js';
import { buildPosReturnReceipt } from './build-pos-receipt.js';

interface OrderRow {
  id: string;
  source: string;
  state: string;
  terminal_id: string;
  cash_session_id: string;
  store_id: string;
  total_cents: string;
}

interface OrderItemRow {
  id: string;
  name: string;
  quantity: number;
  returned_quantity: number;
  is_free_item: boolean;
  variant_id: string | null;
  unit_price_cents: number;
  discount_cents: number;
  tax_cents: number;
}

interface SessionRow {
  id: string;
  terminal_id: string;
  store_id: string;
  status: string;
}

interface ReplayReturnRow {
  id: string;
  total_cents: string;
  status: string;
}

/**
 * Apply a partial or full return to a POS sale.
 *
 * - Locks the order and stock rows.
 * - Restores inventory for returned stock lines.
 * - Updates per-line `returned_quantity` counters.
 * - Emits a single `reporting_payment_lines` row (`status='refund'` or `'partial_refund'`).
 * - Adjusts `pos_cash_sessions.expected_cash_cents` only when cash is refunded.
 * - Transitions the order to `PARTIALLY_REFUNDED` or `REFUNDED`.
 * - Records audit + history events. Returns the return receipt payload.
 */
export class ApplyPosReturnUseCase {
  constructor(private readonly pool: pg.Pool) {}

  async execute(input: PosReturnInput): Promise<PosReturnResult> {
    if (input.lines.length === 0) {
      throw new AppError(400, 'POS_RETURN_EMPTY', 'Selecciona al menos una línea a devolver');
    }
    for (const line of input.lines) {
      if (!Number.isInteger(line.returnedQuantity) || line.returnedQuantity <= 0) {
        throw new AppError(
          400,
          'POS_RETURN_INVALID_QUANTITY',
          'La cantidad devuelta debe ser un entero positivo',
        );
      }
    }

    const client = await this.pool.connect();
    try {
      // Idempotent replay check uses a small dedicated transaction so the
      // read does not stay open while we do the (possibly heavyweight)
      // mutation work.
      const replay = await client.query<ReplayReturnRow>(
        `SELECT id, total_cents, status
           FROM pos_returns
          WHERE order_id = $1 AND idempotency_key = $2`,
        [input.orderId, input.idempotencyKey],
      );
      if (replay.rows[0]) {
        const previous = replay.rows[0];
        const receipt = await buildPosReturnReceipt(
          client,
          input.orderId,
          Number(previous.total_cents),
        );
        return {
          orderId: input.orderId,
          returnId: previous.id,
          idempotencyKey: input.idempotencyKey,
          state: previous.status === 'refund' ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
          refundedCents: Number(previous.total_cents),
          receipt,
        };
      }

      await client.query('BEGIN');

      const orderRow = await client.query<OrderRow>(
        `SELECT id, source, state, terminal_id, cash_session_id, store_id, total_cents
           FROM orders_orders WHERE id = $1 FOR UPDATE`,
        [input.orderId],
      );
      const order = orderRow.rows[0];
      if (!order || order.source !== 'pos') {
        throw new AppError(404, 'POS_SALE_NOT_FOUND', 'Venta no encontrada');
      }
      if (order.state !== 'COMPLETED' && order.state !== 'PARTIALLY_REFUNDED') {
        throw new AppError(
          409,
          'POS_RETURN_NOT_ALLOWED',
          'La venta no admite devoluciones en su estado actual',
        );
      }
      if (order.terminal_id !== input.terminalId || order.cash_session_id !== input.cashSessionId) {
        throw new AppError(
          409,
          'POS_SALE_TERMINAL_MISMATCH',
          'La venta pertenece a otro terminal o sesión de caja',
        );
      }

      const sessionRow = await client.query<SessionRow>(
        `SELECT id, terminal_id, store_id, status
           FROM pos_cash_sessions WHERE id = $1 FOR UPDATE`,
        [input.cashSessionId],
      );
      const session = sessionRow.rows[0];
      if (!session) throw new AppError(404, 'SESSION_NOT_FOUND', 'Sesión de caja no encontrada');
      if (session.status !== 'OPEN') {
        throw new AppError(409, 'SESSION_CLOSED', 'La sesión de caja está cerrada');
      }

      const itemRows = await client.query<OrderItemRow>(
        `SELECT id, name, quantity, returned_quantity, is_free_item, variant_id,
                unit_price_cents, discount_cents, tax_cents
           FROM orders_items
          WHERE order_id = $1
          FOR UPDATE`,
        [input.orderId],
      );
      const itemsById = new Map(itemRows.rows.map((row) => [row.id, row]));
      let totalRefundCents = 0;
      const stockUpdates: Array<{
        variantId: string;
        storeId: string;
        quantity: number;
      }> = [];
      for (const line of input.lines) {
        const item = itemsById.get(line.orderItemId);
        if (!item) {
          throw new AppError(404, 'POS_RETURN_ITEM_NOT_FOUND', 'Línea de venta no encontrada');
        }
        const remaining = item.quantity - item.returned_quantity;
        if (remaining <= 0) {
          throw new AppError(
            409,
            'POS_RETURN_LINE_FULLY_RETURNED',
            'La línea ya está devuelta por completo',
          );
        }
        if (item.is_free_item) {
          if (line.returnedQuantity !== item.quantity - item.returned_quantity) {
            throw new AppError(
              400,
              'POS_RETURN_FREE_ITEM_PARTIAL',
              'Los artículos libres solo pueden devolverse en su totalidad',
            );
          }
        } else if (line.returnedQuantity > remaining) {
          throw new AppError(
            409,
            'POS_RETURN_EXCEEDS_AVAILABLE',
            `Solo quedan ${remaining} unidades de la línea ${item.id}`,
          );
        }
        totalRefundCents +=
          (item.unit_price_cents - item.discount_cents + item.tax_cents) * line.returnedQuantity;
        if (!item.is_free_item && item.variant_id) {
          stockUpdates.push({
            variantId: item.variant_id,
            storeId: order.store_id,
            quantity: line.returnedQuantity,
          });
        }
      }

      // Lock every affected stock row.
      for (const stock of stockUpdates) {
        await client.query(
          `SELECT available FROM inventory_stock
            WHERE variant_id = $1 AND store_id = $2 FOR UPDATE`,
          [stock.variantId, stock.storeId],
        );
      }

      for (const stock of stockUpdates) {
        await client.query(
          `UPDATE inventory_stock
              SET available = available + $3,
                  sold = GREATEST(0, sold - $3),
                  updated_at = now()
            WHERE variant_id = $1 AND store_id = $2`,
          [stock.variantId, stock.storeId, stock.quantity],
        );
        await client.query(
          `INSERT INTO inventory_movements (variant_id, store_id, operation, quantity)
           VALUES ($1, $2, 'return', $3)`,
          [stock.variantId, stock.storeId, stock.quantity],
        );
      }

      for (const line of input.lines) {
        await client.query(
          `UPDATE orders_items
              SET returned_quantity = returned_quantity + $2
            WHERE id = $1`,
          [line.orderItemId, line.returnedQuantity],
        );
      }

      // Decide new state. A line is considered fully returned when
      // `returned_quantity == quantity`; free items are treated the same way.
      const updatedItemsResult = await client.query<{
        quantity: number;
        returned_quantity: number;
        is_free_item: boolean;
      }>(
        `SELECT quantity, returned_quantity, is_free_item
           FROM orders_items WHERE order_id = $1`,
        [input.orderId],
      );
      let allFullyReturned = true;
      let anyReturn = false;
      for (const row of updatedItemsResult.rows) {
        if (row.returned_quantity === 0) {
          allFullyReturned = false;
        } else {
          anyReturn = true;
          if (row.returned_quantity < row.quantity) {
            allFullyReturned = false;
          }
        }
      }
      if (!anyReturn) {
        // Nothing changed (defensive).
        allFullyReturned = false;
      }
      const reportingStatus: 'refund' | 'partial_refund' = allFullyReturned
        ? 'refund'
        : 'partial_refund';
      const newOrderState: 'REFUNDED' | 'PARTIALLY_REFUNDED' = allFullyReturned
        ? 'REFUNDED'
        : 'PARTIALLY_REFUNDED';

      // Cash portion of the refund reduces expected_cash_cents proportional to
      // the share of the original cash payment.
      const cashResult = await client.query<{ sum: string | null }>(
        `SELECT COALESCE(SUM(amount_cents), 0)::text AS sum FROM payments_transactions
          WHERE order_id = $1 AND status = 'succeeded' AND provider = 'pos_cash'`,
        [input.orderId],
      );
      const cashPaidCents = Number(cashResult.rows[0]?.sum ?? '0');
      const totalCents = Number(order.total_cents);
      const cashRefundPortion =
        totalCents > 0
          ? Math.min(
              Math.round((cashPaidCents / totalCents) * totalRefundCents),
              cashPaidCents,
            )
          : 0;
      if (cashRefundPortion > 0) {
        await client.query(
          `UPDATE pos_cash_sessions
              SET expected_cash_cents = GREATEST(0, COALESCE(expected_cash_cents, opening_cash_cents) - $1),
                  updated_at = now()
            WHERE id = $2`,
          [cashRefundPortion, input.cashSessionId],
        );
      }

      const insertedReturn = await client.query<{ id: string }>(
        `INSERT INTO pos_returns (
           order_id, idempotency_key, terminal_id, cash_session_id, store_id, user_id,
           total_cents, status, reason
         ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
         RETURNING id`,
        [
          input.orderId,
          input.idempotencyKey,
          input.terminalId,
          input.cashSessionId,
          order.store_id,
          input.userId,
          totalRefundCents,
          reportingStatus,
          input.reason,
        ],
      );
      const returnId = insertedReturn.rows[0]?.id;
      if (!returnId) throw new AppError(500, 'POS_RETURN_INSERT_FAILED', 'No se pudo registrar la devolución');

      await client.query(
        `INSERT INTO reporting_payment_lines (
           order_id, store_id, terminal_id, cash_session_id, payment_method_id,
           provider, amount_cents, currency, status, provider_ref
         ) VALUES ($1, $2, $3, $4, NULL, 'pos_return', $5, 'EUR', $6, $7)`,
        [
          input.orderId,
          order.store_id,
          input.terminalId,
          input.cashSessionId,
          totalRefundCents,
          reportingStatus,
          returnId,
        ],
      );
      await client.query(
        `INSERT INTO orders_order_history (order_id, event_type, message, actor_email, metadata)
         VALUES ($1, 'RETURNED', $2, (SELECT email FROM backoffice_users WHERE id = $3), $4)`,
        [
          input.orderId,
          formatReturnMessage({
            refundedCents: totalRefundCents,
            status: reportingStatus,
            lines: input.lines,
            reason: input.reason,
            items: itemsById,
          }),
          input.userId,
          JSON.stringify({
            returnId,
            refundedCents: totalRefundCents,
            status: reportingStatus,
            lines: input.lines,
            reason: input.reason,
          }),
        ],
      );

      await client.query(
        `UPDATE orders_orders SET state = $2, state_changed_at = now(), updated_at = now()
          WHERE id = $1`,
        [input.orderId, newOrderState],
      );

      await client.query(
        `INSERT INTO security_audit_log (actor_id, action, target, metadata)
         VALUES ($1, $2, $3, jsonb_build_object('refundedCents', $4::int, 'returnId', $5::text))`,
        [
          input.userId,
          newOrderState === 'REFUNDED' ? 'pos.sale.returned' : 'pos.sale.partial_returned',
          input.orderId,
          totalRefundCents,
          returnId,
        ],
      );

      const receipt = await buildPosReturnReceipt(client, input.orderId, totalRefundCents);
      await client.query('COMMIT');
      return {
        orderId: input.orderId,
        returnId,
        idempotencyKey: input.idempotencyKey,
        state: newOrderState,
        refundedCents: totalRefundCents,
        receipt,
      };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
}

interface FormatReturnMessageInput {
  refundedCents: number;
  status: 'refund' | 'partial_refund';
  lines: ReadonlyArray<{ orderItemId: string; returnedQuantity: number }>;
  reason: string;
  items: ReadonlyMap<string, OrderItemRow>;
}

/**
 * Renders a refund event as a short, human-readable line for the order
 * timeline. We deliberately do NOT dump the raw payload here: customers and
 * back-office staff see `event.message` and the JSON was unreadable.
 * Structured fields are still stored on `orders_order_history.metadata` so
 * support tooling can read them when needed.
 */
function formatReturnMessage(input: FormatReturnMessageInput): string {
  const euros = (input.refundedCents / 100).toFixed(2);
  const action = input.status === 'refund' ? 'Devolución total' : 'Devolución parcial';
  const lineDescriptions = input.lines
    .filter((line) => line.returnedQuantity > 0)
    .map((line) => {
      const item = input.items.get(line.orderItemId);
      const name = item?.name ?? 'Artículo';
      return `${line.returnedQuantity} × ${name}`;
    });
  const linesSummary =
    lineDescriptions.length === 0
      ? ''
      : ` — ${lineDescriptions.slice(0, 3).join(', ')}${
          lineDescriptions.length > 3 ? ` y ${lineDescriptions.length - 3} más` : ''
        }`;
  const reason = input.reason?.trim();
  return `${action} de ${euros} €${linesSummary}${reason ? ` (motivo: ${reason})` : ''}`;
}
