import { createHash } from 'node:crypto'
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { speechTokenBodySchema } from '@/lib/api/contracts/media/speech'
import { parseOptionalJsonBody } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor'
import { recordUsage } from '@/lib/billing/core/usage-log'
import { env } from '@/lib/core/config/env'
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/env-flags'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { validateAuthToken } from '@/lib/core/security/deployment'
import { getClientIp } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'

const logger = createLogger('SpeechTokenAPI')

export const dynamic = 'force-dynamic'

const ELEVENLABS_TOKEN_URL = 'https://api.elevenlabs.io/v1/single-use-token/realtime_scribe'

const VOICE_SESSION_COST_PER_MIN = 0.008
const WORKSPACE_SESSION_MAX_MINUTES = 3
const CHAT_SESSION_MAX_MINUTES = 1

const STT_TOKEN_RATE_LIMIT = {
  maxTokens: 30,
  refillRate: 3,
  refillIntervalMs: 72 * 1000,
} as const

/**
 * This body only ever carries an optional chatId/workspaceId string, so a
 * tight cap keeps an unauthenticated caller from forcing a large in-memory
 * allocation before the auth checks below run.
 */
const MAX_SPEECH_TOKEN_BODY_BYTES = 16 * 1024

function hashVoiceToken(token: string): string {
  return createHash('sha256').update(token).digest('hex')
}

const rateLimiter = new RateLimiter()

async function validateChatAuth(
  request: NextRequest,
  chatId: string
): Promise<{ valid: boolean; ownerId?: string; workspaceId?: string | null }> {
  try {
    const chatResult = await db
      .select({
        id: chat.id,
        userId: chat.userId,
        isActive: chat.isActive,
        authType: chat.authType,
        password: chat.password,
        workspaceId: workflow.workspaceId,
      })
      .from(chat)
      .leftJoin(workflow, eq(workflow.id, chat.workflowId))
      .where(eq(chat.id, chatId))
      .limit(1)

    if (chatResult.length === 0 || !chatResult[0].isActive) {
      return { valid: false }
    }

    const chatData = chatResult[0]

    if (chatData.authType === 'public') {
      return { valid: true, ownerId: chatData.userId, workspaceId: chatData.workspaceId }
    }

    const cookieName = `chat_auth_${chatId}`
    const authCookie = request.cookies.get(cookieName)
    if (
      authCookie &&
      validateAuthToken(authCookie.value, chatId, chatData.authType, chatData.password)
    ) {
      return { valid: true, ownerId: chatData.userId, workspaceId: chatData.workspaceId }
    }

    return { valid: false }
  } catch (error) {
    logger.error('Error validating chat auth for STT:', error)
    return { valid: false }
  }
}

export const POST = withRouteHandler(async (request: NextRequest) => {
  try {
    const parsedBody = await parseOptionalJsonBody(request, MAX_SPEECH_TOKEN_BODY_BYTES)
    if (!parsedBody.success) return parsedBody.response
    const body = speechTokenBodySchema.safeParse(parsedBody.data ?? {})
    const chatId =
      body.success && typeof body.data.chatId === 'string' ? body.data.chatId : undefined

    let billingUserId: string | undefined
    let workspaceId: string | undefined

    if (chatId) {
      const chatAuth = await validateChatAuth(request, chatId)
      if (!chatAuth.valid) {
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
      }
      // A deployed chat is used by anonymous end-users, so the cost belongs to the
      // workspace's billed account (the deployment's payer) — matching how the
      // chat's workflow execution bills. Fall back to the chat owner only when no
      // billed account resolves.
      workspaceId = chatAuth.workspaceId ?? undefined
      const billedAccountUserId = workspaceId
        ? await getWorkspaceBilledAccountUserId(workspaceId)
        : null
      billingUserId = billedAccountUserId ?? chatAuth.ownerId
    } else {
      const session = await getSession()
      if (!session?.user?.id) {
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
      }
      billingUserId = session.user.id
      // Editor voice: only attribute to a workspace the caller actually belongs to,
      // so a client-supplied id can't misattribute (or dodge) per-member usage.
      const requestedWorkspaceId =
        body.success && typeof body.data.workspaceId === 'string'
          ? body.data.workspaceId
          : undefined
      if (requestedWorkspaceId) {
        const permission = await verifyWorkspaceMembership(session.user.id, requestedWorkspaceId)
        if (permission) workspaceId = requestedWorkspaceId
      }
      // Editor voice is always workspace-scoped; require an attributable workspace
      // so per-member usage can't be skipped and the cost stamped workspace-less.
      if (!workspaceId) {
        return NextResponse.json({ error: 'Workspace context is required.' }, { status: 400 })
      }
    }

    if (isBillingEnabled) {
      const rateLimitKey = chatId
        ? `stt-token:chat:${chatId}:${getClientIp(request)}`
        : `stt-token:user:${billingUserId}`

      const rateCheck = await rateLimiter.checkRateLimitDirect(rateLimitKey, STT_TOKEN_RATE_LIMIT)
      if (!rateCheck.allowed) {
        return NextResponse.json(
          { error: 'Voice input rate limit exceeded. Please try again later.' },
          {
            status: 429,
            headers: {
              'Retry-After': String(Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000)),
            },
          }
        )
      }
    }

    if (billingUserId) {
      const usageCheck = await checkActorUsageLimits(billingUserId, workspaceId)
      if (usageCheck.isExceeded) {
        return NextResponse.json(
          {
            error:
              usageCheck.message || 'Usage limit exceeded. Please upgrade your plan to continue.',
            scope: usageCheck.scope,
          },
          { status: 402 }
        )
      }
    }

    const apiKey = env.ELEVENLABS_API_KEY
    if (!apiKey?.trim()) {
      return NextResponse.json(
        { error: 'Speech-to-text service is not configured' },
        { status: 503 }
      )
    }

    const response = await fetch(ELEVENLABS_TOKEN_URL, {
      method: 'POST',
      headers: { 'xi-api-key': apiKey },
    })

    if (!response.ok) {
      const errBody = await response.json().catch(() => ({}))
      const message =
        errBody.detail || errBody.message || `Token request failed (${response.status})`
      logger.error('ElevenLabs token request failed', { status: response.status, message })
      return NextResponse.json({ error: message }, { status: 502 })
    }

    const data = await response.json()

    if (billingUserId) {
      const maxMinutes = chatId ? CHAT_SESSION_MAX_MINUTES : WORKSPACE_SESSION_MAX_MINUTES
      const sessionCost = VOICE_SESSION_COST_PER_MIN * maxMinutes

      await recordUsage({
        userId: billingUserId,
        workspaceId,
        entries: [
          {
            category: 'fixed',
            source: 'voice-input',
            description: `Voice input session (${maxMinutes} min)`,
            cost: sessionCost * getCostMultiplier(),
            sourceReference: `voice-input:${hashVoiceToken(data.token)}`,
          },
        ],
      }).catch((err) => {
        logger.warn('Failed to record voice input usage, continuing:', err)
      })
    }

    return NextResponse.json({ token: data.token })
  } catch (error) {
    const message = getErrorMessage(error, 'Failed to generate speech token')
    logger.error('Speech token error:', error)
    return NextResponse.json({ error: message }, { status: 500 })
  }
})
