/**
 * Referral attribution helpers.
 *
 * resolveRefCode   — look up an active referral_links row by code.
 * isSelfReferral   — detect same-account or same-phone-blind-index collisions.
 * bindReferralOnSignup — last-click bind of a new referee to the link owner.
 */

import { eq, and } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import { referralLinksTable, referralsTable, type Referral, type ReferralLink } from './schema.js'
import type { FraudHostStore } from './fraud/host-store.js'
import type { AdapterConfig, SignupCtx } from './fraud/types.js'
import { registerFraudAdapters } from './fraud/register-all.js'
import { runFraudPipeline } from './fraud/registry.js'

/** Adopter DB handle — generic over the host's combined schema. */
type AttributionDb = Querier<Record<string, unknown>>

// ---------------------------------------------------------------------------
// Typed errors
// ---------------------------------------------------------------------------

export class ReferralSettingsNotFoundError extends Error {
  readonly code = 'REFERRAL_SETTINGS_NOT_FOUND' as const
  readonly _affiliateError = 'ReferralSettingsNotFoundError' as const

  constructor(message = 'referral_settings row not found — run migration seed') {
    super(message)
    this.name = 'ReferralSettingsNotFoundError'
  }
}

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

// ---------------------------------------------------------------------------
// resolveRefCode
// ---------------------------------------------------------------------------

/**
 * Return the active referral_links row for the given code, or null if not
 * found / inactive.
 */
export async function resolveRefCode(
  db: AttributionDb,
  code: string,
): Promise<ReferralLink | null> {
  const [link] = await db
    .select()
    .from(referralLinksTable)
    .where(and(eq(referralLinksTable.code, code), eq(referralLinksTable.active, true)))
  return link ?? null
}

// ---------------------------------------------------------------------------
// isSelfReferral
// ---------------------------------------------------------------------------

/**
 * Returns true when the referrer and referee are the same account, OR when
 * they share the same phone blind-index (i.e. the same real phone number
 * registered under two different accounts).
 *
 * Host-data rule (spec §3A, module-wide): the `users` table is host-resident,
 * so the phone-index FETCH routes through the `FraudHostStore`; the COMPARISON
 * (index equality) stays in the module. No raw host-table SQL in core.
 */
export async function isSelfReferral(
  host: FraudHostStore,
  args: { referrerUserId: string; refereeUserId: string },
): Promise<boolean> {
  if (args.referrerUserId === args.refereeUserId) return true

  const rows = await host.getPhoneBlindIndexes([args.referrerUserId, args.refereeUserId])

  const referrer = rows.find((x) => x.userId === args.referrerUserId)
  const referee = rows.find((x) => x.userId === args.refereeUserId)

  return !!(
    referrer?.phoneBlindIndex &&
    referee?.phoneBlindIndex &&
    referrer.phoneBlindIndex === referee.phoneBlindIndex
  )
}

// ---------------------------------------------------------------------------
// bindReferralOnSignup
// ---------------------------------------------------------------------------

async function loadFraudConfig(host: FraudHostStore): Promise<Record<string, AdapterConfig>> {
  // `referral_settings` is host-resident (not affiliate-owned schema): the FETCH
  // routes through FraudHostStore (spec §3A). null row → fail loud in the module.
  const fraudConfig = await host.getFraudConfig()
  if (fraudConfig === null) {
    throw new ReferralSettingsNotFoundError()
  }
  return fraudConfig
}

/**
 * Bind a newly-registered referee to the link owner as a pending referral.
 *
 * Semantics:
 *  - Last-click (cookie overwritten on each touch; most recent click before
 *    signup binds) / one-per-account: referrals.refereeUserId is UNIQUE, so a
 *    second call for the same referee silently returns the pre-existing row.
 *  - Self-referral guard: returns null when the link owner is the referee.
 *  - Inactive link guard: returns null when the link no longer exists or is
 *    inactive.
 *
 * Returns the referrals row (new or pre-existing), or null on guard failure.
 */
export interface BindReferralInput {
  refereeUserId: string
  linkId: string
  clickedAt?: Date | null
  refereeEmail?: string
  refereePhone?: string
  visitorId?: string
  ipHash?: string
  cfBotScore?: number
  /** Axis-C host store — required for SIGNUP fraud adapters. */
  host: FraudHostStore
}

export async function bindReferralOnSignup(
  db: AttributionDb,
  args: BindReferralInput,
): Promise<Referral | null> {
  const [link] = await db
    .select()
    .from(referralLinksTable)
    .where(eq(referralLinksTable.id, args.linkId))

  if (!link || !link.active) return null
  if (link.ownerUserId === args.refereeUserId) return null
  if (
    await isSelfReferral(args.host, {
      referrerUserId: link.ownerUserId,
      refereeUserId: args.refereeUserId,
    })
  ) {
    return null
  }

  registerFraudAdapters()

  const fraudConfig = await loadFraudConfig(args.host)
  const signupCtx: SignupCtx = {
    db,
    host: args.host,
    refereeUserId: args.refereeUserId,
    refereeEmail: args.refereeEmail ?? '',
    refereePhone: args.refereePhone,
    visitorId: args.visitorId,
    ipHash: args.ipHash,
    cfBotScore: args.cfBotScore,
  }
  const { action } = await runFraudPipeline({
    point: 'SIGNUP',
    ctx: signupCtx,
    userId: args.refereeUserId,
    fraudConfig,
    db,
    persistEvents: true,
  })

  if (action === 'block') {
    return null
  }

  const inserted = await db
    .insert(referralsTable)
    .values({
      referrerUserId: link.ownerUserId,
      refereeUserId: args.refereeUserId,
      linkId: link.id,
      kind: link.kind,
      status: 'pending',
      clickedAt: args.clickedAt ?? new Date(),
      quarantinedAt: action === 'hold' ? new Date() : null,
    })
    .onConflictDoNothing({ target: referralsTable.refereeUserId })
    .returning()

  if (inserted.length > 0) return inserted[0]!

  const [existing] = await db
    .select()
    .from(referralsTable)
    .where(eq(referralsTable.refereeUserId, args.refereeUserId))
  return existing ?? null
}
