import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { executeProviderContract } from '@/lib/api/contracts/providers'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import {
  getServiceAccountToken,
  refreshTokenIfNeeded,
  resolveOAuthAccountId,
} from '@/app/api/auth/oauth/utils'
import {
  assertPermissionsAllowed,
  IntegrationNotAllowedError,
  ModelNotAllowedError,
  ProviderNotAllowedError,
} from '@/ee/access-control/utils/permission-check'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'

const logger = createLogger('ProvidersAPI')

export const dynamic = 'force-dynamic'

/**
 * Server-side proxy for provider requests
 */
export const POST = withRouteHandler(async (request: NextRequest) => {
  const requestId = generateRequestId()
  const startTime = Date.now()

  try {
    const auth = await checkInternalAuth(request, { requireWorkflowId: false })
    if (!auth.success || !auth.userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    logger.info(`[${requestId}] Provider API request started`, {
      timestamp: new Date().toISOString(),
      userAgent: request.headers.get('User-Agent'),
      contentType: request.headers.get('Content-Type'),
    })

    const validation = await parseRequest(
      executeProviderContract,
      request,
      {},
      {
        validationErrorResponse: () =>
          NextResponse.json({ error: 'Invalid request body' }, { status: 400 }),
        invalidJsonResponse: () =>
          NextResponse.json({ error: 'Invalid request body' }, { status: 400 }),
      }
    )
    if (!validation.success) return validation.response

    const body = validation.data.body
    const {
      provider,
      model,
      systemPrompt,
      context,
      tools,
      temperature,
      maxTokens,
      apiKey,
      azureEndpoint,
      azureApiVersion,
      vertexProject,
      vertexLocation,
      vertexCredential,
      bedrockAccessKeyId,
      bedrockSecretKey,
      bedrockRegion,
      responseFormat,
      workflowId,
      workspaceId,
      stream,
      messages,
      environmentVariables,
      workflowVariables,
      blockData,
      blockNameMapping,
      reasoningEffort,
      verbosity,
    } = body

    logger.info(`[${requestId}] Provider request details`, {
      provider,
      model,
      hasSystemPrompt: !!systemPrompt,
      hasContext: !!context,
      hasTools: !!tools?.length,
      toolCount: tools?.length || 0,
      hasApiKey: !!apiKey,
      hasAzureEndpoint: !!azureEndpoint,
      hasAzureApiVersion: !!azureApiVersion,
      hasVertexProject: !!vertexProject,
      hasVertexLocation: !!vertexLocation,
      hasVertexCredential: !!vertexCredential,
      hasBedrockAccessKeyId: !!bedrockAccessKeyId,
      hasBedrockSecretKey: !!bedrockSecretKey,
      hasBedrockRegion: !!bedrockRegion,
      hasResponseFormat: !!responseFormat,
      workflowId,
      stream: !!stream,
      hasMessages: !!messages?.length,
      messageCount: messages?.length || 0,
      hasEnvironmentVariables:
        !!environmentVariables && Object.keys(environmentVariables).length > 0,
      hasWorkflowVariables: !!workflowVariables && Object.keys(workflowVariables).length > 0,
      reasoningEffort,
      verbosity,
    })

    if (workspaceId) {
      const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
      if (!workspaceAccess.hasAccess) {
        return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
      }

      try {
        await assertPermissionsAllowed({
          userId: auth.userId,
          workspaceId,
          model,
        })
      } catch (err) {
        if (
          err instanceof ProviderNotAllowedError ||
          err instanceof ModelNotAllowedError ||
          err instanceof IntegrationNotAllowedError
        ) {
          return NextResponse.json({ error: err.message }, { status: 403 })
        }
        throw err
      }
    }

    let finalApiKey: string | undefined = apiKey
    try {
      if (provider === 'vertex' && vertexCredential) {
        const vertexCredAccess = await authorizeCredentialUse(request, {
          credentialId: vertexCredential,
          workflowId: workflowId || undefined,
          requireWorkflowIdForInternal: false,
        })
        if (!vertexCredAccess.ok) {
          logger.warn(`[${requestId}] Vertex credential access denied`, {
            error: vertexCredAccess.error,
            credentialId: vertexCredential,
          })
          return NextResponse.json(
            { error: vertexCredAccess.error || 'Unauthorized' },
            { status: 401 }
          )
        }
        finalApiKey = await resolveVertexCredential(requestId, vertexCredential)
      }
    } catch (error) {
      logger.error(`[${requestId}] Failed to resolve Vertex credential:`, {
        provider,
        model,
        error: toError(error).message,
        hasVertexCredential: !!vertexCredential,
      })
      return NextResponse.json(
        { error: getErrorMessage(error, 'Credential error') },
        { status: 400 }
      )
    }

    logger.info(`[${requestId}] Executing provider request`, {
      provider,
      model,
      workflowId,
      hasApiKey: !!finalApiKey,
    })

    const response = await executeProviderRequest(provider, {
      model,
      systemPrompt,
      context,
      tools,
      temperature,
      maxTokens,
      apiKey: finalApiKey,
      azureEndpoint,
      azureApiVersion,
      vertexProject,
      vertexLocation,
      bedrockAccessKeyId,
      bedrockSecretKey,
      bedrockRegion,
      responseFormat,
      workflowId,
      workspaceId,
      userId: auth.userId,
      stream,
      messages,
      environmentVariables,
      workflowVariables,
      blockData,
      blockNameMapping,
      reasoningEffort,
      verbosity,
    })

    const executionTime = Date.now() - startTime
    logger.info(`[${requestId}] Provider request completed successfully`, {
      provider,
      model,
      workflowId,
      executionTime,
      responseType:
        response instanceof ReadableStream
          ? 'stream'
          : response && typeof response === 'object' && 'stream' in response
            ? 'streaming-execution'
            : 'json',
    })

    // Check if the response is a StreamingExecution
    if (
      response &&
      typeof response === 'object' &&
      'stream' in response &&
      'execution' in response
    ) {
      const streamingExec = response as StreamingExecution
      logger.info(`[${requestId}] Received StreamingExecution from provider`)

      // Extract the stream and execution data
      const stream = streamingExec.stream
      const executionData = streamingExec.execution

      // Attach the execution data as a custom header
      // We need to safely serialize the execution data to avoid circular references
      let executionDataHeader
      try {
        // Create a safe version of execution data with the most important fields
        const safeExecutionData = {
          success: executionData.success,
          output: {
            // Sanitize content to remove non-ASCII characters that would cause ByteString errors
            content: executionData.output?.content
              ? String(executionData.output.content).replace(/[\u0080-\uFFFF]/g, '')
              : '',
            model: executionData.output?.model,
            tokens: executionData.output?.tokens || {
              input: 0,
              output: 0,
              total: 0,
            },
            // Sanitize any potential Unicode characters in tool calls
            toolCalls: executionData.output?.toolCalls
              ? sanitizeToolCalls(executionData.output.toolCalls)
              : undefined,
            providerTiming: executionData.output?.providerTiming,
            cost: executionData.output?.cost,
          },
          error: executionData.error,
          logs: [], // Strip logs from header to avoid encoding issues
          metadata: {
            startTime: executionData.metadata?.startTime,
            endTime: executionData.metadata?.endTime,
            duration: executionData.metadata?.duration,
          },
          isStreaming: true, // Always mark streaming execution data as streaming
          blockId: executionData.logs?.[0]?.blockId,
          blockName: executionData.logs?.[0]?.blockName,
          blockType: executionData.logs?.[0]?.blockType,
        }
        executionDataHeader = JSON.stringify(safeExecutionData)
      } catch (error) {
        logger.error(`[${requestId}] Failed to serialize execution data:`, error)
        executionDataHeader = JSON.stringify({
          success: executionData.success,
          error: 'Failed to serialize full execution data',
        })
      }

      // Return the stream with execution data in a header
      return new Response(stream, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          Connection: 'keep-alive',
          'X-Execution-Data': executionDataHeader,
        },
      })
    }

    // Check if the response is a ReadableStream for streaming
    if (response instanceof ReadableStream) {
      logger.info(`[${requestId}] Streaming response from provider`)
      return new Response(response, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          Connection: 'keep-alive',
        },
      })
    }

    // Return regular JSON response for non-streaming
    return NextResponse.json(response)
  } catch (error) {
    const executionTime = Date.now() - startTime
    logger.error(`[${requestId}] Provider request failed:`, {
      error: toError(error).message,
      errorName: error instanceof Error ? error.name : 'Unknown',
      errorStack: error instanceof Error ? error.stack : undefined,
      executionTime,
      timestamp: new Date().toISOString(),
    })

    return NextResponse.json({ error: toError(error).message }, { status: 500 })
  }
})

