/**
 * `@platform-modules/ledger/vesting` — opt-in vesting and carried-debt projection.
 */
export * from './vesting-schema.js'

export { VestingInvariantError, isVestingInvariantError } from './errors.js'

import { and, eq, inArray, isNull, lte, sql } from 'drizzle-orm'
import type { PostgresTransaction, Querier } from '@platform-modules/db'
import { VestingInvariantError } from './errors.js'
import { ledgerEntryVesting, walletVesting, type VestingSchema } from './vesting-schema.js'

export async function accrueVesting<S extends VestingSchema>(
  tx: PostgresTransaction<S>,
  input: { ownerId: string; entryId: string; amountMinor: bigint; matureAt: Date },
): Promise<{ state: 'pending' | 'matured' }> {
  if (input.amountMinor <= 0n) throw new Error('accrueVesting: amount must be positive')

  const now = new Date()
  const isPending = input.matureAt > now
  const state = isPending ? ('pending' as const) : ('matured' as const)

  await tx.insert(ledgerEntryVesting).values({
    entryId: input.entryId,
    matureAt: input.matureAt,
    sweptAt: isPending ? null : now,
  })

  await tx
    .insert(walletVesting)
    .values({
      ownerId: input.ownerId,
      pendingMinor: isPending ? input.amountMinor : 0n,
      maturedMinor: isPending ? 0n : input.amountMinor,
      lifetimeEarnedMinor: input.amountMinor,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: walletVesting.ownerId,
      set: {
        pendingMinor: isPending
          ? sql`${walletVesting.pendingMinor} + ${input.amountMinor}`
          : walletVesting.pendingMinor,
        maturedMinor: isPending
          ? walletVesting.maturedMinor
          : sql`${walletVesting.maturedMinor} + greatest(${input.amountMinor} - ${walletVesting.carriedDebtMinor}, 0)`,
        carriedDebtMinor: isPending
          ? walletVesting.carriedDebtMinor
          : sql`greatest(${walletVesting.carriedDebtMinor} - ${input.amountMinor}, 0)`,
        lifetimeEarnedMinor: sql`${walletVesting.lifetimeEarnedMinor} + ${input.amountMinor}`,
        updatedAt: now,
      },
    })

  return { state }
}

export async function promoteEntries<S extends VestingSchema>(
  tx: PostgresTransaction<S>,
  input: {
    earnEntryIds: { entryId: string; ownerId: string }[]
    perOwnerDeltas: { ownerId: string; deltaMinor: bigint }[]
  },
): Promise<void> {
  const ids = input.earnEntryIds.map((e) => e.entryId)
  if (new Set(ids).size !== ids.length) {
    throw new VestingInvariantError('duplicate earnEntryId')
  }

  const dOwners = input.perOwnerDeltas.map((d) => d.ownerId)
  if (new Set(dOwners).size !== dOwners.length) {
    throw new VestingInvariantError('duplicate owner in perOwnerDeltas')
  }

  if (input.perOwnerDeltas.some((d) => d.deltaMinor < 0n)) {
    throw new VestingInvariantError('negative promotion delta')
  }

  const swept = await tx
    .update(ledgerEntryVesting)
    .set({ sweptAt: sql`now()` })
    .where(
      and(
        inArray(ledgerEntryVesting.entryId, ids),
        isNull(ledgerEntryVesting.sweptAt),
        lte(ledgerEntryVesting.matureAt, sql`now()`),
      ),
    )
    .returning({ entryId: ledgerEntryVesting.entryId })
  if (swept.length !== input.earnEntryIds.length) {
    throw new VestingInvariantError('earn not found, not matured, or already swept')
  }

  const earnOwners = new Set(input.earnEntryIds.map((e) => e.ownerId))
  const deltaOwners = new Set(dOwners)
  if (
    earnOwners.size !== deltaOwners.size ||
    [...earnOwners].some((o) => !deltaOwners.has(o))
  ) {
    throw new VestingInvariantError('earn-owner / delta-owner set mismatch')
  }

  for (const { ownerId, deltaMinor } of input.perOwnerDeltas) {
    const updated = await tx
      .update(walletVesting)
      .set({
        pendingMinor: sql`${walletVesting.pendingMinor} - ${deltaMinor}`,
        maturedMinor: sql`${walletVesting.maturedMinor} + greatest(${deltaMinor} - ${walletVesting.carriedDebtMinor}, 0)`,
        carriedDebtMinor: sql`greatest(${walletVesting.carriedDebtMinor} - ${deltaMinor}, 0)`,
        updatedAt: sql`now()`,
      })
      .where(eq(walletVesting.ownerId, ownerId))
      .returning({ ownerId: walletVesting.ownerId })
    if (updated.length !== 1) {
      throw new VestingInvariantError('missing wallet_vesting row for owner ' + ownerId)
    }
  }
}

