import crypto from 'node:crypto'
import { normalizeEmail } from '../lib/auth.js'
import { generateAvatar } from '../lib/generate-avatar.js'
import { prisma } from '../lib/prisma.js'

const pkceStore = new Map()

function storePkce(state, codeVerifier, meta = {}) {
  pkceStore.set(state, { codeVerifier, meta, createdAt: Date.now() })
}

function consumePkce(state) {
  const entry = pkceStore.get(state)
  if (entry) {
    pkceStore.delete(state)
    return { codeVerifier: entry.codeVerifier, meta: entry.meta }
  }
  return null
}

function generatePkcePair() {
  const verifier = crypto.randomBytes(48).toString('base64url').slice(0, 64)
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
  return { codeVerifier: verifier, codeChallenge: challenge }
}

function decodeIdTokenPayload(idToken) {
  const parts = idToken.split('.')
  if (parts.length !== 3) return null
  try {
    return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
  } catch {
    return null
  }
}

function clientRedirect(fastify, reply, token) {
  const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'
  const url = `${base.replace(/\/$/, '')}/auth/callback?token=${encodeURIComponent(token)}`
  return reply.redirect(url)
}

function oauthErrorRedirect(reply, msg) {
  const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'
  const url = `${base.replace(/\/$/, '')}/auth?oauthError=${encodeURIComponent(msg)}`
  return reply.redirect(url)
}

async function issueUserJwt(fastify, userId, email) {
  return fastify.jwt.sign({ sub: userId, email })
}

async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken, suggestedEmail, linkToUserId }) {
  const existingLink = await prisma.oAuthAccount.findUnique({
    where: { provider_providerUserId: { provider, providerUserId } },
    include: { user: true },
  })
  if (existingLink?.user) {
    if (accessToken !== undefined) {
      await prisma.oAuthAccount.update({
        where: { provider_providerUserId: { provider, providerUserId } },
        data: { accessToken },
      })
    }
    return existingLink.user
  }

  const trimmed = typeof suggestedEmail === 'string' ? suggestedEmail.trim() : ''
  const norm = trimmed ? normalizeEmail(trimmed) : null

  if (linkToUserId) {
    await prisma.oAuthAccount.create({
      data: { provider, providerUserId: String(providerUserId), userId: linkToUserId, accessToken },
    })
    return prisma.user.findUnique({ where: { id: linkToUserId } })
  }

  let user = norm ? await prisma.user.findUnique({ where: { email: norm } }) : null
  if (user) {
    await prisma.oAuthAccount.create({
      data: { provider, providerUserId: String(providerUserId), userId: user.id, accessToken },
    })
    return user
  }

  const email = norm || `${provider}_${providerUserId}@vk.local`

  user = await prisma.user.create({
    data: {
      email,
      displayName: norm ? norm.split('@')[0] : 'Пользователь',
      avatar: await generateAvatar(email),
      avatarStyle: 'initials',
    },
  })
  await prisma.oAuthAccount.create({
    data: { provider, providerUserId: String(providerUserId), userId: user.id, accessToken },
  })
  await prisma.notificationPreference.create({
    data: { userId: user.id, globalEnabled: true },
  })
  return user
}

