/**
 * 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 { 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';

// ─── 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,
): 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')`,
      ),
    );

  // 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 = '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, pipelineStage: 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');
  }

  const [row] = await db
    .insert(payments)
    .values({
      portId,
      interestId: data.interestId,
      clientId: interest.clientId,
      paymentType: data.paymentType,
      amount: data.amount,
      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: data.amount,
      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') {
    const { total } = await getDepositTotalForInterest(data.interestId, portId);
    const expected = interest.depositExpectedAmount ? Number(interest.depositExpectedAmount) : null;
    if (expected !== null && Number.isFinite(expected) && Number(total) >= expected) {
      const { advanceStageIfBehindGated } = await import('@/lib/services/interests.service');
      void advanceStageIfBehindGated(
        data.interestId,
        portId,
        'deposit_paid',
        meta,
        `Deposit total (${total} ${data.currency}) 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);
    }
  }

  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;

  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 };
}
