/**
 * Portal auth helpers — tenant-portals (wave-9 leaf 5).
 *
 * Generates and validates time-limited portal access tokens stored in
 * magic_link_tokens (purpose = 'portal'). Token is URL-safe plaintext;
 * only the SHA-256 hash is used for DB lookup.
 *
 * Uses Web Crypto API (available in Cloudflare Workers + modern Node) —
 * no external crypto package required from packages/db.
 */
import { and, eq, gt, isNull } from 'drizzle-orm'
import type { Db } from '../client'
import { magicLinkTokens } from '../schema/time'
import { customerContacts } from '../schema/customers'

// Portal tokens expire in 1 hour
const PORTAL_TOKEN_TTL_MS = 60 * 60 * 1000

// ── Crypto helpers ────────────────────────────────────────────────────────────

/** Generate a 32-byte URL-safe random token. */
function generateToken(): string {
  const bytes = new Uint8Array(32)
  crypto.getRandomValues(bytes)
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
}

/** SHA-256 hex hash of a plaintext token. */
async function sha256(token: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(token)
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}

// ── Public interfaces ─────────────────────────────────────────────────────────

export interface PortalTokenResult {
  /** URL-safe plaintext token to embed in magic link */
  token: string
  expiresAt: Date
}

export interface PortalCustomerSession {
  tenantId: string
  customerId: string
  contactId: string
  email: string
}

// ── Functions ─────────────────────────────────────────────────────────────────

/**
 * Generate a time-limited portal token for a customer contact.
 * Stores the token hash (not plaintext) in magic_link_tokens.
 * Returns the plaintext token for inclusion in the magic-link email.
 */
export async function generatePortalToken(
  db: Db,
  tenantId: string,
  _customerId: string,
  email: string,
): Promise<PortalTokenResult> {
  const token = generateToken()
  const tokenHash = await sha256(token)
  const expiresAt = new Date(Date.now() + PORTAL_TOKEN_TTL_MS)

  await db.insert(magicLinkTokens).values({
    tenantId,
    tokenHash,
    email,
    purpose: 'portal',
    expiresAt,
  })

  return { token, expiresAt }
}

/**
 * Validate a portal token and return the linked session info.
 * Marks the token as used atomically.
 * Returns null if the token is missing, expired, or already used.
 */
export async function validatePortalToken(
  db: Db,
  token: string,
): Promise<PortalCustomerSession | null> {
  const tokenHash = await sha256(token)

  // Atomically consume the token — wins the row-level lock, prevents double-use
  const consumed = await db
    .update(magicLinkTokens)
    .set({ usedAt: new Date() })
    .where(
      and(
        eq(magicLinkTokens.tokenHash, tokenHash),
        eq(magicLinkTokens.purpose, 'portal'),
        isNull(magicLinkTokens.usedAt),
        gt(magicLinkTokens.expiresAt, new Date()),
      ),
    )
    .returning({
      id: magicLinkTokens.id,
      email: magicLinkTokens.email,
      tenantId: magicLinkTokens.tenantId,
    })

  if (consumed.length === 0) return null

  const tokenRow = consumed[0]!
  if (!tokenRow.email) return null

  // Resolve the contact → customer
  const [contactRow] = await db
    .select({
      id: customerContacts.id,
      customerId: customerContacts.customerId,
      tenantId: customerContacts.tenantId,
      email: customerContacts.email,
    })
    .from(customerContacts)
    .where(
      and(
        eq(customerContacts.tenantId, tokenRow.tenantId!),
        eq(customerContacts.email, tokenRow.email!),
      ),
    )
    .limit(1)

  if (!contactRow) return null

  return {
    tenantId: contactRow.tenantId,
    customerId: contactRow.customerId,
    contactId: contactRow.id,
    email: contactRow.email,
  }
}