/**
 * Helper function to sanitize tool calls to remove Unicode characters
 */
function sanitizeToolCalls(toolCalls: any) {
  // If it's an object with a list property, sanitize the list
  if (toolCalls && typeof toolCalls === 'object' && Array.isArray(toolCalls.list)) {
    return {
      ...toolCalls,
      list: toolCalls.list.map(sanitizeToolCall),
    }
  }

  // If it's an array, sanitize each item
  if (Array.isArray(toolCalls)) {
    return toolCalls.map(sanitizeToolCall)
  }

  return toolCalls
}

/**
 * Sanitize a single tool call to remove Unicode characters
 */
function sanitizeToolCall(toolCall: any) {
  if (!toolCall || typeof toolCall !== 'object') return toolCall

  // Create a sanitized copy
  const sanitized = { ...toolCall }

  // Sanitize any string fields that might contain Unicode
  if (typeof sanitized.name === 'string') {
    sanitized.name = sanitized.name.replace(/[\u0080-\uFFFF]/g, '')
  }

  // Sanitize input/arguments
  if (sanitized.input && typeof sanitized.input === 'object') {
    sanitized.input = sanitizeObject(sanitized.input)
  }

  if (sanitized.arguments && typeof sanitized.arguments === 'object') {
    sanitized.arguments = sanitizeObject(sanitized.arguments)
  }

  // Sanitize output/result
  if (sanitized.output && typeof sanitized.output === 'object') {
    sanitized.output = sanitizeObject(sanitized.output)
  }

  if (sanitized.result && typeof sanitized.result === 'object') {
    sanitized.result = sanitizeObject(sanitized.result)
  }

  // Sanitize error message
  if (typeof sanitized.error === 'string') {
    sanitized.error = sanitized.error.replace(/[\u0080-\uFFFF]/g, '')
  }

  return sanitized
}

