/**
 * Customer portal auth routes — tenant-portals (wave 9c, Task 4).
 *
 *   POST /api/portal/auth/login
 *   POST /api/portal/auth/magic
 *   GET  /api/portal/auth/magic/verify
 *   POST /api/portal/auth/refresh   (portalSessionAuthMiddleware)
 *   POST /api/portal/auth/logout    (portalSessionAuthMiddleware)
 */
import { Hono, type Context } from 'hono'
import { deleteCookie, setCookie } from 'hono/cookie'
import {
  createPortalSession,
  generateOpaqueToken,
  hashToken,
  PORTAL_COOKIE_NAME,
  PORTAL_COOKIE_OPTS,
  PORTAL_SESSION_TTL_SECONDS,
  parsePortalCookie,
  revokePortalSession,
  rotatePortalSession,
  signPortalSession,
  timingSafeEqual,
  verifyPassword,
  verifyPortalToken,
} from '@zync/auth'
import { and, eq, gt, isNull, sql } from '@zync/db'
import { createDb } from '@zync/db/queries'
import {
  customerContacts,
  customerPortalUsers,
  magicLinkTokens,
  tenantSettings,
  tenants,
  users,
} from '@zync/db/schema'
import { sendEmail } from '@zync/notifications'
import type { AppEnv } from '../../types'
import {
  portalSessionAuthMiddleware,
  type PortalAuthVariables,
} from '../../middleware/portalAuth'
import { getAppOrigins } from '../../lib/origins'
import { withDoHash } from '../../lib/password-hash-do'
import { portalLoginSchema, portalMagicSchema } from '../../schemas/portalAuth'

const MAGIC_LINK_TTL_MS = 60 * 60 * 1000
const REFRESH_THRESHOLD_SECONDS = 1800
const DEFAULT_MAX_SESSION_HOURS = 24

type PortalAuthEnv = {
  Bindings: AppEnv['Bindings']
  Variables: PortalAuthVariables
}

export const portalAuthRoutes = new Hono<PortalAuthEnv>()

function checkOrigin(originHeader: string | undefined, environment: string | undefined): boolean {
  const allowedOrigins = getAppOrigins({ environment })
  return !!originHeader && allowedOrigins.has(originHeader)
}

function appBaseUrl(env: AppEnv['Bindings']): string {
  return env.APP_BASE_URL ?? 'https://app.zync.is'
}

async function resolveTenantBySlug(db: ReturnType<typeof createDb>, slug: string) {
  const [row] = await db
    .select({ id: tenants.id, slug: tenants.slug })
    .from(tenants)
    .where(eq(tenants.slug, slug))
    .limit(1)
  return row ?? null
}

async function getPortalMaxSessionHours(
  db: ReturnType<typeof createDb>,
  tenantId: string,
): Promise<number> {
  const [row] = await db
    .select({
      hours: sql<number>`coalesce(portal_max_session_hours, ${DEFAULT_MAX_SESSION_HOURS})`.mapWith(
        Number,
      ),
    })
    .from(tenantSettings)
    .where(eq(tenantSettings.tenantId, tenantId))
    .limit(1)
  return row?.hours ?? DEFAULT_MAX_SESSION_HOURS
}

async function findActivePortalUserByEmail(
  db: ReturnType<typeof createDb>,
  tenantId: string,
  email: string,
) {
  const [row] = await db
    .select({
      customerId: customerPortalUsers.customerId,
      userId: customerPortalUsers.userId,
      portalRole: customerPortalUsers.portalRole,
      passwordHash: users.passwordHash,
      contactEmail: customerContacts.email,
    })
    .from(customerPortalUsers)
    .innerJoin(customerContacts, eq(customerContacts.id, customerPortalUsers.contactId))
    .innerJoin(users, eq(users.id, customerPortalUsers.userId))
    .where(
      and(
        eq(customerPortalUsers.tenantId, tenantId),
        eq(customerContacts.email, email),
        eq(customerPortalUsers.status, 'active'),
      ),
    )
    .limit(1)
  return row ?? null
}

