/**
 * Session issuance — foundation-auth-rbac (Task 12).
 *
 * Shared by login / verify-email / switch-tenant / refresh: builds the
 * SessionPayload from a (user, tenant, role, permissions) tuple, signs the
 * access JWT, mints + stores a rotating refresh token (hash only), and sets
 * both cookies. Returns the access-token expiry for the JSON body.
 */
import type { Context } from 'hono'
import {
  buildSessionPayload,
  generateOpaqueToken,
  hashToken,
  recordSessionOnLogin,
  signSession,
  SESSION_TTL_SECONDS,
} from '@zync/auth'
import {
  countActiveSessions,
  getPermissionsForRole,
  getPrimaryMembership,
  getMembershipView,
  getTenantSecuritySettings,
  insertRefreshToken,
  type Db,
} from '@zync/db/queries'
import type { TenantId, TenantTier, UserId } from '@zync/types'
import type { AppEnv } from '../types'
import { getUserVersion } from '../middleware/user-version'
import { refreshExpiry, setRefreshCookie, setSessionCookie } from './cookies'
import { loadTwoFactorOptsForTenant, type TwoFactorOpts } from './session-two-factor'

interface MembershipView {
  tenantId: string
  tenantSlug: string
  tier: string
  roleId: string
  role: string
}

export class SessionCapExceededError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'SessionCapExceededError'
  }
}

async function issueForMembership(
  c: Context<AppEnv>,
  db: Db,
  userId: UserId,
  m: MembershipView,
  twoFactor?: TwoFactorOpts,
): Promise<{ expiresAt: number }> {
  const cap = await assertSessionCapAllowsLogin(db, userId, m.tenantId as TenantId)
  if (!cap.allowed) {
    throw new SessionCapExceededError(cap.message)
  }

  const permissions = await getPermissionsForRole(db, m.roleId)
  const version = await getUserVersion(c.env, userId)
  const payload = buildSessionPayload({
    user: { id: userId },
    tenant: { id: m.tenantId as TenantId, tier: m.tier as TenantTier, slug: m.tenantSlug },
    role: m.role,
    permissions,
    version,
    type: 'user',
    enforce2fa: twoFactor?.enforce2fa ?? false,
    twoFactorVerified: twoFactor?.twoFactorVerified ?? false,
  })
  const accessToken = await signSession(payload, c.env.JWT_SECRET)
  const expiresAtDate = new Date(Date.now() + SESSION_TTL_SECONDS * 1000)

  await recordSessionOnLogin(db, {
    tenantId: m.tenantId,
    userId,
    accessToken,
    userAgent: c.req.header('User-Agent'),
    ip: c.req.header('CF-Connecting-IP'),
    countryCode: c.req.header('CF-IPCountry'),
    expiresAt: expiresAtDate,
  })

  const refreshPlain = generateOpaqueToken()
  const refreshHash = await hashToken(refreshPlain)
  await insertRefreshToken(db, {
    userId,
    tenantId: m.tenantId as TenantId,
    tokenHash: refreshHash,
    expiresAt: refreshExpiry(),
  })

  setSessionCookie(c, accessToken)
  setRefreshCookie(c, refreshPlain)
  return { expiresAt: Date.now() + SESSION_TTL_SECONDS * 1000 }
}

/** Issue a session for the user's primary active membership. Null if none. */
export async function issueSessionForUser(
  c: Context<AppEnv>,
  db: Db,
  userId: UserId,
): Promise<{ expiresAt: number } | null> {
  const m = await getPrimaryMembership(db, userId)
  if (!m) return null
  return issueForMembership(c, db, userId, m)
}

/** Issue a session for a SPECIFIC tenant the user is an active member of. */
export async function issueSessionForTenant(
  c: Context<AppEnv>,
  db: Db,
  userId: UserId,
  tenantId: TenantId,
): Promise<{ expiresAt: number } | null> {
  const m = await getMembershipView(db, userId, tenantId)
  if (!m || m.status !== 'active') return null
  const twoFactor = await loadTwoFactorOptsForTenant(db, tenantId)
  return issueForMembership(c, db, userId, m, twoFactor)
}

/**
 * Issue a session for a SPECIFIC tenant with 2FA flags set.
 * Used by auth-2fa routes after second-factor verification.
 */
export async function issueSessionForTenantWith2FA(
  c: Context<AppEnv>,
  db: Db,
  userId: UserId,
  tenantId: TenantId,
  twoFactor: TwoFactorOpts,
): Promise<{ expiresAt: number } | null> {
  const m = await getMembershipView(db, userId, tenantId)
  if (!m || m.status !== 'active') return null
  return issueForMembership(c, db, userId, m, twoFactor)
}

/** Reject login when the user is at the tenant session cap. */
export async function assertSessionCapAllowsLogin(
  db: Db,
  userId: UserId,
  tenantId: TenantId,
): Promise<{ allowed: true } | { allowed: false; message: string }> {
  const settings = await getTenantSecuritySettings(db, tenantId)
  const activeCount = await countActiveSessions(db, userId, tenantId)
  if (activeCount >= settings.maxSessionsPerUser) {
    return {
      allowed: false,
      message: 'Maximum active sessions reached. Sign out of another device to continue.',
    }
  }
  return { allowed: true }
}
