/**
 * Outlook (Microsoft Graph) OAuth routes — calendar-module.
 * Mounted at /api/auth/outlook-calendar (via router.ts).
 *
 * GET /start      Build consent URL, store state nonce in KV, redirect user
 * GET /callback   Validate state, exchange code, encrypt tokens, upsert connection
 */
import { Hono } from 'hono'
import type { Context } from 'hono'
import type { AppEnv } from '../../types'
import { authMiddleware } from '../../middleware/auth'
import { upsertCalendarConnection } from '@zync/db/queries'
import {
  buildOutlookOAuthUrl,
  exchangeOutlookCode,
  OutlookCalendarProvider,
} from '@zync/calendar'
import { outlookClientStateKey } from '../../lib/outlook-calendar-webhook'
import { encryptToken } from '@zync/calendar/server'

export const oauthOutlookRoute = new Hono<AppEnv>()
oauthOutlookRoute.use('/init', authMiddleware)
oauthOutlookRoute.use('/start', authMiddleware)

const NONCE_TTL_SECONDS = 600

// ── GET /start ────────────────────────────────────────────────────────────────

async function handleInit(c: Context<AppEnv>) {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const clientId = c.env.MICROSOFT_OAUTH_CLIENT_ID
  if (!clientId) {
    return c.json({ error: 'Outlook Calendar integration not configured' }, 500)
  }

  const nonce = crypto.randomUUID()
  const statePayload = JSON.stringify({ nonce, userId: session.sub, tenantId: session.tid })
  const state = btoa(statePayload)

  const kv = c.env.KV
  if (!kv) {
    return c.json({ error: 'KV not configured' }, 500)
  }
  await kv.put(`oauth_state:outlook:${nonce}`, statePayload, { expirationTtl: NONCE_TTL_SECONDS })

  const redirectUri = `${new URL(c.req.url).origin}/api/auth/outlook/callback`
  const url = buildOutlookOAuthUrl(clientId, redirectUri, state)

  return c.json({ url }, 200)
}

oauthOutlookRoute.get('/init', handleInit)
oauthOutlookRoute.get('/start', handleInit)

// ── GET /callback ─────────────────────────────────────────────────────────────

oauthOutlookRoute.get('/callback', async (c) => {
  const url = new URL(c.req.url)
  const code = url.searchParams.get('code')
  const stateParam = url.searchParams.get('state')
  const errorParam = url.searchParams.get('error')

  if (errorParam) {
    return c.redirect(`/settings/integrations/calendar?error=${encodeURIComponent(errorParam)}`, 302)
  }

  if (!code || !stateParam) {
    return c.json({ error: 'Missing code or state' }, 400)
  }

  let statePayload: { nonce: string; userId: string; tenantId: string }
  try {
    statePayload = JSON.parse(atob(stateParam)) as typeof statePayload
  } catch {
    return c.json({ error: 'Invalid state' }, 400)
  }

  const kv = c.env.KV
  if (!kv) {
    return c.json({ error: 'KV not configured' }, 500)
  }

  const stored = await kv.get(`oauth_state:outlook:${statePayload.nonce}`)
  if (!stored) {
    return c.json({ error: 'State expired or invalid' }, 400)
  }
  await kv.delete(`oauth_state:outlook:${statePayload.nonce}`)

  // The KV-stored payload is authoritative — it was written server-side at /start
  // from the authenticated session. The URL state param is attacker-controllable;
  // never trust its userId/tenantId for the connection binding.
  let verified: { nonce: string; userId: string; tenantId: string }
  try {
    verified = JSON.parse(stored) as typeof verified
  } catch {
    return c.json({ error: 'Corrupt state' }, 400)
  }
  if (verified.nonce !== statePayload.nonce) {
    return c.json({ error: 'State mismatch' }, 400)
  }

  const clientId = c.env.MICROSOFT_OAUTH_CLIENT_ID
  const clientSecret = c.env.MICROSOFT_OAUTH_CLIENT_SECRET
  const encryptionKey = c.env.INTEGRATION_ENCRYPTION_KEY

  if (!clientId || !clientSecret || !encryptionKey) {
    return c.json({ error: 'Outlook Calendar integration not configured' }, 500)
  }

  const redirectUri = `${url.origin}/api/auth/outlook/callback`

  try {
    const tokens = await exchangeOutlookCode(code, clientId, clientSecret, redirectUri)
    const connectedEmail = await fetchOutlookEmail(tokens.accessToken)

    const [encryptedAccess, encryptedRefresh] = await Promise.all([
      encryptToken(tokens.accessToken, encryptionKey),
      encryptToken(tokens.refreshToken, encryptionKey),
    ])

    const db = c.get('db')
    const conn = await upsertCalendarConnection(db, {
      tenantId: verified.tenantId,
      userId: verified.userId,
      provider: 'outlook',
      externalUserId: tokens.oid,
      connectedEmail,
      accessToken: encryptedAccess,
      refreshToken: encryptedRefresh,
      tokenExpiresAt: new Date(tokens.expiresAt),
      syncDirection: 'two_way',
      syncTaskDueDates: true,
      syncManualEvents: true,
      syncCustomerMeetings: false,
      syncEnabled: true,
      status: 'active',
      lastSyncError: null,
    })

    const clientState = crypto.randomUUID()
    await kv.put(outlookClientStateKey(conn.id), clientState)

    const callbackUrl = `${url.origin}/api/webhooks/calendar/outlook/${conn.id}`
    const provider = new OutlookCalendarProvider(clientId, clientSecret)
    try {
      await provider.registerWatch(tokens.accessToken, '', callbackUrl, { clientState })
    } catch (registerErr) {
      console.warn('[oauth-outlook] registerWatch failed', {
        connId: conn.id,
        err: registerErr instanceof Error ? registerErr.message : String(registerErr),
      })
    }

    return c.redirect('/settings/integrations/calendar?connected=outlook', 302)
  } catch (e) {
    const msg = e instanceof Error ? e.message : 'Unknown error'
    return c.redirect(`/settings/integrations/calendar?error=${encodeURIComponent(msg)}`, 302)
  }
})

async function fetchOutlookEmail(accessToken: string): Promise<string | null> {
  const res = await fetch('https://graph.microsoft.com/v1.0/me?$select=mail,userPrincipalName', {
    headers: { Authorization: `Bearer ${accessToken}` },
  })
  if (!res.ok) {
    return null
  }

  const body = (await res.json()) as { mail?: string | null; userPrincipalName?: string | null }
  return body.mail ?? body.userPrincipalName ?? null
}