async function mintPortalSession(
  c: Context<PortalAuthEnv>,
  db: ReturnType<typeof createDb>,
  args: {
    tenantId: string
    customerId: string
    userId: string
    portalRole: string
    maxSessionHours: number
    sessionStartedAt?: number
  },
): Promise<{ jwt: string; expiresAt: Date; ttlSeconds: number } | null> {
  const nowSec = Math.floor(Date.now() / 1000)
  const sessionStartedAt = args.sessionStartedAt ?? nowSec
  const maxSeconds = args.maxSessionHours * 3600
  const elapsed = nowSec - sessionStartedAt
  const remainingBudget = maxSeconds - elapsed
  if (remainingBudget <= 0) return null

  const ttlSeconds = Math.min(PORTAL_SESSION_TTL_SECONDS, remainingBudget)
  const expiresAt = new Date(Date.now() + ttlSeconds * 1000)
  const portalSessionId = crypto.randomUUID()

  const jwt = await signPortalSession(
    {
      sub: args.userId,
      tenantId: args.tenantId,
      customerId: args.customerId,
      userId: args.userId,
      portalRole: args.portalRole,
      portalSessionId,
      sessionStartedAt,
    },
    c.env.JWT_SECRET,
    ttlSeconds,
  )

  await createPortalSession(db, {
    tenantId: args.tenantId,
    customerId: args.customerId,
    userId: args.userId,
    jwt,
    expiresAt,
  })

  return { jwt, expiresAt, ttlSeconds }
}

function setPortalSessionCookie(c: Context<PortalAuthEnv>, jwt: string, ttlSeconds: number): void {
  setCookie(c, PORTAL_COOKIE_NAME, jwt, {
    ...PORTAL_COOKIE_OPTS,
    maxAge: ttlSeconds,
  })
}

portalAuthRoutes.post('/login', async (c) => {
  if (!checkOrigin(c.req.header('Origin'), (c.env as { ENVIRONMENT?: string }).ENVIRONMENT)) {
    return c.json({ error: 'Forbidden: invalid Origin' }, 403)
  }

  const parsed = portalLoginSchema.safeParse(await c.req.json().catch(() => null))
  if (!parsed.success) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const { tenantSlug, email, password } = parsed.data
  const db = createDb(c.env)
  const tenant = await resolveTenantBySlug(db, tenantSlug)
  if (!tenant) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const portalUser = await findActivePortalUserByEmail(db, tenant.id, email)
  if (!portalUser) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const passwordOk = await verifyPassword(password, portalUser.passwordHash, withDoHash(c.env))
  if (!passwordOk) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const maxSessionHours = await getPortalMaxSessionHours(db, tenant.id)
  const session = await mintPortalSession(c, db, {
    tenantId: tenant.id,
    customerId: portalUser.customerId,
    userId: portalUser.userId,
    portalRole: portalUser.portalRole,
    maxSessionHours,
  })
  if (!session) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  setPortalSessionCookie(c, session.jwt, session.ttlSeconds)

  return c.json({
    customerId: portalUser.customerId,
    tenantSlug: tenant.slug,
    expiresAt: session.expiresAt.toISOString(),
  })
})

portalAuthRoutes.post('/magic', async (c) => {
  if (!checkOrigin(c.req.header('Origin'), (c.env as { ENVIRONMENT?: string }).ENVIRONMENT)) {
    return c.json({ error: 'Forbidden: invalid Origin' }, 403)
  }

  const parsed = portalMagicSchema.safeParse(await c.req.json().catch(() => null))
  if (!parsed.success) {
    return c.body(null, 204)
  }

  const { tenantSlug, email } = parsed.data
  const db = createDb(c.env)
  const tenant = await resolveTenantBySlug(db, tenantSlug)
  if (!tenant) {
    return c.body(null, 204)
  }

  const portalUser = await findActivePortalUserByEmail(db, tenant.id, email)
  if (portalUser) {
    const plainToken = generateOpaqueToken()
    const tokenHash = await hashToken(plainToken)
    const expiresAt = new Date(Date.now() + MAGIC_LINK_TTL_MS)

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

    const link = `${appBaseUrl(c.env)}/portal/${tenantSlug}/magic?token=${encodeURIComponent(plainToken)}`
    const locale: 'he-IL' | 'en-US' = 'he-IL'

    await sendEmail(
      {
        to: email,
        templateKey: 'timer-magic-link',
        vars: {
          subject: locale === 'he-IL' ? 'כניסה לפורטל הלקוחות' : 'Customer portal sign-in',
          title: locale === 'he-IL' ? 'כניסה לפורטל הלקוחות' : 'Customer portal sign-in',
          body:
            locale === 'he-IL'
              ? 'לחצו על הכפתור כדי להיכנס לפורטל הלקוחות.'
              : 'Click the button to sign in to the customer portal.',
          link,
          ctaLabel: locale === 'he-IL' ? 'כניסה לפורטל' : 'Sign in',
          expiryNote: locale === 'he-IL' ? 'הקישור תקף לשעה אחת.' : 'This link expires in 1 hour.',
        },
        locale,
      },
      c.env,
    )
  }

  return c.body(null, 204)
})