export async function registerOAuthSocialRoutes(fastify) {
  const serverPublic = (process.env.SERVER_PUBLIC_URL || 'http://127.0.0.1:3333').replace(/\/$/, '')

  /** --- VK --- */
  fastify.get('/api/auth/oauth/vk', async (_request, reply) => {
    const clientId = process.env.VK_CLIENT_ID
    const clientSecret = process.env.VK_CLIENT_SECRET
    if (!clientId || !clientSecret) return reply.code(503).send({ error: 'VK OAuth не настроен (нет VK_* в env)' })

    const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
    const { codeVerifier, codeChallenge } = generatePkcePair()
    const state = crypto.randomUUID()
    storePkce(state, codeVerifier)

    const url = new URL('https://id.vk.ru/authorize')
    url.searchParams.set('client_id', clientId)
    url.searchParams.set('redirect_uri', redirectUri)
    url.searchParams.set('response_type', 'code')
    url.searchParams.set('scope', 'email')
    url.searchParams.set('code_challenge', codeChallenge)
    url.searchParams.set('code_challenge_method', 'S256')
    url.searchParams.set('state', state)

    return reply.redirect(url.toString())
  })

  fastify.get('/api/auth/oauth/vk/link', { preHandler: [fastify.authenticate] }, async (request, reply) => {
    const adminEmail = normalizeEmail(process.env.ADMIN_EMAIL)
    if (request.user.email === adminEmail) {
      return reply.code(403).send({ error: 'Администратор не может привязывать OAuth' })
    }

    const clientId = process.env.VK_CLIENT_ID
    const clientSecret = process.env.VK_CLIENT_SECRET
    if (!clientId || !clientSecret) return reply.code(503).send({ error: 'VK OAuth не настроен' })

    const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
    const { codeVerifier, codeChallenge } = generatePkcePair()
    const state = crypto.randomUUID()
    storePkce(state, codeVerifier, { action: 'link', userId: request.user.sub })

    const url = new URL('https://id.vk.ru/authorize')
    url.searchParams.set('client_id', clientId)
    url.searchParams.set('redirect_uri', redirectUri)
    url.searchParams.set('response_type', 'code')
    url.searchParams.set('scope', 'email')
    url.searchParams.set('code_challenge', codeChallenge)
    url.searchParams.set('code_challenge_method', 'S256')
    url.searchParams.set('state', state)

    return reply.redirect(url.toString())
  })

  fastify.get('/api/auth/oauth/vk/callback', async (request, reply) => {
    const query = request.query ?? {}
    if (query.error || query.error_description) {
      return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK'))
    }

    const state = typeof query.state === 'string' ? query.state.trim() : ''
    if (!state) return oauthErrorRedirect(reply, 'Недействительный state OAuth')

    const pkceEntry = consumePkce(state)
    if (!pkceEntry) return oauthErrorRedirect(reply, 'Недействительный state OAuth')

    const code = typeof query.code === 'string' ? query.code.trim() : ''
    if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK')

    const deviceId = typeof query.device_id === 'string' ? query.device_id : null

    const clientId = process.env.VK_CLIENT_ID
    const clientSecret = process.env.VK_CLIENT_SECRET
    const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`

    const body = new URLSearchParams()
    body.set('grant_type', 'authorization_code')
    body.set('client_id', clientId)
    body.set('client_secret', clientSecret)
    body.set('code', code)
    body.set('code_verifier', pkceEntry.codeVerifier)
    body.set('redirect_uri', redirectUri)
    if (deviceId) {
      body.set('device_id', deviceId)
    }

    const tokenRes = await fetch('https://id.vk.ru/oauth2/auth', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    })
    const tokenBody = await tokenRes.json()

    if (tokenBody?.error_description || tokenBody?.error || !tokenRes.ok) {
      return oauthErrorRedirect(reply, tokenBody?.error_description || tokenBody?.error || 'Не удалось обменять код VK')
    }

    const idToken = typeof tokenBody?.id_token === 'string' ? tokenBody.id_token : null
    const claims = idToken ? decodeIdTokenPayload(idToken) : null

    const vkUserId = claims?.sub ?? tokenBody?.user_id
    const emailSuggestion = claims?.email ?? tokenBody?.email ?? null

    if (!vkUserId) return oauthErrorRedirect(reply, 'no_user_id')

    const linkToUserId = pkceEntry.meta?.action === 'link' ? pkceEntry.meta.userId : undefined

    const user = await findOrCreateUserFromOAuth({
      provider: 'vk',
      providerUserId: String(vkUserId),
      accessToken: tokenBody?.access_token ?? null,
      suggestedEmail: emailSuggestion,
      linkToUserId,
    })

    if (linkToUserId) {
      const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'
      return reply.redirect(`${base.replace(/\/$/, '')}/me?linked=vk`)
    }

    const token = await issueUserJwt(fastify, user.id, user.email)
    return clientRedirect(fastify, reply, token)
  })

  /** --- Yandex --- */
  fastify.get('/api/auth/oauth/yandex', async (_request, reply) => {
    const clientId = process.env.YANDEX_CLIENT_ID
    if (!clientId) return reply.code(503).send({ error: 'Yandex OAuth не настроен (нет YANDEX_* в env)' })

    const redirectUri = `${serverPublic}/api/auth/oauth/yandex/callback`
    const state = fastify.jwt.sign({ oauth: 'yandex' }, { expiresIn: '15m' })

    const url = new URL('https://oauth.yandex.ru/authorize')
    url.searchParams.set('response_type', 'code')
    url.searchParams.set('client_id', clientId)
    url.searchParams.set('redirect_uri', redirectUri)
    url.searchParams.set('scope', 'login:email')
    url.searchParams.set('state', state)

    return reply.redirect(url.toString())
  })

  fastify.get('/api/auth/oauth/yandex/link', { preHandler: [fastify.authenticate] }, async (request, reply) => {
    const adminEmail = normalizeEmail(process.env.ADMIN_EMAIL)
    if (request.user.email === adminEmail) {
      return reply.code(403).send({ error: 'Администратор не может привязывать OAuth' })
    }

    const clientId = process.env.YANDEX_CLIENT_ID
    if (!clientId) return reply.code(503).send({ error: 'Yandex OAuth не настроен' })

    const redirectUri = `${serverPublic}/api/auth/oauth/yandex/callback`
    const state = fastify.jwt.sign({ oauth: 'yandex', action: 'link', userId: request.user.sub }, { expiresIn: '15m' })

    const url = new URL('https://oauth.yandex.ru/authorize')
    url.searchParams.set('response_type', 'code')
    url.searchParams.set('client_id', clientId)
    url.searchParams.set('redirect_uri', redirectUri)
    url.searchParams.set('scope', 'login:email')
    url.searchParams.set('state', state)

    return reply.redirect(url.toString())
  })

  fastify.get('/api/auth/oauth/yandex/callback', async (request, reply) => {
    const query = request.query ?? {}
    if (query.error) return oauthErrorRedirect(reply, String(query.error))

    const statePayload = (() => {
      try {
        const raw = typeof query.state === 'string' ? query.state : ''
        return fastify.jwt.verify(raw || '')
      } catch {
        return null
      }
    })()
    if (!statePayload) return oauthErrorRedirect(reply, 'Недействительный state OAuth')

    const code = typeof query.code === 'string' ? query.code.trim() : ''
    if (!code) return oauthErrorRedirect(reply, 'Не получен код от Яндекс')

    const clientId = process.env.YANDEX_CLIENT_ID
    const clientSecret = process.env.YANDEX_CLIENT_SECRET
    const redirectUri = `${serverPublic}/api/auth/oauth/yandex/callback`

    const body = new URLSearchParams()
    body.set('grant_type', 'authorization_code')
    body.set('code', code)
    body.set('client_id', clientId)
    body.set('client_secret', clientSecret)
    if (redirectUri) body.set('redirect_uri', redirectUri)

    const tokenRes = await fetch('https://oauth.yandex.ru/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    })
    const tokenBody = await tokenRes.json()

    if (!tokenRes.ok || !tokenBody.access_token) {
      return oauthErrorRedirect(
        reply,
        tokenBody.error_description || tokenBody.error || 'Не удалось обменять код Yandex',
      )
    }

    const yaToken = tokenBody.access_token

    const infoRes = await fetch('https://login.yandex.ru/info', {
      headers: { Authorization: `OAuth ${yaToken}` },
    })
    const info = await infoRes.json()
    const yaUserId = String(info?.id || '')
    if (!yaUserId) return oauthErrorRedirect(reply, 'Не удалось получить профиль Yandex')

    const emailGuess = (Array.isArray(info?.emails) && info.emails[0]) || info?.default_email || null

    if (!emailGuess) return oauthErrorRedirect(reply, 'no_email')

    const linkToUserId = statePayload?.action === 'link' ? statePayload.userId : undefined

    const user = await findOrCreateUserFromOAuth({
      provider: 'yandex',
      providerUserId: yaUserId,
      accessToken: yaToken,
      suggestedEmail: emailGuess,
      linkToUserId,
    })

    if (!user) return oauthErrorRedirect(reply, 'Не удалось получить email от Яндекс')

    if (linkToUserId) {
      const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'
      return reply.redirect(`${base.replace(/\/$/, '')}/me?linked=yandex`)
    }

    const token = await issueUserJwt(fastify, user.id, user.email)
    return clientRedirect(fastify, reply, token)
  })
}
