import { and, desc, eq, gte, isNull, lte, sql } from 'drizzle-orm';

import { db } from '@/lib/db';
import { clients } from '@/lib/db/schema/clients';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { berths } from '@/lib/db/schema/berths';
import { payments } from '@/lib/db/schema/pipeline';
import { expenses } from '@/lib/db/schema/financial';
import { activeInterestsWhere } from '@/lib/services/active-interest';
import { resolvePortCurrency, normalizeAmount, CurrencyAccumulator } from './currency';
import {
  agingBucket,
  AGING_BUCKETS,
  monthKey,
  monthRange,
  netContribution,
  type AgingBucket,
} from './financial-math';

import type { DateRange } from './sales.service';

// ─── KPI strip ───────────────────────────────────────────────────────────────

export interface FinancialKpis {
  /** Σ of every payment received in range; refunds net out (their magnitude is
   *  always subtracted regardless of stored sign — audit H12). In port
   *  currency. */
  revenueCollected: number;
  depositsCollected: number;
  balanceCollected: number;
  /** Absolute value of refund-type payments issued in range. */
  refundsIssued: number;
  /** Σ expected deposit across active (open, non-archived) deals — the
   *  pipeline forecast in deposit terms. */
  pipelineExpected: number;
  /** Σ (expected − collected) across active deals, floored at 0 per deal.
   *  The payments-model analogue of outstanding AR. */
  expectedDepositsOutstanding: number;
  /** Σ expenses booked in range, normalised to port currency. */
  expensesTotal: number;
  /** revenueCollected − expensesTotal. */
  netContribution: number;
  currency: string;
}

/** Per-deal deposit position — expected vs collected. Internal; feeds the
 *  pipeline/outstanding KPIs, the aging chart, and the outstanding table. */
interface DepositPosition {
  interestId: string;
  clientName: string;
  mooring: string | null;
  expected: number;
  collected: number;
  remaining: number;
  currency: string;
  daysOutstanding: number;
}

/**
 * Expected-vs-collected deposit position for every active deal that has
 * an expected deposit set. Collected = Σ deposit-type payments for that
 * interest. Amounts normalised to port currency. Age measured from deal
 * creation (no invoice due dates exist in the payments model).
 */
async function getDepositPositions(portId: string): Promise<DepositPosition[]> {
  const targetCurrency = await resolvePortCurrency(portId);
  const now = Date.now();

  const rows = await db
    .select({
      interestId: interests.id,
      clientName: clients.fullName,
      mooring: berths.mooringNumber,
      expected: interests.depositExpectedAmount,
      expectedCurrency: interests.depositExpectedCurrency,
      createdAt: interests.createdAt,
    })
    .from(interests)
    .innerJoin(clients, eq(clients.id, interests.clientId))
    .leftJoin(
      interestBerths,
      and(eq(interestBerths.interestId, interests.id), eq(interestBerths.isPrimary, true)),
    )
    .leftJoin(berths, eq(interestBerths.berthId, berths.id))
    .where(and(activeInterestsWhere(portId), sql`${interests.depositExpectedAmount} IS NOT NULL`));

  // Deposits collected per interest (deposit-type payments only).
  const collectedRows = await db
    .select({
      interestId: payments.interestId,
      amount: payments.amount,
      currency: payments.currency,
    })
    .from(payments)
    .where(and(eq(payments.portId, portId), eq(payments.paymentType, 'deposit')));

  // Accumulate collected deposits per interest in their SOURCE currency, then
  // settle each interest's buckets to the target currency once (M19) — never
  // cents-rounding mid-accumulation.
  const collectedAccByInterest = new Map<string, CurrencyAccumulator>();
  for (const r of collectedRows) {
    let acc = collectedAccByInterest.get(r.interestId);
    if (!acc) {
      acc = new CurrencyAccumulator();
      collectedAccByInterest.set(r.interestId, acc);
    }
    acc.add(Number(r.amount ?? 0), r.currency ?? targetCurrency);
  }
  const collectedByInterest = new Map<string, number>();
  for (const [interestId, acc] of collectedAccByInterest) {
    const { total } = await acc.settle(targetCurrency);
    collectedByInterest.set(interestId, total);
  }

  const positions: DepositPosition[] = [];
  for (const r of rows) {
    // A single expected figure per deal — `normalizeAmount` is fine here; on a
    // missing rate it returns null, in which case we fall back to the raw
    // amount labelled in source units (the figure can't be dropped silently as
    // it gates the outstanding/pipeline KPIs). 0-expected deals are excluded
    // upstream by the IS NOT NULL filter.
    const expected =
      (await normalizeAmount(
        Number(r.expected ?? 0),
        r.expectedCurrency ?? targetCurrency,
        targetCurrency,
      )) ?? Number(r.expected ?? 0);
    const collected = collectedByInterest.get(r.interestId) ?? 0;
    const remaining = Math.max(0, expected - collected);
    const daysOutstanding = r.createdAt
      ? Math.floor((now - new Date(r.createdAt).getTime()) / 86_400_000)
      : 0;
    positions.push({
      interestId: r.interestId,
      clientName: r.clientName,
      mooring: r.mooring,
      expected,
      collected,
      remaining,
      currency: targetCurrency,
      daysOutstanding,
    });
  }
  return positions;
}

