import { eq, and, gte, lte, sql } from 'drizzle-orm';
import type { PgColumn } from 'drizzle-orm/pg-core';

import { db } from '@/lib/db';
import { expenses, invoices, invoiceExpenses } from '@/lib/db/schema/financial';
import { buildListQuery } from '@/lib/db/query-builder';
import { createAuditLog } from '@/lib/audit';
import { diffEntity } from '@/lib/entity-diff';
import { softDelete, restore } from '@/lib/db/utils';
import { NotFoundError, ConflictError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import { convert } from '@/lib/services/currency';
import { logger } from '@/lib/logger';
import type {
  CreateExpenseInput,
  UpdateExpenseInput,
  ListExpensesInput,
} from '@/lib/validators/expenses';

export type { ListExpensesInput };

// AuditMeta type expected by service functions
export interface ServiceAuditMeta {
  userId: string;
  portId: string;
  ipAddress: string;
  userAgent: string;
}

export async function listExpenses(portId: string, query: ListExpensesInput) {
  const filters = [];

  if (query.category) {
    filters.push(eq(expenses.category, query.category));
  }
  if (query.paymentStatus) {
    filters.push(eq(expenses.paymentStatus, query.paymentStatus));
  }
  if (query.currency) {
    filters.push(eq(expenses.currency, query.currency));
  }
  if (query.payer) {
    filters.push(eq(expenses.payer, query.payer));
  }
  if (query.dateFrom) {
    filters.push(gte(expenses.expenseDate, new Date(query.dateFrom)));
  }
  if (query.dateTo) {
    filters.push(lte(expenses.expenseDate, new Date(query.dateTo)));
  }

  return buildListQuery({
    table: expenses,
    portIdColumn: expenses.portId,
    portId,
    idColumn: expenses.id,
    updatedAtColumn: expenses.updatedAt,
    filters,
    page: query.page,
    pageSize: query.limit,
    searchColumns: [expenses.establishmentName, expenses.description],
    searchTerm: query.search,
    includeArchived: query.includeArchived,
    archivedAtColumn: expenses.archivedAt,
    sort: query.sort
      ? {
          column: expenses[query.sort as keyof typeof expenses] as unknown as PgColumn,
          direction: query.order,
        }
      : undefined,
  });
}

export async function getExpenseById(id: string, portId: string) {
  const expense = await db.query.expenses.findFirst({
    where: and(eq(expenses.id, id), eq(expenses.portId, portId)),
  });
  if (!expense) throw new NotFoundError('Expense');
  return expense;
}

export async function createExpense(
  portId: string,
  data: CreateExpenseInput,
  meta: ServiceAuditMeta,
) {
  let amountUsd: string | null = null;
  let exchangeRate: string | null = null;

  if (data.currency !== 'USD') {
    const conversion = await convert(data.amount, data.currency, 'USD');
    if (conversion) {
      amountUsd = String(conversion.result);
      exchangeRate = String(conversion.rate);
    } else {
      // BR-040: if rate unavailable, save without conversion + log warning
      logger.warn(
        { currency: data.currency },
        'Currency rate unavailable, saving expense without USD conversion',
      );
    }
  } else {
    amountUsd = String(data.amount);
    exchangeRate = '1';
  }

  const [expense] = await db
    .insert(expenses)
    .values({
      portId,
      establishmentName: data.establishmentName,
      amount: String(data.amount),
      currency: data.currency,
      amountUsd,
      exchangeRate,
      paymentMethod: data.paymentMethod,
      category: data.category,
      payer: data.payer,
      expenseDate: data.expenseDate,
      description: data.description,
      receiptFileIds: data.receiptFileIds ?? [],
      paymentStatus: data.paymentStatus,
      paymentDate: data.paymentDate ?? null,
      paymentReference: data.paymentReference ?? null,
      paymentNotes: data.paymentNotes ?? null,
      createdBy: meta.userId,
    })
    .returning();

  if (!expense) throw new Error('Insert failed');

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'expense',
    entityId: expense.id,
    newValue: expense as unknown as Record<string, unknown>,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'expense:created', {
    expenseId: expense.id,
    amount: Number(expense.amount),
    currency: expense.currency,
    category: expense.category ?? '',
  });

  // Schedule a duplicate-detection sweep. Best-effort — we don't want a
  // queue-side hiccup to fail the user's create.
  try {
    const { getQueue } = await import('@/lib/queue');
    await getQueue('maintenance').add('expense-dedup-scan', { expenseId: expense.id });
  } catch (err) {
    logger.warn({ err, expenseId: expense.id }, 'Failed to enqueue expense-dedup-scan');
  }

  return expense;
}

