/**
 * Payment-records service. The CRM does NOT generate invoices - banks invoice
 * clients directly. We record that money was received (or refunded) with an
 * optional uploaded receipt for audit purposes.
 *
 * Auto-advance: when the running deposit total (SUM where payment_type='deposit'
 * minus SUM of refunds) reaches `interests.depositExpectedAmount`, the pipeline
 * stage moves to 'deposit_paid' (no-op if already past).
 */

import { and, asc, desc, eq, sql } from 'drizzle-orm';

import { db } from '@/lib/db';
import { berths, interests, payments } from '@/lib/db/schema';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import type { CreatePaymentInput, UpdatePaymentInput } from '@/lib/validators/payments';

/**
 * Normalize a payment amount's sign by type (audit H12). Refunds are stored
 * as a negative magnitude so every aggregate that sums payments nets them out
 * regardless of what sign the rep entered; all other types pass through
 * verbatim. Preserves the numeric-string column format.
 */
function normalizeRefundSign(paymentType: string, amount: string): string {
  if (paymentType !== 'refund') return amount;
  const n = Number(amount);
  if (!Number.isFinite(n)) return amount;
  return (-Math.abs(n)).toString();
}

// ─── Reads ──────────────────────────────────────────────────────────────────

/** All payments for a single interest, newest received first. */
export async function listPaymentsForInterest(interestId: string, portId: string) {
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
    columns: { id: true },
  });
  if (!interest) throw new NotFoundError('Interest');

  return db
    .select()
    .from(payments)
    .where(and(eq(payments.interestId, interestId), eq(payments.portId, portId)))
    .orderBy(desc(payments.receivedAt), asc(payments.id));
}

/** Net deposit total for an interest. `deposit` rows add; `refund` rows
 *  subtract (their `amount` may be either positive or already negative - we
 *  always treat refunds as deductions to match the UI convention). */
export async function getDepositTotalForInterest(
  interestId: string,
  portId: string,
  targetCurrency?: string,
): Promise<{ total: string; currency: string }> {
  const rows = await db
    .select({
      paymentType: payments.paymentType,
      amount: payments.amount,
      currency: payments.currency,
    })
    .from(payments)
    .where(
      and(
        eq(payments.interestId, interestId),
        eq(payments.portId, portId),
        sql`${payments.paymentType} IN ('deposit', 'refund')`,
        // Currency-consistency (audit C1): when a target currency is supplied
        // (the interest's depositExpectedCurrency) only matching-currency rows
        // count, so the auto-advance gate can never satisfy a EUR expectation
        // with a USD payment. Without a target, behaviour is unchanged for the
        // display-only callers.
        targetCurrency ? eq(payments.currency, targetCurrency) : undefined,
      ),
    );

  // Use BigInt-ish accumulator via Number - amounts are EUR scale; we don't
  // need cent-precise math for the auto-advance gate, but we DO normalize the
  // sign of refunds so a refund stored as "+200" still subtracts.
  let net = 0;
  let currency = targetCurrency ?? 'EUR';
  for (const row of rows) {
    const n = Number(row.amount);
    if (!Number.isFinite(n)) continue;
    currency = row.currency;
    net += row.paymentType === 'refund' ? -Math.abs(n) : n;
  }
  return { total: net.toFixed(2), currency };
}

// ─── Writes ─────────────────────────────────────────────────────────────────

