import type pg from 'pg';
import { AppError } from '../../../shared/errors.js';
import type {
  PosPaymentInput,
  PosPaymentKind,
  PosPaymentResult,
  PosSaleResult,
} from '../domain/pos-sale.js';
import { validatePaymentAllocations } from './create-pos-sale.js';
import { buildPosReceipt } from './build-pos-receipt.js';

export interface ReceiveRestPaymentInput {
  orderId: string;
  idempotencyKey: string;
  cashSessionId: string;
  terminalId: string;
  userId: string;
  payments: PosPaymentInput[];
}

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

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

/**
 * Apply additional payments to a pending POS sale.
 *
 * Transitions the order to COMPLETED when the new cumulative allocation
 * equals the order total. Each payment immediately writes reporting
 * lines and accumulates expected cash. The order row is locked during the
 * transaction so concurrent rest-payment attempts cannot overpay.
 */
export class ReceiveRestPaymentUseCase {
  constructor(private readonly pool: pg.Pool) {}

  async execute(input: ReceiveRestPaymentInput): Promise<PosSaleResult> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');

      const orderRow = await client.query<OrderRow>(
        `SELECT id, state, source, total_cents, terminal_id, cash_session_id, store_id
           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') {
        throw new AppError(409, 'POS_SALE_NOT_PENDING', 'La venta ya está completamente cobrada');
      }
      if (order.terminal_id !== input.terminalId) {
        throw new AppError(
          409,
          'POS_SALE_TERMINAL_MISMATCH',
          'La venta pertenece a otro terminal',
        );
      }
      // F-191: orders from CLOSED sessions can be completed via a new OPEN session
      // (same terminal). Orders from OPEN sessions must use that same session.
      if (order.cash_session_id !== input.cashSessionId) {
        const origSession = await client.query<{ status: string }>(
          `SELECT status FROM pos_cash_sessions WHERE id = $1`,
          [order.cash_session_id],
        );
        if (origSession.rows[0]?.status !== 'CLOSED') {
          throw new AppError(
            409,
            'POS_SALE_SESSION_MISMATCH',
            'La venta pertenece a otra sesión de caja abierta',
          );
        }
      }
      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 totalCents = Number(order.total_cents);
      const paidBefore = 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'`,
        [input.orderId],
      );
      const paidCentsBefore = Number(paidBefore.rows[0]?.sum ?? '0');
      const outstandingBefore = totalCents - paidCentsBefore;
      if (outstandingBefore <= 0) {
        throw new AppError(409, 'POS_SALE_NOT_PENDING', 'La venta ya está completamente cobrada');
      }

      const methodsResult = await client.query<{
        id: string;
        code: string;
        label: string;
        kind: PosPaymentKind;
      }>(
        `SELECT id, code, label, kind
           FROM pos_payment_methods
          WHERE store_id = $1 AND active = true
          ORDER BY sort_order, label`,
        [session.store_id],
      );
      // Validate that the new payments do not overpay the outstanding balance.
      const validated = validatePaymentAllocations(
        outstandingBefore,
        input.payments,
        methodsResult.rows,
      );

      let cashAppliedCents = 0;
      const paymentResults: PosPaymentResult[] = [];
      for (const [index, payment] of validated.entries()) {
        // Unique per (orderId, idempotencyKey, payment index). Rest-payments are
        // idempotent for the same key so retries do not duplicate transactions.
        const providerEventId = `rest-${input.orderId}-${input.idempotencyKey}-${index}`;
        const existing = await client.query<{ id: string }>(
          `SELECT id FROM payments_transactions WHERE provider_event_id = $1`,
          [providerEventId],
        );
        let paymentId = existing.rows[0]?.id;
        if (!paymentId) {
          const raw = {
            methodCode: payment.methodCode,
            methodLabel: payment.methodLabel,
            kind: payment.kind,
            tenderedCents: payment.tenderedCents,
            changeCents: payment.changeCents,
            ...(payment.last4 ? { last4: payment.last4 } : {}),
            restPayment: true,
          };
          const inserted = await client.query<{ id: string }>(
            `INSERT INTO payments_transactions (
               provider, provider_event_id, provider_payment_id, order_id,
               amount_cents, currency, status, raw
             ) VALUES ($1, $2, $3, $4, $5, 'EUR', 'succeeded', $6)
             RETURNING id`,
            [
              `pos_${payment.methodCode}`,
              providerEventId,
              providerEventId,
              input.orderId,
              payment.amountCents,
              JSON.stringify(raw),
            ],
          );
          paymentId = inserted.rows[0]?.id;
          if (!paymentId)
            throw new AppError(500, 'PAYMENT_INSERT_FAILED', 'No se pudo registrar el pago');
          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, $5, $6, $7, 'EUR', 'payment', $8)`,
            [
              input.orderId,
              session.store_id,
              input.terminalId,
              input.cashSessionId,
              payment.methodId,
              `pos_${payment.methodCode}`,
              payment.amountCents,
              paymentId,
            ],
          );
        }
        if (payment.kind === 'cash') cashAppliedCents += payment.amountCents;
        paymentResults.push({ id: paymentId, ...payment });
      }

      if (cashAppliedCents > 0) {
        await client.query(
          `UPDATE pos_cash_sessions
              SET expected_cash_cents = COALESCE(expected_cash_cents, opening_cash_cents) + $1,
                  updated_at = now()
            WHERE id = $2`,
          [cashAppliedCents, input.cashSessionId],
        );
      }

      const paidAfterRow = 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'`,
        [input.orderId],
      );
      const paidCentsAfter = Number(paidAfterRow.rows[0]?.sum ?? '0');
      let finalState: 'PENDING' | 'COMPLETED' = 'PENDING';
      if (paidCentsAfter >= totalCents) {
        finalState = 'COMPLETED';
        await client.query(
          `UPDATE orders_orders
              SET state = 'COMPLETED', state_changed_at = now(), updated_at = now()
            WHERE id = $1`,
          [input.orderId],
        );
      }

      const receipt = await buildPosReceipt(client, input.orderId);
      await client.query(
        `INSERT INTO security_audit_log (actor_id, action, target, metadata)
         VALUES ($1, $2, $3, jsonb_build_object('paidCents', $4::int, 'totalCents', $5::int))`,
        [
          input.userId,
          finalState === 'COMPLETED' ? 'pos.sale.fulfilled' : 'pos.sale.partial',
          input.orderId,
          paidCentsAfter,
          totalCents,
        ],
      );
      await client.query('COMMIT');

      return {
        orderId: input.orderId,
        idempotencyKey: input.idempotencyKey,
        receiptNumber: receipt.receiptNumber,
        state: finalState,
        totalCents,
        paidCents: paidCentsAfter,
        outstandingCents: totalCents - paidCentsAfter,
        changeCents: paymentResults.reduce((sum, payment) => sum + payment.changeCents, 0),
        items: [],
        payments: paymentResults,
        receipt,
        createdAt: new Date(),
        clubEarnedCents: 0,
      };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
}