portalAuthRoutes.get('/magic/verify', async (c) => {
  const token = c.req.query('token')
  const tenantSlug = c.req.query('tenantSlug')
  if (!token || token.length < 10 || !tenantSlug) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = createDb(c.env)
  const tenant = await resolveTenantBySlug(db, tenantSlug)
  if (!tenant) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const tokenHash = await hashToken(token)
  const [tokenRow] = await db
    .select({
      id: magicLinkTokens.id,
      tokenHash: magicLinkTokens.tokenHash,
      email: magicLinkTokens.email,
      tenantId: magicLinkTokens.tenantId,
    })
    .from(magicLinkTokens)
    .where(
      and(
        eq(magicLinkTokens.tokenHash, tokenHash),
        eq(magicLinkTokens.purpose, 'portal'),
        eq(magicLinkTokens.tenantId, tenant.id),
        isNull(magicLinkTokens.usedAt),
        gt(magicLinkTokens.expiresAt, new Date()),
      ),
    )
    .limit(1)

  if (!tokenRow || !timingSafeEqual(tokenHash, tokenRow.tokenHash)) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const consumed = await db
    .update(magicLinkTokens)
    .set({ usedAt: new Date() })
    .where(and(eq(magicLinkTokens.id, tokenRow.id), isNull(magicLinkTokens.usedAt)))
    .returning({ id: magicLinkTokens.id })

  if (consumed.length === 0 || !tokenRow.email) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const portalUser = await findActivePortalUserByEmail(db, tenant.id, tokenRow.email)
  if (!portalUser) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const maxSessionHours = await getPortalMaxSessionHours(db, tenant.id)
  const session = await mintPortalSession(c, db, {
    tenantId: tenant.id,
    customerId: portalUser.customerId,
    userId: portalUser.userId,
    portalRole: portalUser.portalRole,
    maxSessionHours,
  })
  if (!session) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  setPortalSessionCookie(c, session.jwt, session.ttlSeconds)
  return c.redirect(`${appBaseUrl(c.env)}/portal/${tenantSlug}/`, 302)
})

portalAuthRoutes.post('/refresh', portalSessionAuthMiddleware, async (c) => {
  const token = parsePortalCookie(c.req.header('Cookie'))
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  let payload
  try {
    payload = await verifyPortalToken(token, c.env.JWT_SECRET)
  } catch {
    return c.json({ code: 'portal_session_expired', error: 'Unauthorized' }, 401)
  }

  const nowSec = Math.floor(Date.now() / 1000)
  if (payload.exp - nowSec >= REFRESH_THRESHOLD_SECONDS) {
    return c.body(null, 204)
  }

  const portal = c.get('portal')
  const db = c.get('db')
  const maxSessionHours = await getPortalMaxSessionHours(db, portal.tenantId)
  const sessionStartedAt =
    typeof payload.sessionStartedAt === 'number'
      ? payload.sessionStartedAt
      : payload.iat

  const maxSeconds = maxSessionHours * 3600
  if (nowSec - sessionStartedAt >= maxSeconds) {
    return c.json({ code: 'portal_session_expired', error: 'Unauthorized' }, 401)
  }

  const remainingBudget = maxSeconds - (nowSec - sessionStartedAt)
  const ttlSeconds = Math.min(PORTAL_SESSION_TTL_SECONDS, remainingBudget)
  const expiresAt = new Date(Date.now() + ttlSeconds * 1000)
  const portalSessionId = crypto.randomUUID()

  const jwt = await signPortalSession(
    {
      sub: portal.userId,
      tenantId: portal.tenantId,
      customerId: portal.customerId,
      userId: portal.userId,
      portalRole: payload.portalRole,
      portalSessionId,
      sessionStartedAt,
    },
    c.env.JWT_SECRET,
    ttlSeconds,
  )

  await rotatePortalSession(db, {
    oldSessionId: portal.portalSessionId,
    tenantId: portal.tenantId,
    customerId: portal.customerId,
    userId: portal.userId,
    jwt,
    expiresAt,
  })

  setPortalSessionCookie(c, jwt, ttlSeconds)
  return c.body(null, 204)
})

portalAuthRoutes.post('/logout', portalSessionAuthMiddleware, async (c) => {
  const portal = c.get('portal')
  const db = c.get('db')

  await revokePortalSession(db, portal.portalSessionId)

  deleteCookie(c, PORTAL_COOKIE_NAME, {
    domain: PORTAL_COOKIE_OPTS.domain,
    path: PORTAL_COOKIE_OPTS.path,
  })

  return c.body(null, 204)
})