/** Σ of all payments in range, bucketed by payment type, normalised. */
async function sumPaymentsInRange(
  portId: string,
  range: DateRange,
  targetCurrency: string,
): Promise<{ total: number; deposit: number; balance: number; refund: number }> {
  const rows = await db
    .select({
      paymentType: payments.paymentType,
      amount: payments.amount,
      currency: payments.currency,
    })
    .from(payments)
    .where(
      and(
        eq(payments.portId, portId),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    );

  // Sum in source currency per type, convert each currency bucket once at the
  // end (M19). Refunds accumulate as MAGNITUDES (audit H12): `Math.abs` is
  // applied to the source amount before bucketing — conversion preserves
  // magnitude (rates are positive) — so a refund always subtracts and can
  // never inflate revenue regardless of its stored sign.
  const depositAcc = new CurrencyAccumulator();
  const balanceAcc = new CurrencyAccumulator();
  const refundMagAcc = new CurrencyAccumulator();
  for (const r of rows) {
    const raw = Number(r.amount ?? 0);
    const cur = r.currency ?? targetCurrency;
    if (r.paymentType === 'refund') {
      refundMagAcc.add(Math.abs(raw), cur);
    } else if (r.paymentType === 'deposit') {
      depositAcc.add(raw, cur);
    } else if (r.paymentType === 'balance') {
      balanceAcc.add(raw, cur);
    } else {
      // Any other non-refund type still contributes to total via its own
      // bucket; fold it into deposit so `total` stays = deposit+balance−refund.
      depositAcc.add(raw, cur);
    }
  }

  const [deposit, balance, refundMag] = await Promise.all([
    depositAcc.settle(targetCurrency),
    balanceAcc.settle(targetCurrency),
    refundMagAcc.settle(targetCurrency),
  ]);

  return {
    total: Number((deposit.total + balance.total - refundMag.total).toFixed(2)),
    deposit: deposit.total,
    balance: balance.total,
    refund: -refundMag.total,
  };
}

export async function getFinancialKpis(portId: string, range: DateRange): Promise<FinancialKpis> {
  const currency = await resolvePortCurrency(portId);
  const [paymentsAgg, positions, expensesTotal] = await Promise.all([
    sumPaymentsInRange(portId, range, currency),
    getDepositPositions(portId),
    sumExpensesInRange(portId, range, currency),
  ]);

  const pipelineExpected = positions.reduce((s, p) => s + p.expected, 0);
  const expectedDepositsOutstanding = positions.reduce((s, p) => s + p.remaining, 0);

  return {
    revenueCollected: paymentsAgg.total,
    depositsCollected: paymentsAgg.deposit,
    balanceCollected: paymentsAgg.balance,
    refundsIssued: Math.abs(paymentsAgg.refund),
    pipelineExpected,
    expectedDepositsOutstanding,
    expensesTotal,
    netContribution: netContribution(paymentsAgg.total, expensesTotal),
    currency,
  };
}

async function sumExpensesInRange(
  portId: string,
  range: DateRange,
  targetCurrency: string,
): Promise<number> {
  const rows = await db
    .select({ amount: expenses.amount, currency: expenses.currency })
    .from(expenses)
    .where(
      and(
        eq(expenses.portId, portId),
        isNull(expenses.archivedAt),
        gte(expenses.expenseDate, range.from),
        lte(expenses.expenseDate, range.to),
      ),
    );
  const acc = new CurrencyAccumulator();
  for (const r of rows) {
    acc.add(Number(r.amount ?? 0), r.currency ?? targetCurrency);
  }
  const { total } = await acc.settle(targetCurrency);
  return total;
}

// ─── Charts ──────────────────────────────────────────────────────────────────

export interface RevenueByMonthRow {
  month: string; // YYYY-MM
  deposit: number;
  balance: number;
}

/** Monthly collected revenue, split deposit vs balance. Continuous month
 *  axis (zero-filled) over the selected range, normalised to port currency. */
export async function getRevenueByMonth(
  portId: string,
  range: DateRange,
): Promise<RevenueByMonthRow[]> {
  const currency = await resolvePortCurrency(portId);
  const rows = await db
    .select({
      paymentType: payments.paymentType,
      amount: payments.amount,
      paymentCurrency: payments.currency,
      receivedAt: payments.receivedAt,
    })
    .from(payments)
    .where(
      and(
        eq(payments.portId, portId),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    );

  // One trio of source-currency accumulators per month; convert each once at
  // settle time (M19). Deposit bar = deposit − refund magnitude (audit H12):
  // `Math.abs` on the source amount so legacy positive-stored refunds also
  // subtract; conversion preserves magnitude.
  type MonthAcc = {
    deposit: CurrencyAccumulator;
    balance: CurrencyAccumulator;
    refundMag: CurrencyAccumulator;
  };
  const byMonth = new Map<string, MonthAcc>();
  for (const key of monthRange(range.from, range.to))
    byMonth.set(key, {
      deposit: new CurrencyAccumulator(),
      balance: new CurrencyAccumulator(),
      refundMag: new CurrencyAccumulator(),
    });
  for (const r of rows) {
    if (!r.receivedAt) continue;
    const acc = byMonth.get(monthKey(new Date(r.receivedAt)));
    if (!acc) continue;
    const raw = Number(r.amount ?? 0);
    const cur = r.paymentCurrency ?? currency;
    if (r.paymentType === 'balance') acc.balance.add(raw, cur);
    else if (r.paymentType === 'refund') acc.refundMag.add(Math.abs(raw), cur);
    else acc.deposit.add(raw, cur); // deposit + other → deposit bar
  }

  const out: RevenueByMonthRow[] = [];
  for (const [month, acc] of byMonth) {
    const [deposit, balance, refundMag] = await Promise.all([
      acc.deposit.settle(currency),
      acc.balance.settle(currency),
      acc.refundMag.settle(currency),
    ]);
    out.push({
      month,
      deposit: Number((deposit.total - refundMag.total).toFixed(2)),
      balance: balance.total,
    });
  }
  return out;
}

export interface CollectionFunnelRow {
  stage: 'eoi' | 'deposit' | 'contract' | 'won';
  label: string;
  count: number;
}

/** Money funnel: EOI sent → deposit received → contract reached → won,
 *  counting distinct deals reaching each step within the range. */
export async function getCollectionFunnel(
  portId: string,
  range: DateRange,
): Promise<CollectionFunnelRow[]> {
  const [eoiRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(interests)
    .where(
      and(
        eq(interests.portId, portId),
        gte(interests.dateEoiSent, range.from),
        lte(interests.dateEoiSent, range.to),
      ),
    );

  const [depositRow] = await db
    .select({ count: sql<number>`count(distinct ${payments.interestId})::int` })
    .from(payments)
    .where(
      and(
        eq(payments.portId, portId),
        eq(payments.paymentType, 'deposit'),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    );

  const [contractRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(interests)
    .where(
      and(
        eq(interests.portId, portId),
        sql`${interests.pipelineStage} = 'contract'`,
        isNull(interests.archivedAt),
      ),
    );

  const [wonRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(interests)
    .where(
      and(
        eq(interests.portId, portId),
        eq(interests.outcome, 'won'),
        gte(interests.outcomeAt, range.from),
        lte(interests.outcomeAt, range.to),
      ),
    );

  return [
    { stage: 'eoi', label: 'EOI sent', count: Number(eoiRow?.count ?? 0) },
    { stage: 'deposit', label: 'Deposit received', count: Number(depositRow?.count ?? 0) },
    { stage: 'contract', label: 'At contract', count: Number(contractRow?.count ?? 0) },
    { stage: 'won', label: 'Won', count: Number(wonRow?.count ?? 0) },
  ];
}

export interface AgingRow {
  bucket: AgingBucket;
  count: number;
  value: number;
}

/** Expected deposits still outstanding, bucketed by deal age. */
export async function getExpectedDepositAging(portId: string): Promise<AgingRow[]> {
  const positions = await getDepositPositions(portId);
  const byBucket = new Map<AgingBucket, { count: number; value: number }>();
  for (const b of AGING_BUCKETS) byBucket.set(b, { count: 0, value: 0 });
  for (const p of positions) {
    if (p.remaining <= 0) continue;
    const bucket = byBucket.get(agingBucket(p.daysOutstanding))!;
    bucket.count += 1;
    bucket.value += p.remaining;
  }
  return AGING_BUCKETS.map((bucket) => ({ bucket, ...byBucket.get(bucket)! }));
}

export interface CashFlowRow {
  month: string;
  inflow: number;
  outflow: number;
}

/** Monthly inflow (payments received) vs outflow (expenses booked). */
export async function getCashFlow(portId: string, range: DateRange): Promise<CashFlowRow[]> {
  const currency = await resolvePortCurrency(portId);
  // Per-month source-currency accumulators; convert each bucket once at settle
  // (M19). Inflow = non-refund payments − refund magnitudes (audit H12); the
  // refund magnitude rides its own accumulator so it converts at its own
  // currency's rate before being subtracted.
  type FlowAcc = {
    inflow: CurrencyAccumulator;
    refundMag: CurrencyAccumulator;
    outflow: CurrencyAccumulator;
  };
  const byMonth = new Map<string, FlowAcc>();
  for (const key of monthRange(range.from, range.to))
    byMonth.set(key, {
      inflow: new CurrencyAccumulator(),
      refundMag: new CurrencyAccumulator(),
      outflow: new CurrencyAccumulator(),
    });

  const paymentRows = await db
    .select({
      paymentType: payments.paymentType,
      amount: payments.amount,
      currency: payments.currency,
      receivedAt: payments.receivedAt,
    })
    .from(payments)
    .where(
      and(
        eq(payments.portId, portId),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    );
  for (const r of paymentRows) {
    if (!r.receivedAt) continue;
    const acc = byMonth.get(monthKey(new Date(r.receivedAt)));
    if (!acc) continue;
    const raw = Number(r.amount ?? 0);
    const cur = r.currency ?? currency;
    if (r.paymentType === 'refund') acc.refundMag.add(Math.abs(raw), cur);
    else acc.inflow.add(raw, cur);
  }

  const expenseRows = await db
    .select({
      amount: expenses.amount,
      currency: expenses.currency,
      expenseDate: expenses.expenseDate,
    })
    .from(expenses)
    .where(
      and(
        eq(expenses.portId, portId),
        isNull(expenses.archivedAt),
        gte(expenses.expenseDate, range.from),
        lte(expenses.expenseDate, range.to),
      ),
    );
  for (const r of expenseRows) {
    if (!r.expenseDate) continue;
    const acc = byMonth.get(monthKey(new Date(r.expenseDate)));
    if (!acc) continue;
    acc.outflow.add(Number(r.amount ?? 0), r.currency ?? currency);
  }

  const out: CashFlowRow[] = [];
  for (const [month, acc] of byMonth) {
    const [inflow, refundMag, outflow] = await Promise.all([
      acc.inflow.settle(currency),
      acc.refundMag.settle(currency),
      acc.outflow.settle(currency),
    ]);
    out.push({
      month,
      inflow: Number((inflow.total - refundMag.total).toFixed(2)),
      outflow: outflow.total,
    });
  }
  return out;
}

export interface ExpenseBreakdownRow {
  category: string;
  total: number;
}

/** Expenses for the period grouped by category, normalised, sorted desc. */
export async function getExpenseBreakdown(
  portId: string,
  range: DateRange,
): Promise<ExpenseBreakdownRow[]> {
  const currency = await resolvePortCurrency(portId);
  const rows = await db
    .select({ category: expenses.category, amount: expenses.amount, currency: expenses.currency })
    .from(expenses)
    .where(
      and(
        eq(expenses.portId, portId),
        isNull(expenses.archivedAt),
        gte(expenses.expenseDate, range.from),
        lte(expenses.expenseDate, range.to),
      ),
    );
  // One source-currency accumulator per category; settle each once (M19).
  const byCategory = new Map<string, CurrencyAccumulator>();
  for (const r of rows) {
    const cat = r.category ?? 'Uncategorised';
    let acc = byCategory.get(cat);
    if (!acc) {
      acc = new CurrencyAccumulator();
      byCategory.set(cat, acc);
    }
    acc.add(Number(r.amount ?? 0), r.currency ?? currency);
  }
  const out: ExpenseBreakdownRow[] = [];
  for (const [category, acc] of byCategory) {
    const { total } = await acc.settle(currency);
    out.push({ category, total });
  }
  return out.sort((a, b) => b.total - a.total);
}

// ─── Tables ──────────────────────────────────────────────────────────────────

export interface OutstandingDepositRow {
  interestId: string;
  clientName: string;
  mooring: string | null;
  expected: number;
  collected: number;
  remaining: number;
  currency: string;
  daysOutstanding: number;
}

/** Active deals with an expected deposit not yet fully collected, sorted
 *  by remaining amount desc. The "chase these" list. */
export async function getOutstandingDeposits(portId: string): Promise<OutstandingDepositRow[]> {
  const positions = await getDepositPositions(portId);
  return positions
    .filter((p) => p.remaining > 0)
    .sort((a, b) => b.remaining - a.remaining)
    .slice(0, 50);
}

export interface RecentPaymentRow {
  id: string;
  receivedAt: string;
  clientName: string;
  mooring: string | null;
  paymentType: string;
  amount: number;
  currency: string;
}

export async function getRecentPayments(
  portId: string,
  range: DateRange,
): Promise<RecentPaymentRow[]> {
  const currency = await resolvePortCurrency(portId);
  const rows = await db
    .select({
      id: payments.id,
      receivedAt: payments.receivedAt,
      clientName: clients.fullName,
      mooring: berths.mooringNumber,
      paymentType: payments.paymentType,
      amount: payments.amount,
      paymentCurrency: payments.currency,
    })
    .from(payments)
    .innerJoin(clients, eq(clients.id, payments.clientId))
    .leftJoin(
      interestBerths,
      and(eq(interestBerths.interestId, payments.interestId), eq(interestBerths.isPrimary, true)),
    )
    .leftJoin(berths, eq(interestBerths.berthId, berths.id))
    .where(
      and(
        eq(payments.portId, portId),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    )
    .orderBy(desc(payments.receivedAt))
    .limit(50);

  const out: RecentPaymentRow[] = [];
  for (const r of rows) {
    out.push({
      id: r.id,
      receivedAt: r.receivedAt ? new Date(r.receivedAt).toISOString() : '',
      clientName: r.clientName,
      mooring: r.mooring,
      paymentType: r.paymentType,
      // Single displayed figure: on a missing/stale rate fall back to the raw
      // source amount (logged in normalizeAmount) rather than dropping the row.
      amount:
        (await normalizeAmount(Number(r.amount ?? 0), r.paymentCurrency ?? currency, currency)) ??
        Number(r.amount ?? 0),
      currency,
    });
  }
  return out;
}

export interface RefundRow {
  id: string;
  receivedAt: string;
  clientName: string;
  amount: number;
  currency: string;
  notes: string | null;
}

/** Refund-type payments in range (write-off / refund log analogue). */
export async function getRefundLog(portId: string, range: DateRange): Promise<RefundRow[]> {
  const currency = await resolvePortCurrency(portId);
  const rows = await db
    .select({
      id: payments.id,
      receivedAt: payments.receivedAt,
      clientName: clients.fullName,
      amount: payments.amount,
      paymentCurrency: payments.currency,
      notes: payments.notes,
    })
    .from(payments)
    .innerJoin(clients, eq(clients.id, payments.clientId))
    .where(
      and(
        eq(payments.portId, portId),
        eq(payments.paymentType, 'refund'),
        gte(payments.receivedAt, range.from),
        lte(payments.receivedAt, range.to),
      ),
    )
    .orderBy(desc(payments.receivedAt))
    .limit(50);

  const out: RefundRow[] = [];
  for (const r of rows) {
    out.push({
      id: r.id,
      receivedAt: r.receivedAt ? new Date(r.receivedAt).toISOString() : '',
      clientName: r.clientName,
      amount: Math.abs(
        (await normalizeAmount(Number(r.amount ?? 0), r.paymentCurrency ?? currency, currency)) ??
          Number(r.amount ?? 0),
      ),
      currency,
      notes: r.notes,
    });
  }
  return out;
}

export interface ExpenseLedgerRow {
  id: string;
  expenseDate: string;
  payer: string | null;
  category: string | null;
  establishmentName: string | null;
  amount: number;
  currency: string;
  paymentStatus: string | null;
}

export async function getExpenseLedger(
  portId: string,
  range: DateRange,
): Promise<ExpenseLedgerRow[]> {
  const currency = await resolvePortCurrency(portId);
  const rows = await db
    .select({
      id: expenses.id,
      expenseDate: expenses.expenseDate,
      payer: expenses.payer,
      category: expenses.category,
      establishmentName: expenses.establishmentName,
      amount: expenses.amount,
      expenseCurrency: expenses.currency,
      paymentStatus: expenses.paymentStatus,
    })
    .from(expenses)
    .where(
      and(
        eq(expenses.portId, portId),
        isNull(expenses.archivedAt),
        gte(expenses.expenseDate, range.from),
        lte(expenses.expenseDate, range.to),
      ),
    )
    .orderBy(desc(expenses.expenseDate))
    .limit(50);

  const out: ExpenseLedgerRow[] = [];
  for (const r of rows) {
    out.push({
      id: r.id,
      expenseDate: r.expenseDate ? new Date(r.expenseDate).toISOString() : '',
      payer: r.payer,
      category: r.category,
      establishmentName: r.establishmentName,
      amount:
        (await normalizeAmount(Number(r.amount ?? 0), r.expenseCurrency ?? currency, currency)) ??
        Number(r.amount ?? 0),
      currency,
      paymentStatus: r.paymentStatus,
    });
  }
  return out;
}

/**
 * Window-independent existence check: does this port have any payment OR
 * expense? Drives the Financial report-level empty state.
 */
export async function financialHasData(portId: string): Promise<boolean> {
  const [pay, exp] = await Promise.all([
    db
      .select({ one: sql<number>`1` })
      .from(payments)
      .where(eq(payments.portId, portId))
      .limit(1),
    db
      .select({ one: sql<number>`1` })
      .from(expenses)
      .where(eq(expenses.portId, portId))
      .limit(1),
  ]);
  return pay.length > 0 || exp.length > 0;
}