export async function createPayment(portId: string, data: CreatePaymentInput, meta: AuditMeta) {
  // Resolve interest + sanity-check it belongs to this port.
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, data.interestId), eq(interests.portId, portId)),
    columns: {
      id: true,
      clientId: true,
      depositExpectedAmount: true,
      depositExpectedCurrency: true,
      pipelineStage: true,
      // L24: needed to tell whether the deposit gate had previously fired
      // (so a later refund can recompute it and re-lock if net drops below
      // expected).
      dateDepositReceived: true,
    },
  });
  if (!interest) throw new NotFoundError('Interest');

  const amountNum = Number(data.amount);
  if (!Number.isFinite(amountNum) || amountNum === 0) {
    throw new ValidationError('amount must be a non-zero numeric value');
  }

  // Sign convention (audit H12): refunds are always stored as a NEGATIVE
  // magnitude so every summation path nets them out consistently. The regex
  // validator accepts either sign, so normalize here regardless of what the
  // rep typed (a positive "200" refund becomes "-200").
  const normalizedAmount = normalizeRefundSign(data.paymentType, data.amount);

  const [row] = await db
    .insert(payments)
    .values({
      portId,
      interestId: data.interestId,
      clientId: interest.clientId,
      paymentType: data.paymentType,
      amount: normalizedAmount,
      currency: data.currency,
      receivedAt: new Date(data.receivedAt),
      receiptFileId: data.receiptFileId ?? null,
      notes: data.notes ?? null,
      recordedBy: meta.userId,
    })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'payment',
    entityId: row!.id,
    newValue: {
      interestId: data.interestId,
      paymentType: data.paymentType,
      amount: normalizedAmount,
      currency: data.currency,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'payment:created', {
    paymentId: row!.id,
    interestId: data.interestId,
    paymentType: data.paymentType,
  });

  // Auto-advance: when the running deposit total reaches the expected amount,
  // promote the stage to 'deposit_paid'. Dynamic import keeps the
  // payments ↔ interests cycle one-way at module-load time.
  if (data.paymentType === 'deposit' || data.paymentType === 'refund') {
    // Gate on the deposit total expressed in the interest's expected currency
    // only — a wrong-currency payment must not satisfy the expectation (C1).
    const expectedCurrency = interest.depositExpectedCurrency ?? 'EUR';
    const { total } = await getDepositTotalForInterest(data.interestId, portId, expectedCurrency);
    const expected = interest.depositExpectedAmount ? Number(interest.depositExpectedAmount) : null;
    const gateMet = expected !== null && Number.isFinite(expected) && Number(total) >= expected;

    if (gateMet) {
      const { advanceStageIfBehindGated } = await import('@/lib/services/interests.service');
      void advanceStageIfBehindGated(
        data.interestId,
        portId,
        'deposit_paid',
        meta,
        `Deposit total (${total} ${expectedCurrency}) reached expected amount`,
        'deposit_received',
      );

      // Stamp dateDepositReceived if not already set so the timeline shows
      // when the threshold was met (not the date of the first payment row).
      await db
        .update(interests)
        .set({ dateDepositReceived: new Date(), updatedAt: new Date() })
        .where(eq(interests.id, data.interestId));

      // Berth rule fires via the same hook the legacy invoices.ts path uses.
      const { evaluateRule } = await import('@/lib/services/berth-rules-engine');
      void evaluateRule('deposit_received', data.interestId, portId, meta);
    } else if (
      data.paymentType === 'refund' &&
      expected !== null &&
      Number.isFinite(expected) &&
      interest.dateDepositReceived !== null
    ) {
      // L24 — lower-bound re-lock. A prior deposit tripped the gate (stamping
      // dateDepositReceived and, via the deposit_received rule, flipping the
      // primary berth to 'sold'). This refund just dropped the net deposit
      // (in the expected currency) BACK below the expected amount, so the
      // "deposit met" state is no longer true. Reverse the deposit-driven
      // side effects conservatively:
      //
      //   1. Un-stamp dateDepositReceived — the timeline/reports must stop
      //      claiming the deposit was met.
      //   2. Revert the primary berth 'sold' → 'under_offer', but ONLY when
      //      it is currently 'sold' AND that status was set automatically by
      //      the rules engine (statusOverrideMode='automated'). We never undo
      //      a 'sold' an admin set by hand, and we never touch a berth a
      //      different deal/contract subsequently moved on.
      //
      // We deliberately do NOT auto-regress the pipeline stage here: stage is
      // monotonic-forward by design (advanceStageIfBehind never moves a deal
      // backwards) and a contract may already have been signed off this deal.
      // Reversing the stage is left to a rep via the manual /stage path. The
      // un-stamp + berth re-lock are the conservative, non-destructive subset
      // that stops the underpaid deposit from reading as fully met.
      await db
        .update(interests)
        .set({ dateDepositReceived: null, updatedAt: new Date() })
        .where(eq(interests.id, data.interestId));

      const { getPrimaryBerth } = await import('@/lib/services/interest-berths.service');
      const primaryBerth = await getPrimaryBerth(data.interestId);
      if (primaryBerth?.berthId) {
        const [reverted] = await db
          .update(berths)
          .set({
            status: 'under_offer',
            statusLastChangedBy: meta.userId,
            statusLastChangedReason: `Deposit refunded — net (${total} ${expectedCurrency}) fell below expected; auto-reverted from sold`,
            statusLastModified: new Date(),
            statusOverrideMode: 'automated',
            updatedAt: new Date(),
          })
          .where(
            and(
              eq(berths.id, primaryBerth.berthId),
              eq(berths.portId, portId),
              eq(berths.status, 'sold'),
              eq(berths.statusOverrideMode, 'automated'),
            ),
          )
          .returning({ id: berths.id });

        if (reverted) {
          void createAuditLog({
            userId: meta.userId,
            portId,
            action: 'update',
            entityType: 'berth',
            entityId: reverted.id,
            oldValue: { status: 'sold' },
            newValue: { status: 'under_offer' },
            metadata: { type: 'deposit_refund_relock', interestId: data.interestId },
            ipAddress: meta.ipAddress,
            userAgent: meta.userAgent,
          });
          emitToRoom(`port:${portId}`, 'berth:statusChanged', {
            berthId: reverted.id,
            newStatus: 'under_offer',
            triggeredBy: meta.userId,
            trigger: 'deposit_refund_relock',
          });
        }
      }
    }
  }

  return row!;
}

