/**
 * Affiliate credit-ledger accrual — ledger-first, idempotent pending/matured projection.
 *
 * Accrual: append a ledger entry in-tx (appendLedgerEntryTx) + compute the maturation timestamp (computeMatureAt).
 * Insert order: core ledger_entries → affiliate_entries side-row → accrueVesting (same tx).
 */
import type { Transaction } from '@platform-modules/db'
import { appendEntry, type LedgerSchema } from '@platform-modules/ledger'
import { accrueVesting, type VestingSchema } from '@platform-modules/ledger/vesting'
import { affiliateEntriesTable, type AffiliateSchema } from './schema.js'

export type CreditEntryType =
  | 'referral_reward'
  | 'affiliate_commission'
  | 'redemption'
  | 'refund_clawback'
  | 'adjustment'

/** Host precondition (I0): earn sourceIds must be colon-free opaque ids. */
export class InvalidSourceIdError extends Error {
  readonly code = 'INVALID_SOURCE_ID' as const
  readonly _affiliateError = 'InvalidSourceIdError' as const

  constructor(sourceId: string) {
    super(`sourceId must be colon-free (I0 precondition); got: ${sourceId}`)
    this.name = 'InvalidSourceIdError'
  }
}

export function isInvalidSourceIdError(e: unknown): e is InvalidSourceIdError {
  return (
    typeof e === 'object' &&
    e !== null &&
    (e as { _affiliateError?: unknown })._affiliateError === 'InvalidSourceIdError'
  )
}

/** Positive accrual must be an earn entry type — non-earn credits use other entrypoints. */
export class UnsupportedAccrualEntryTypeError extends Error {
  readonly code = 'UNSUPPORTED_ACCRUAL_ENTRY_TYPE' as const
  readonly _affiliateError = 'UnsupportedAccrualEntryTypeError' as const

  constructor(entryType: CreditEntryType) {
    super(`accrueCommission does not accept positive non-earn entryType: ${entryType}`)
    this.name = 'UnsupportedAccrualEntryTypeError'
  }
}

export function isUnsupportedAccrualEntryTypeError(
  e: unknown,
): e is UnsupportedAccrualEntryTypeError {
  return (
    typeof e === 'object' &&
    e !== null &&
    (e as { _affiliateError?: unknown })._affiliateError === 'UnsupportedAccrualEntryTypeError'
  )
}

export type MatureAtInput = {
  kind: 'coupon' | 'physical' | string
  paidAt: Date
  expiresAt: Date | null
  redeemedAt: Date | null
}

export type MatureAtSettings = { holdDays: number }

/**
 * Compute the mature_at timestamp for a credit ledger entry.
 *
 * - physical deal: paidAt + holdDays
 * - coupon (redeemed): redeemedAt + holdDays
 * - coupon (not yet redeemed): max(paidAt, expiresAt) + holdDays
 */
export function computeMatureAt(input: MatureAtInput, settings: MatureAtSettings): Date {
  const holdMs = settings.holdDays * 86_400_000

  if (input.kind === 'coupon') {
    if (input.redeemedAt) {
      return new Date(input.redeemedAt.getTime() + holdMs)
    }
    const anchor = input.expiresAt
      ? new Date(Math.max(input.paidAt.getTime(), input.expiresAt.getTime()))
      : input.paidAt
    return new Date(anchor.getTime() + holdMs)
  }

  return new Date(input.paidAt.getTime() + holdMs)
}

export interface AccrueCommissionInput {
  userId: string
  amountMinor: bigint
  entryType: CreditEntryType
  sourceType: string
  sourceId: string
  matureAt: Date
  referralId?: string
  memo?: string
  resolvedPct?: number
}

export type AccrueCommissionState = 'pending' | 'matured' | 'audit'

export interface AccrueCommissionResult {
  /** True when a NEW ledger row was inserted (idempotent no-op → false). */
  inserted: boolean
  amountMinor: bigint
  /**
   * Wallet bucket touched — `pending` when hold active, `matured` when instant.
   * `audit` = no vesting bucket claimed: zero-amount rows, non-earn rows, OR a
   * positive-earn idempotent REPLAY no-op (`inserted === false` — nothing moved).
   * A positive NON-earn entryType never reaches here — it throws
   * `UnsupportedAccrualEntryTypeError` at the boundary (no stranded credit).
   */
  state: AccrueCommissionState
}

