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

interface SessionRow {
  id: string;
  terminal_id: string;
  store_id: string;
  status: string;
  terminal_settings: Record<string, unknown>;
}

interface CatalogLineRow {
  variant_id: string;
  product_id: string;
  sku: string;
  ean: string | null;
  name: string;
  net_unit_amount_cents: number;
  offer_cents: number | null;
  vat_rate: 'general' | 'reduced' | 'super-reduced';
}

const POS_VAT_BASIS_POINTS: Record<CatalogLineRow['vat_rate'], number> = {
  general: 2100,
  reduced: 1000,
  'super-reduced': 400,
};

export interface ConfiguredPaymentMethod {
  id: string;
  code: string;
  label: string;
  kind: PosPaymentKind;
}

export interface ValidatedPayment {
  methodId: string;
  methodCode: string;
  methodLabel: string;
  kind: PosPaymentKind;
  amountCents: number;
  tenderedCents: number | null;
  changeCents: number;
  last4?: string;
}

export function validatePaymentAllocations(
  totalCents: number,
  inputs: PosPaymentInput[],
  methods: ConfiguredPaymentMethod[],
): ValidatedPayment[] {
  // Zero payments are allowed — creates a parked/pending sale with full outstanding balance.
  if (inputs.length === 0) {
    return [];
  }
  const byCode = new Map(methods.map((method) => [method.code, method]));
  const validated = inputs.map((input) => {
    const methodCode = input.methodCode ?? input.kind;
    const method = methodCode ? byCode.get(methodCode) : undefined;
    if (!method) {
      throw new AppError(400, 'POS_PAYMENT_METHOD_DISABLED', 'La forma de pago no está habilitada');
    }
    if (!Number.isInteger(input.amountCents) || input.amountCents <= 0) {
      throw new AppError(
        400,
        'POS_PAYMENT_INVALID_AMOUNT',
        'El importe del pago debe ser positivo',
      );
    }
    if (method.kind === 'cash') {
      const tenderedCents = input.tenderedCents ?? input.amountCents;
      if (!Number.isInteger(tenderedCents) || tenderedCents < input.amountCents) {
        throw new AppError(
          400,
          'POS_CASH_TENDERED_TOO_LOW',
          'El efectivo entregado no cubre el importe aplicado',
        );
      }
      return {
        methodId: method.id,
        methodCode: method.code,
        methodLabel: method.label,
        kind: method.kind,
        amountCents: input.amountCents,
        tenderedCents,
        changeCents: tenderedCents - input.amountCents,
        ...(input.last4 ? { last4: input.last4 } : {}),
      };
    }
    if (input.tenderedCents !== undefined) {
      throw new AppError(400, 'POS_NON_CASH_TENDERED', 'Solo el efectivo admite importe entregado');
    }
    return {
      methodId: method.id,
      methodCode: method.code,
      methodLabel: method.label,
      kind: method.kind,
      amountCents: input.amountCents,
      tenderedCents: null,
      changeCents: 0,
      ...(input.last4 ? { last4: input.last4 } : {}),
    };
  });
  const allocatedCents = validated.reduce((sum, payment) => sum + payment.amountCents, 0);
  if (allocatedCents > totalCents) {
    throw new AppError(
      400,
      'POS_PAYMENT_OVERPAYMENT',
      'Los pagos asignados superan el total de la venta',
    );
  }
  return validated;
}

function grossFromNet(netUnitAmountCents: number, vatRate: CatalogLineRow['vat_rate']): number {
  return Math.round(netUnitAmountCents * (1 + POS_VAT_BASIS_POINTS[vatRate] / 10_000));
}

function netFromGross(grossUnitAmountCents: number, vatRate: CatalogLineRow['vat_rate']): number {
  return Math.round(grossUnitAmountCents / (1 + POS_VAT_BASIS_POINTS[vatRate] / 10_000));
}

