/**
 * Onboarding step 4 — bulk team invites.
 *
 * Reuses createInvitation from foundation-auth-rbac and sendInvitationEmail queue seam.
 */
import { and, eq, isNull } from '@zync/db'
import type { Db, DbTx } from '@zync/db'
import { invitations } from '@zync/db/schema'
import {
  countActiveMembers,
  createInvitationInTx,
  findUserByEmail,
  getMembership,
  getRoleByName,
  getTenantById,
} from '@zync/db/queries'
import { generateOpaqueToken, getMaxTeamMembers, hashToken } from '@zync/auth'
import type { Env, TenantId, TenantTier, UserId } from '@zync/types'
import { sendInvitationEmail } from '../adapters/email'

const INVITE_TTL_MS = 1000 * 60 * 60 * 24 * 7

export type OnboardingInviteInput = {
  email: string
  role: 'ADMIN' | 'MEMBER' | 'VIEWER' | 'CONTRACTOR'
}

export class OnboardingSeatCapError extends Error {
  constructor() {
    super('Upgrade required')
    this.name = 'OnboardingSeatCapError'
  }
}

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase()
}

async function hasPendingInvitation(
  db: Db | DbTx,
  tenantId: string,
  email: string,
): Promise<boolean> {
  const [row] = await db
    .select({ id: invitations.id })
    .from(invitations)
    .where(
      and(
        eq(invitations.tenantId, tenantId),
        eq(invitations.email, email),
        isNull(invitations.acceptedAt),
      ),
    )
    .limit(1)
  return Boolean(row)
}

async function isActiveMember(db: Db | DbTx, tenantId: string, email: string): Promise<boolean> {
  const user = await findUserByEmail(db, email)
  if (!user) return false
  const membership = await getMembership(db, user.id as UserId, tenantId as TenantId)
  return membership?.status === 'active'
}

export async function sendOnboardingInvites(
  tx: DbTx,
  tenantId: string,
  actorId: string,
  invites: OnboardingInviteInput[],
  env: Env,
  opts?: { actorIp?: string | null; requestId?: string | null },
): Promise<{ sent: number }> {
  const tenant = await getTenantById(tx, tenantId as TenantId)
  if (!tenant) {
    throw new Error('Tenant not found')
  }

  const seen = new Set<string>()
  const unique: OnboardingInviteInput[] = []
  for (const invite of invites) {
    const email = normalizeEmail(invite.email)
    if (!email || seen.has(email)) continue
    seen.add(email)
    unique.push({ email, role: invite.role })
  }

  const toSend: OnboardingInviteInput[] = []
  for (const invite of unique) {
    if (await isActiveMember(tx, tenantId, invite.email)) continue
    if (await hasPendingInvitation(tx, tenantId, invite.email)) continue
    toSend.push(invite)
  }

  if (toSend.length === 0) {
    return { sent: 0 }
  }

  const active = await countActiveMembers(tx, tenantId as TenantId)
  const max = getMaxTeamMembers(tenant.tier as TenantTier)
  if (active + toSend.length > max) {
    throw new OnboardingSeatCapError()
  }

  let sent = 0
  for (const invite of toSend) {
    const role = await getRoleByName(tx, tenantId as TenantId, invite.role)
    if (!role) {
      throw new Error(`Invalid role: ${invite.role}`)
    }

    const plain = generateOpaqueToken()
    const tokenHash = await hashToken(plain)
    await createInvitationInTx(tx, {
      tenantId: tenantId as TenantId,
      email: invite.email,
      roleId: role.id,
      tokenHash,
      expiresAt: new Date(Date.now() + INVITE_TTL_MS),
      invitedBy: actorId as UserId,
      actorIp: opts?.actorIp ?? null,
      requestId: opts?.requestId ?? null,
    })

    void sendInvitationEmail(env, invite.email, plain, tenantId, actorId)
    sent += 1
  }

  return { sent }
}
