/**
 * Customer portal access control — customer-portal-access-control (wave-10 leaf 7).
 *
 * Manages which customers have portal access, what modules they can see,
 * and invite token generation.
 *
 * Uses existing customer_portal_users table (from customers schema).
 * Module enablement stored in JSONB config keyed per portal user record
 * via the customers.settings JSONB (per-customer portal config).
 *
 * Zod schemas re-exported for route-side validation.
 */
import { and, eq } from 'drizzle-orm'
import { z } from 'zod'
import type { Db } from '../client'
import { customers, customerContacts, customerPortalUsers } from '../schema/customers'
import { users } from '../schema/users'
import { auditLog } from './_audit-forward'

// ── Zod schemas ───────────────────────────────────────────────────────────────

export const portalModuleSchema = z.enum(['invoices', 'projects', 'tickets'])
export type PortalModule = z.infer<typeof portalModuleSchema>

export const enablePortalAccessSchema = z.object({
  customerId: z.string().uuid(),
  contactEmail: z.string().email(),
  modules: z.array(portalModuleSchema).min(1),
})
export type EnablePortalAccessInput = z.infer<typeof enablePortalAccessSchema>

export const sendPortalInviteSchema = z.object({
  email: z.string().email(),
})
export type SendPortalInviteInput = z.infer<typeof sendPortalInviteSchema>

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

export interface CustomerPortalAccessRow {
  customerId: string
  customerName: string
  contactEmail: string | null
  portalStatus: 'active' | 'frozen' | 'none'
  lastLoginAt: string | null
  modulesEnabled: PortalModule[]
  invitedAt: string | null
  acceptedAt: string | null
}