/**
 * Recursively sanitize an object to remove Unicode characters from strings
 */
function sanitizeObject(obj: any): any {
  if (!obj || typeof obj !== 'object') return obj

  // Handle arrays
  if (Array.isArray(obj)) {
    return obj.map((item) => sanitizeObject(item))
  }

  // Handle objects
  const result: any = {}
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'string') {
      result[key] = value.replace(/[\u0080-\uFFFF]/g, '')
    } else if (typeof value === 'object' && value !== null) {
      result[key] = sanitizeObject(value)
    } else {
      result[key] = value
    }
  }

  return result
}

/**
 * Resolves a Vertex AI OAuth credential to an access token
 */
async function resolveVertexCredential(requestId: string, credentialId: string): Promise<string> {
  logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)

  const resolved = await resolveOAuthAccountId(credentialId)
  if (!resolved) {
    throw new Error(`Vertex AI credential not found: ${credentialId}`)
  }

  if (resolved.credentialType === 'service_account' && resolved.credentialId) {
    const accessToken = await getServiceAccountToken(resolved.credentialId, [
      'https://www.googleapis.com/auth/cloud-platform',
    ])
    logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
    return accessToken
  }

  const credential = await db.query.account.findFirst({
    where: eq(account.id, resolved.accountId),
  })

  if (!credential) {
    throw new Error(`Vertex AI credential not found: ${credentialId}`)
  }

  const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId)

  if (!accessToken) {
    throw new Error('Failed to get Vertex AI access token')
  }

  logger.info(`[${requestId}] Successfully resolved Vertex AI credential`)
  return accessToken
}