export async function settleClawback<S extends VestingSchema>(
  tx: PostgresTransaction<S>,
  input: { ownerId: string; amountMinor: bigint },
): Promise<void> {
  if (input.amountMinor <= 0n) {
    throw new VestingInvariantError('settleClawback: amount must be positive')
  }

  const now = new Date()
  await tx
    .insert(walletVesting)
    .values({
      ownerId: input.ownerId,
      carriedDebtMinor: input.amountMinor,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: walletVesting.ownerId,
      set: {
        maturedMinor: sql`greatest(${walletVesting.maturedMinor} - ${input.amountMinor}, 0)`,
        carriedDebtMinor: sql`${walletVesting.carriedDebtMinor} + greatest(${input.amountMinor} - ${walletVesting.maturedMinor}, 0)`,
        updatedAt: now,
      },
    })
}

/**
 * Recomputes `withdrawableMinor` as max(0, eligible − paid) under the wallet_vesting owner lock.
 *
 * LOCK-THEN-COMPUTE (P5): upsert-then-lock the owner row FIRST, then call `compute()` so the
 * caller reads eligible/paid under the held lock, then write the clamped withdrawable.
 *
 * DEADLOCK-AVOIDANCE: `compute()` MUST read matured `ledger_entry_vesting` rows PLAIN — never
 * FOR UPDATE. Locking entry rows while holding the owner lock reverses lock order vs
 * `promoteEntries`' sweep (entry→owner) and deadlocks. The owner lock alone serializes
 * recompute against the T12 withdrawal debit (which locks the SAME wallet_vesting row first).
 */
export async function recomputeWithdrawable<S extends VestingSchema>(
  tx: PostgresTransaction<S>,
  target: { ownerId: string },
  compute: () => Promise<{ eligibleMinor: bigint; paidMinor: bigint }>,
): Promise<void> {
  const now = new Date()
  const { ownerId } = target

  await tx
    .insert(walletVesting)
    .values({
      ownerId,
      pendingMinor: 0n,
      maturedMinor: 0n,
      withdrawableMinor: 0n,
      lifetimeEarnedMinor: 0n,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: walletVesting.ownerId,
      set: { updatedAt: now },
    })
    .returning()

  const { eligibleMinor, paidMinor } = await compute()

  const w = eligibleMinor - paidMinor
  const withdrawableMinor = w > 0n ? w : 0n

  await tx
    .update(walletVesting)
    .set({ withdrawableMinor, updatedAt: now })
    .where(eq(walletVesting.ownerId, ownerId))
}

export async function setEntryWithdrawableAt<S extends VestingSchema>(
  tx: PostgresTransaction<S>,
  input: { entryId: string; withdrawableAt: Date | null },
): Promise<void> {
  const updated = await tx
    .update(ledgerEntryVesting)
    .set({ withdrawableAt: input.withdrawableAt })
    .where(eq(ledgerEntryVesting.entryId, input.entryId))
    .returning({ entryId: ledgerEntryVesting.entryId })
  if (updated.length !== 1) {
    throw new VestingInvariantError('missing ledger_entry_vesting row for entry ' + input.entryId)
  }
}

export async function getVesting<S extends VestingSchema>(
  tx: Querier<S>,
  ownerId: string,
): Promise<{
  pendingMinor: bigint
  maturedMinor: bigint
  carriedDebtMinor: bigint
  netMinor: bigint
  withdrawableMinor: bigint
  lifetimeEarnedMinor: bigint
}> {
  const [row] = await tx
    .select({
      pendingMinor: walletVesting.pendingMinor,
      maturedMinor: walletVesting.maturedMinor,
      carriedDebtMinor: walletVesting.carriedDebtMinor,
      withdrawableMinor: walletVesting.withdrawableMinor,
      lifetimeEarnedMinor: walletVesting.lifetimeEarnedMinor,
    })
    .from(walletVesting)
    .where(eq(walletVesting.ownerId, ownerId))
    .limit(1)

  const maturedMinor = row?.maturedMinor ?? 0n
  const carriedDebtMinor = row?.carriedDebtMinor ?? 0n

  return {
    pendingMinor: row?.pendingMinor ?? 0n,
    maturedMinor,
    carriedDebtMinor,
    netMinor: maturedMinor - carriedDebtMinor,
    withdrawableMinor: row?.withdrawableMinor ?? 0n,
    lifetimeEarnedMinor: row?.lifetimeEarnedMinor ?? 0n,
  }
}