export function buildIdempotencyKey(
  entryType: CreditEntryType,
  sourceType: string,
  sourceId: string,
): string {
  if (sourceId.includes(':')) {
    throw new InvalidSourceIdError(sourceId)
  }
  return `${entryType}:${sourceType}:${sourceId}`
}

function isPositiveEarn(amountMinor: bigint, entryType: CreditEntryType): boolean {
  return (
    amountMinor > 0n &&
    (entryType === 'referral_reward' || entryType === 'affiliate_commission')
  )
}

/**
 * Core ledger insert + vesting projection — runs INSIDE a caller-provided transaction.
 */
async function appendAffiliateLedgerEntryTx<S extends AffiliateSchema & LedgerSchema & VestingSchema>(
  tx: Transaction<S>,
  input: AccrueCommissionInput,
): Promise<{ inserted: boolean; vestingState?: 'pending' | 'matured' }> {
  if (input.amountMinor > 0n && !isPositiveEarn(input.amountMinor, input.entryType)) {
    throw new UnsupportedAccrualEntryTypeError(input.entryType)
  }

  const idempotencyKey = buildIdempotencyKey(input.entryType, input.sourceType, input.sourceId)

  const { inserted, id } = await appendEntry(tx, {
    key: idempotencyKey,
    delta: input.amountMinor,
    reason: input.entryType,
    ref: {
      sourceType: input.sourceType,
      sourceId: input.sourceId,
      userId: input.userId,
      ...(input.referralId ? { referralId: input.referralId } : {}),
    },
  })

  if (!inserted || id === null) return { inserted: false }

  await tx.insert(affiliateEntriesTable).values({
    entryId: id,
    ownerId: input.userId,
    entryType: input.entryType,
    sourceType: input.sourceType,
    sourceId: input.sourceId,
    referralId: input.referralId,
    resolvedPct: input.resolvedPct,
    memo: input.memo,
  })

  if (isPositiveEarn(input.amountMinor, input.entryType)) {
    const vesting = await accrueVesting(tx, {
      ownerId: input.userId,
      entryId: id,
      amountMinor: input.amountMinor,
      matureAt: input.matureAt,
    })
    return { inserted: true, vestingState: vesting.state }
  }

  return { inserted: true }
}

function resolveAccrueState(
  amountMinor: bigint,
  entryType: CreditEntryType,
  inserted: boolean,
  vestingState?: 'pending' | 'matured',
): AccrueCommissionState {
  if (amountMinor === 0n) return 'audit'
  if (isPositiveEarn(amountMinor, entryType)) {
    if (!inserted) return 'audit'
    if (vestingState === undefined) {
      throw new Error('INVARIANT: vestingState missing after positive earn insert')
    }
    return vestingState
  }
  return 'matured'
}

/**
 * Accrue commission/reward — insert core ledger row first (idempotent),
 * then affiliate side-row + vesting buckets in the same tx.
 */
export async function accrueCommission<S extends AffiliateSchema & LedgerSchema & VestingSchema>(
  tx: Transaction<S>,
  input: AccrueCommissionInput,
): Promise<AccrueCommissionResult> {
  const { inserted, vestingState } = await appendAffiliateLedgerEntryTx(tx, input)

  return {
    inserted,
    amountMinor: input.amountMinor,
    state: resolveAccrueState(input.amountMinor, input.entryType, inserted, vestingState),
  }
}

/** @internal Earn path audit rows (fraud:block zero-amount sentinel). */
export async function appendAffiliateLedgerEntry<
  S extends AffiliateSchema & LedgerSchema & VestingSchema,
>(
  tx: Transaction<S>,
  input: AccrueCommissionInput,
): Promise<boolean> {
  const { inserted } = await appendAffiliateLedgerEntryTx(tx, input)
  return inserted
}