export async function updatePayment(
  id: string,
  portId: string,
  data: UpdatePaymentInput,
  meta: AuditMeta,
) {
  const existing = await db.query.payments.findFirst({
    where: and(eq(payments.id, id), eq(payments.portId, portId)),
  });
  if (!existing) throw new NotFoundError('Payment');

  const next: Record<string, unknown> = {};
  if (data.paymentType !== undefined) next.paymentType = data.paymentType;
  if (data.amount !== undefined) next.amount = data.amount;
  if (data.currency !== undefined) next.currency = data.currency;
  if (data.receivedAt !== undefined) next.receivedAt = new Date(data.receivedAt);
  if (data.receiptFileId !== undefined) next.receiptFileId = data.receiptFileId;
  if (data.notes !== undefined) next.notes = data.notes;

  // Re-apply the refund sign convention (audit H12) whenever the effective
  // type or amount changes. The effective type is the incoming one if present,
  // otherwise the row's existing type — so flipping a row to/from 'refund'
  // re-signs the stored amount even when the amount field itself is untouched.
  if (data.paymentType !== undefined || data.amount !== undefined) {
    const effectiveType = data.paymentType ?? existing.paymentType;
    const effectiveAmount = data.amount ?? existing.amount;
    next.amount = normalizeRefundSign(effectiveType, effectiveAmount);
  }

  const [updated] = await db
    .update(payments)
    .set(next)
    .where(and(eq(payments.id, id), eq(payments.portId, portId)))
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'payment',
    entityId: id,
    oldValue: {
      paymentType: existing.paymentType,
      amount: existing.amount,
      currency: existing.currency,
    },
    newValue: next,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return updated!;
}

export async function deletePayment(id: string, portId: string, meta: AuditMeta) {
  const existing = await db.query.payments.findFirst({
    where: and(eq(payments.id, id), eq(payments.portId, portId)),
  });
  if (!existing) throw new NotFoundError('Payment');

  await db.delete(payments).where(and(eq(payments.id, id), eq(payments.portId, portId)));

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'delete',
    entityType: 'payment',
    entityId: id,
    oldValue: {
      paymentType: existing.paymentType,
      amount: existing.amount,
      currency: existing.currency,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return { ok: true };
}