function effectiveGrossUnitPrice(catalog: CatalogLineRow): number {
  return catalog.offer_cents ?? grossFromNet(Number(catalog.net_unit_amount_cents), catalog.vat_rate);
}

export class CreatePosSaleUseCase {
  constructor(private readonly pool: pg.Pool) {}

  async execute(input: PosSaleInput): Promise<PosSaleResult> {
    const client = await this.pool.connect();
    let clubEarnedCents = 0;
    let clubMemberCode: string | undefined;
    try {
      await client.query('BEGIN');

      const existing = await client.query<{ id: string }>(
        `SELECT id FROM orders_orders WHERE idempotency_key = $1 AND source = 'pos'`,
        [input.idempotencyKey],
      );
      if (existing.rows[0]) {
        const orderId = existing.rows[0].id;
        await client.query('ROLLBACK');
        return this.loadResult(orderId, input.idempotencyKey);
      }

      const sessionResult = await client.query<SessionRow>(
        `SELECT session.id, session.terminal_id, session.store_id, session.status,
                terminal.settings AS terminal_settings
           FROM pos_cash_sessions session
           JOIN pos_terminals terminal ON terminal.id = session.terminal_id
          WHERE session.id = $1
          FOR UPDATE OF session, terminal`,
        [input.cashSessionId],
      );
      const session = sessionResult.rows[0];
      if (!session)
        throw new AppError(404, 'SESSION_NOT_FOUND', 'No se encontró la sesión de caja');
      if (session.status !== 'OPEN')
        throw new AppError(409, 'SESSION_CLOSED', 'La sesión de caja está cerrada');
      if (session.terminal_id !== input.terminalId) {
        throw new AppError(409, 'TERMINAL_SESSION_MISMATCH', 'La sesión pertenece a otro terminal');
      }
      if (input.items.length === 0)
        throw new AppError(400, 'POS_EMPTY_CART', 'El carrito está vacío');

      const lineDiscountsEnabled = session.terminal_settings?.lineDiscountsEnabled !== false;
      let subtotalCents = 0;
      let discountCents = 0;
      let taxCents = 0;
      const items: PosSaleLineItem[] = [];
      for (const inputItem of input.items) {
        if (inputItem.kind === 'free') {
          const name = inputItem.name.trim();
          if (
            !name ||
            !Number.isInteger(inputItem.unitPriceCents) ||
            inputItem.unitPriceCents <= 0
          ) {
            throw new AppError(
              400,
              'POS_INVALID_FREE_ITEM',
              'El artículo libre requiere nombre y precio positivo',
            );
          }
          subtotalCents += inputItem.unitPriceCents * inputItem.quantity;
          items.push({
            kind: 'free',
            variantId: null,
            productId: null,
            sku: 'LIBRE',
            ean: null,
            name,
            unitPriceCents: inputItem.unitPriceCents,
            discountCents: 0,
            taxCents: 0,
            quantity: inputItem.quantity,
            vatRate: null,
          });
          continue;
        }

        const catalogResult = await client.query<CatalogLineRow>(
          `SELECT variant.id AS variant_id, variant.product_id, variant.sku, variant.ean,
                  product.name,
                  price.net_unit_amount_cents,
                  price.offer_cents,
                  price.vat_rate
             FROM catalog_product_variants variant
             JOIN catalog_products product ON product.id = variant.product_id
             JOIN pricing_variant_prices price ON price.variant_id = variant.id AND price.currency = 'EUR'
            WHERE variant.id = $1 AND product.state = 'active'`,
          [inputItem.variantId],
        );
        const catalog = catalogResult.rows[0];
        if (!catalog)
          throw new AppError(404, 'POS_PRODUCT_NOT_FOUND', 'El producto ya no está disponible');
        const grossUnitPriceCents = effectiveGrossUnitPrice(catalog);
        const discountGrossCents = inputItem.discountCents ?? 0;
        if (
          !Number.isInteger(discountGrossCents) ||
          discountGrossCents < 0 ||
          discountGrossCents > grossUnitPriceCents
        ) {
          throw new AppError(400, 'POS_INVALID_DISCOUNT', 'El descuento de línea no es válido');
        }
        if (!lineDiscountsEnabled && discountGrossCents > 0) {
          throw new AppError(
            403,
            'POS_DISCOUNTS_DISABLED',
            'Los descuentos están desactivados en este terminal',
          );
        }
        const stock = await client.query<{ available: number }>(
          `SELECT available FROM inventory_stock
            WHERE variant_id = $1 AND store_id = $2
            FOR UPDATE`,
          [catalog.variant_id, session.store_id],
        );
        if (Number(stock.rows[0]?.available ?? 0) < inputItem.quantity) {
          throw new AppError(
            409,
            'POS_STOCK_UNAVAILABLE',
            `Stock insuficiente para ${catalog.name}`,
          );
        }
        const netUnitPriceCents =
          catalog.offer_cents === null
            ? Number(catalog.net_unit_amount_cents)
            : netFromGross(grossUnitPriceCents, catalog.vat_rate);
        const discountedGrossUnitCents = grossUnitPriceCents - discountGrossCents;
        const discountedNetUnitCents = netFromGross(discountedGrossUnitCents, catalog.vat_rate);
        const discountNetCents = Math.max(0, netUnitPriceCents - discountedNetUnitCents);
        const taxUnitCents = discountedGrossUnitCents - discountedNetUnitCents;

        subtotalCents += grossUnitPriceCents * inputItem.quantity;
        discountCents += discountGrossCents * inputItem.quantity;
        taxCents += taxUnitCents * inputItem.quantity;
        items.push({
          kind: 'stock',
          variantId: catalog.variant_id,
          productId: catalog.product_id,
          sku: catalog.sku,
          ean: catalog.ean,
          name: catalog.name,
          unitPriceCents: netUnitPriceCents,
          discountCents: discountNetCents,
          taxCents: taxUnitCents,
          quantity: inputItem.quantity,
          vatRate: catalog.vat_rate,
        });
      }

      const totalCents = subtotalCents - discountCents;
      if (totalCents <= 0)
        throw new AppError(400, 'POS_INVALID_TOTAL', 'El total de la venta debe ser positivo');

      const methodsResult = await client.query<ConfiguredPaymentMethod>(
        `SELECT id, code, label, kind
           FROM pos_payment_methods
          WHERE store_id = $1 AND active = true
          ORDER BY sort_order, label`,
        [session.store_id],
      );
      const payments = validatePaymentAllocations(totalCents, input.payments, methodsResult.rows);

      await client.query(
        `INSERT INTO pos_receipt_settings (store_id) VALUES ($1)
         ON CONFLICT (store_id) DO NOTHING`,
        [session.store_id],
      );
      const receiptSettings = await client.query<{
        prefix: string;
        next_number: string;
        padding: number;
      }>(
        `SELECT prefix, next_number, padding FROM pos_receipt_settings WHERE store_id = $1 FOR UPDATE`,
        [session.store_id],
      );
      const sequence = receiptSettings.rows[0];
      if (!sequence)
        throw new AppError(500, 'POS_RECEIPT_CONFIG_MISSING', 'Falta la configuración de tickets');
      const receiptNumber = `${sequence.prefix}-${String(sequence.next_number).padStart(sequence.padding, '0')}`;
      await client.query(
        `UPDATE pos_receipt_settings SET next_number = next_number + 1, updated_at = now() WHERE store_id = $1`,
        [session.store_id],
      );

      const orderResult = await client.query<{ id: string; created_at: Date }>(
        `INSERT INTO orders_orders (
           user_id, idempotency_key, state, subtotal_cents, discount_cents, tax_cents,
           total_cents, source, terminal_id, cash_session_id, store_id, receipt_number, pos_label
         ) VALUES ($1, $2, 'PENDING', $3, $4, $5, $6, 'pos', $7, $8, $9, $10, $11)
         RETURNING id, created_at`,
        [
          input.customerId ?? null,
          input.idempotencyKey,
          subtotalCents,
          discountCents,
          taxCents,
          totalCents,
          input.terminalId,
          input.cashSessionId,
          session.store_id,
          receiptNumber,
          input.posLabel ?? null,
        ],
      );
      const order = orderResult.rows[0];
      if (!order) throw new AppError(500, 'ORDER_CREATE_FAILED', 'No se pudo crear la venta');

      for (const item of items) {
        await client.query(
          `INSERT INTO orders_items (
             order_id, product_id, variant_id, sku, ean, name, unit_price_cents,
             discount_cents, tax_cents, quantity, vat_rate, is_free_item
           ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
          [
            order.id,
            item.productId,
            item.variantId,
            item.sku,
            item.ean,
            item.name,
            item.unitPriceCents,
            item.discountCents,
            item.taxCents,
            item.quantity,
            item.vatRate,
            item.kind === 'free',
          ],
        );
        if (item.kind === 'stock') {
          await client.query(
            `UPDATE inventory_stock
                SET available = available - $3, sold = sold + $3, updated_at = now()
              WHERE variant_id = $1 AND store_id = $2`,
            [item.variantId, session.store_id, item.quantity],
          );
          await client.query(
            `INSERT INTO inventory_movements (variant_id, store_id, operation, quantity)
             VALUES ($1, $2, 'confirm', $3)`,
            [item.variantId, session.store_id, item.quantity],
          );
        }
      }

      const paymentResults: PosPaymentResult[] = [];
      let cashAppliedCents = 0;
      for (const [index, payment] of payments.entries()) {
        const raw = {
          methodCode: payment.methodCode,
          methodLabel: payment.methodLabel,
          kind: payment.kind,
          tenderedCents: payment.tenderedCents,
          changeCents: payment.changeCents,
          ...(payment.last4 ? { last4: payment.last4 } : {}),
        };
        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}`,
            `pos-${order.id}-${index}`,
            `pos-${order.id}-${payment.methodCode}-${index}`,
            order.id,
            payment.amountCents,
            JSON.stringify(raw),
          ],
        );
        const 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)`,
          [
            order.id,
            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 });
      }
      // Recompute initial paidCents from current payments only when this is a
      // first-time creation. Rest-payment use case has its own loader.
      const initialPaidCents = payments.reduce((sum, p) => sum + p.amountCents, 0);
      const orderState: 'PENDING' | 'COMPLETED' =
        initialPaidCents < totalCents ? 'PENDING' : 'COMPLETED';
      if (orderState === 'COMPLETED') {
        await client.query(
          `UPDATE orders_orders SET state = 'COMPLETED',
                                    state_changed_at = now(),
                                    updated_at = now()
            WHERE id = $1`,
          [order.id],
        );

        // ── Club cashback accumulation ────────────────────────────────────────────
        if (input.clubMemberId) {
          try {
            const settingsRow = await client.query<{ value: string }>(
              `SELECT value FROM store_settings WHERE key = 'club_enabled' LIMIT 1`,
            );
            if (settingsRow.rows[0]?.value === 'true') {
              const cashbackRow = await client.query<{ value: string }>(
                `SELECT value FROM store_settings WHERE key = 'club_cashback_bps' LIMIT 1`,
              );
              const cashbackBps = Number(cashbackRow.rows[0]?.value ?? '0');
              if (cashbackBps > 0) {
                clubEarnedCents = Math.round((totalCents * cashbackBps) / 10_000);
                if (clubEarnedCents > 0) {
                  const memberRow = await client.query<{ member_code: string }>(
                    `SELECT member_code FROM club_members WHERE id = $1 AND status = 'active' LIMIT 1`,
                    [input.clubMemberId],
                  );
                  clubMemberCode = memberRow.rows[0]?.member_code;
                  await client.query(
                    `INSERT INTO club_transactions (
                       member_id, sale_id, type, amount_cents, balance_delta_cents, idempotency_key, metadata
                     ) VALUES ($1, $2, 'earn', $3, $4, $5, $6::jsonb)
                     ON CONFLICT (idempotency_key) DO NOTHING`,
                    [
                      input.clubMemberId,
                      order.id,
                      totalCents,
                      clubEarnedCents,
                      `club-earn-${order.id}`,
                      JSON.stringify({ source: 'pos', orderState }),
                    ],
                  );
                }
              }
            }
          } catch {
            // Club cashback failure must not block the sale.
          }
        }
      }

      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 receipt = await buildPosReceipt(client, order.id);
      await client.query('COMMIT');
      return {
        orderId: order.id,
        idempotencyKey: input.idempotencyKey,
        receiptNumber,
        state: orderState,
        totalCents,
        paidCents: initialPaidCents,
        outstandingCents: totalCents - initialPaidCents,
        changeCents: payments.reduce((sum, payment) => sum + payment.changeCents, 0),
        items,
        payments: paymentResults,
        receipt,
        createdAt: order.created_at,
        clubEarnedCents,
        ...(clubMemberCode ? { clubMemberCode } : {}),
      };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  private async loadResult(orderId: string, idempotencyKey: string): Promise<PosSaleResult> {
    const receipt = await buildPosReceipt(this.pool, orderId);
    const orderRows = await this.pool.query<{ state: string }>(
      `SELECT state FROM orders_orders WHERE id = $1`,
      [orderId],
    );
    const orderState: 'PENDING' | 'COMPLETED' =
      orderRows.rows[0]?.state === 'COMPLETED' ? 'COMPLETED' : 'PENDING';
    const paymentRows = await this.pool.query<{
      id: string;
      amount_cents: number;
      raw: Record<string, unknown>;
    }>(
      `SELECT id, amount_cents, raw FROM payments_transactions
        WHERE order_id = $1 AND status = 'succeeded' ORDER BY created_at, id`,
      [orderId],
    );
    const payments: PosPaymentResult[] = paymentRows.rows.map((row, index) => ({
      id: row.id,
      ...(receipt.payments[index] ?? {
        methodCode: String(row.raw.methodCode ?? 'other'),
        methodLabel: String(row.raw.methodLabel ?? 'Otro'),
        kind: 'other' as const,
        amountCents: Number(row.amount_cents),
        tenderedCents: null,
        changeCents: 0,
      }),
    }));
    const items: PosSaleLineItem[] = receipt.items.map((item) => ({
      kind: item.freeItem ? 'free' : 'stock',
      variantId: null,
      productId: null,
      sku: item.sku,
      ean: null,
      name: item.name,
      unitPriceCents: item.unitPriceCents,
      discountCents: item.quantity > 0 ? Math.round(item.discountCents / item.quantity) : 0,
      taxCents: item.quantity > 0 ? Math.round(item.taxCents / item.quantity) : 0,
      quantity: item.quantity,
      vatRate: null,
    }));
    const paidCents = payments.reduce((sum, p) => sum + p.amountCents, 0);
    // Load club earned cashback from ledger if any.
    const clubTxRow = await this.pool.query<{ balance_delta_cents: number }>(
      `SELECT balance_delta_cents FROM club_transactions
         WHERE sale_id = $1 AND type = 'earn' AND idempotency_key = $2
         LIMIT 1`,
      [orderId, `club-earn-${orderId}`],
    );
    const clubEarnedCents = clubTxRow.rows[0]?.balance_delta_cents ?? 0;
    return {
      orderId,
      idempotencyKey,
      receiptNumber: receipt.receiptNumber,
      state: orderState,
      totalCents: receipt.totalCents,
      paidCents,
      outstandingCents: receipt.totalCents - paidCents,
      changeCents: receipt.changeCents,
      items,
      payments,
      receipt,
      createdAt: receipt.issuedAt,
      clubEarnedCents,
    };
  }
}