export async function updateExpense(
  id: string,
  portId: string,
  data: UpdateExpenseInput,
  meta: ServiceAuditMeta,
) {
  const existing = await getExpenseById(id, portId);

  const updateData: Record<string, unknown> = { ...data, updatedAt: new Date() };

  // Re-convert to USD if amount or currency changed
  const newAmount = data.amount ?? Number(existing.amount);
  const newCurrency = data.currency ?? existing.currency;

  if (data.amount !== undefined || data.currency !== undefined) {
    if (newCurrency !== 'USD') {
      const conversion = await convert(newAmount, newCurrency, 'USD');
      if (conversion) {
        updateData.amountUsd = String(conversion.result);
        updateData.exchangeRate = String(conversion.rate);
      } else {
        logger.warn(
          { currency: newCurrency },
          'Currency rate unavailable during update, clearing USD conversion',
        );
        updateData.amountUsd = null;
        updateData.exchangeRate = null;
      }
    } else {
      updateData.amountUsd = String(newAmount);
      updateData.exchangeRate = '1';
    }
  }

  if (data.amount !== undefined) updateData.amount = String(data.amount);

  const { diff } = diffEntity(existing as unknown as Record<string, unknown>, updateData);

  const [updated] = await db
    .update(expenses)
    .set(updateData as Record<string, unknown>)
    .where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
    .returning();

  if (!updated) throw new NotFoundError('Expense');

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'expense',
    entityId: id,
    oldValue: existing as unknown as Record<string, unknown>,
    newValue: updated as unknown as Record<string, unknown>,
    metadata: { diff },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'expense:updated', {
    expenseId: id,
    changedFields: Object.keys(diff),
  });

  return updated;
}

export async function archiveExpense(id: string, portId: string, meta: ServiceAuditMeta) {
  const existing = await getExpenseById(id, portId);

  // BR-045: Check if linked to non-draft invoice
  const linkedInvoice = await db
    .select({ invoiceId: invoiceExpenses.invoiceId })
    .from(invoiceExpenses)
    .innerJoin(invoices, eq(invoices.id, invoiceExpenses.invoiceId))
    .where(and(eq(invoiceExpenses.expenseId, id), sql`${invoices.status} != 'draft'`))
    .limit(1);

  if (linkedInvoice.length > 0) {
    throw new ConflictError('Cannot archive expense linked to a non-draft invoice');
  }

  await softDelete(expenses, expenses.id, id);

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'archive',
    entityType: 'expense',
    entityId: id,
    oldValue: existing as unknown as Record<string, unknown>,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'expense:archived', { expenseId: id });
}

export async function restoreExpense(id: string, portId: string, meta: ServiceAuditMeta) {
  await getExpenseById(id, portId);

  await restore(expenses, expenses.id, id);

  const restored = await getExpenseById(id, portId);

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'restore',
    entityType: 'expense',
    entityId: id,
    newValue: restored as unknown as Record<string, unknown>,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'expense:updated', {
    expenseId: id,
    changedFields: ['archivedAt'],
  });

  return restored;
}

export async function addReceiptFiles(
  id: string,
  portId: string,
  fileIds: string[],
  meta: ServiceAuditMeta,
) {
  await getExpenseById(id, portId);

  const [updated] = await db
    .update(expenses)
    .set({
      receiptFileIds: sql`array_cat(receipt_file_ids, ${fileIds}::text[])`,
      updatedAt: new Date(),
    } as Record<string, unknown>)
    .where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
    .returning();

  if (!updated) throw new NotFoundError('Expense');

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'expense',
    entityId: id,
    metadata: { addedFileIds: fileIds },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return updated;
}