export interface PortalInviteResult {
  token: string
  expiresAt: Date
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function bufToHex(buf: ArrayBuffer): string {
  return Array.from(new Uint8Array(buf))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

function parseModules(raw: unknown): PortalModule[] {
  if (!Array.isArray(raw)) return []
  return raw.filter((m): m is PortalModule =>
    ['invoices', 'projects', 'tickets'].includes(m as string),
  )
}

// ── Read helpers ──────────────────────────────────────────────────────────────

/**
 * List all active customers with their portal access status.
 * Includes contact email, modules enabled, last login, invite timestamps.
 */
export async function listCustomerPortalAccess(
  db: Db,
  tenantId: string,
): Promise<CustomerPortalAccessRow[]> {
  // Fetch all active customers
  const allCustomers = await db
    .select({
      customerId: customers.id,
      customerName: customers.name,
    })
    .from(customers)
    .where(and(eq(customers.tenantId, tenantId), eq(customers.status, 'active')))
    .orderBy(customers.name)

  // Fetch all portal users for this tenant
  const portalUsers = await db
    .select({
      customerId: customerPortalUsers.customerId,
      status: customerPortalUsers.status,
      modulesEnabled: customerPortalUsers.modulesEnabled,
      invitedAt: customerPortalUsers.invitedAt,
      acceptedAt: customerPortalUsers.acceptedAt,
      contactId: customerPortalUsers.contactId,
    })
    .from(customerPortalUsers)
    .where(eq(customerPortalUsers.tenantId, tenantId))

  // Fetch primary contacts for these customers
  const primaryContacts = await db
    .select({
      customerId: customerContacts.customerId,
      email: customerContacts.email,
    })
    .from(customerContacts)
    .where(
      and(
        eq(customerContacts.tenantId, tenantId),
        eq(customerContacts.isPrimary, true),
      ),
    )

  // Build lookup maps
  const portalByCustomer = new Map(portalUsers.map((p) => [p.customerId, p]))
  const contactByCustomer = new Map(primaryContacts.map((c) => [c.customerId, c.email]))

  return allCustomers.map((c) => {
    const portal = portalByCustomer.get(c.customerId)

    return {
      customerId: c.customerId,
      customerName: c.customerName,
      contactEmail: contactByCustomer.get(c.customerId) ?? null,
      portalStatus: portal
        ? (portal.status as 'active' | 'frozen')
        : 'none',
      lastLoginAt: null, // last login tracked via sessions (out of scope for this query)
      modulesEnabled: portal ? parseModules(portal.modulesEnabled) : [],
      invitedAt: portal?.invitedAt?.toISOString() ?? null,
      acceptedAt: portal?.acceptedAt?.toISOString() ?? null,
    }
  })
}

// ── Write helpers ─────────────────────────────────────────────────────────────

/**
 * Enable portal access for a customer contact.
 * Creates a portal user record if none exists; updates if existing.
 * Stores enabled modules in customers.settings JSONB.
 */
export async function enablePortalAccess(
  db: Db,
  tenantId: string,
  actorUserId: string,
  customerId: string,
  modules: PortalModule[],
): Promise<void> {
  // Find primary contact
  const [contact] = await db
    .select({ id: customerContacts.id, email: customerContacts.email })
    .from(customerContacts)
    .where(
      and(
        eq(customerContacts.tenantId, tenantId),
        eq(customerContacts.customerId, customerId),
        eq(customerContacts.isPrimary, true),
      ),
    )
    .limit(1)

  if (!contact) {
    throw new Error('No primary contact found for customer')
  }

  // Find or resolve the user for this contact email
  const [contactUser] = await db
    .select({ id: users.id })
    .from(users)
    .where(eq(users.email, contact.email))
    .limit(1)

  await db.transaction(async (tx) => {
    if (contactUser) {
      // Upsert portal user record with module config
      const existing = await tx
        .select({ id: customerPortalUsers.id })
        .from(customerPortalUsers)
        .where(
          and(
            eq(customerPortalUsers.tenantId, tenantId),
            eq(customerPortalUsers.customerId, customerId),
          ),
        )
        .limit(1)

      if (existing.length > 0) {
        await tx
          .update(customerPortalUsers)
          .set({ status: 'active', modulesEnabled: modules })
          .where(
            and(
              eq(customerPortalUsers.tenantId, tenantId),
              eq(customerPortalUsers.customerId, customerId),
            ),
          )
      } else {
        await tx.insert(customerPortalUsers).values({
          customerId,
          tenantId,
          contactId: contact.id,
          userId: contactUser.id,
          portalRole: 'customer_viewer',
          status: 'active',
          modulesEnabled: modules,
          invitedAt: new Date(),
        })
      }
    }

    await tx.insert(auditLog).values({
      tenantId,
      actorId: actorUserId,
      actorType: 'user',
      entityType: 'customer_portal_access',
      entityId: customerId,
      action: 'enable_portal_access',
      changes: { modules: [null, modules] },
    })
  })
}

/**
 * Revoke portal access for a customer (sets status='frozen').
 */
export async function disablePortalAccess(
  db: Db,
  tenantId: string,
  actorUserId: string,
  customerId: string,
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx
      .update(customerPortalUsers)
      .set({ status: 'frozen', modulesEnabled: [] })
      .where(
        and(
          eq(customerPortalUsers.tenantId, tenantId),
          eq(customerPortalUsers.customerId, customerId),
        ),
      )

    await tx.insert(auditLog).values({
      tenantId,
      actorId: actorUserId,
      actorType: 'user',
      entityType: 'customer_portal_access',
      entityId: customerId,
      action: 'disable_portal_access',
      changes: {},
    })
  })
}

/**
 * Generate a magic-link invite token for a customer portal user.
 * Returns the plaintext token (store hash only in DB — handled by caller).
 * Token is valid for 48 hours.
 */
export async function sendPortalInvite(
  db: Db,
  tenantId: string,
  customerId: string,
  email: string,
): Promise<PortalInviteResult> {
  const rawBytes = crypto.getRandomValues(new Uint8Array(32))
  const plaintext = bufToHex(rawBytes.buffer as ArrayBuffer)
  const hashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(plaintext))
  const hash = bufToHex(hashBuf)
  const expiresAt = new Date(Date.now() + 48 * 60 * 60 * 1000)

  await db.transaction(async (tx) => {
    // Mark invite sent
    await tx
      .update(customerPortalUsers)
      .set({ invitedAt: new Date() })
      .where(
        and(
          eq(customerPortalUsers.tenantId, tenantId),
          eq(customerPortalUsers.customerId, customerId),
        ),
      )

    // Store hashed token in audit log for traceability
    await tx.insert(auditLog).values({
      tenantId,
      actorId: 'system',
      actorType: 'system',
      entityType: 'customer_portal_invite',
      entityId: customerId,
      action: 'portal_invite_sent',
      changes: { email: [null, email], tokenHash: [null, hash], expiresAt: [null, expiresAt.toISOString()] },
    })
  })

  return { token: plaintext, expiresAt }
}
