import { sessionRepo } from '@/lib/database/repositories/session.repo'
import { messageRepo } from '@/lib/database/repositories/message.repo'
import crypto from 'crypto'
import fs from 'fs'
import { EngineRegistry } from '@/lib/engine'
import type { ForgeAttachment } from '@/lib/engine/adapters/claude/client'
import path from 'path'
import { getUploadsDir } from '@/lib/forge-data'
import { createSseCallbacks } from '@/lib/engine/consumers/sse-adapter'
import type { AgentResult } from '@/lib/engine/agent-types'
import { createPermissionBridge, cleanupStaleSessionAllowances } from '@/lib/engine/adapters/claude/permission-bridge'
import { archiveOldMemories } from '@/lib/workspace-fs'
import { resolveProvider } from '@/lib/provider'
import { runSessionCleanup } from '@/lib/session-cleanup'
import { maybeFlushMemory } from '@/lib/memory-flush'
import { extractFilePaths, resolveFileAttachments, parseMediaProtocol } from '@/lib/im/core/conversation-engine'
import { isAuthError, requireUser, requireWorkspaceWriteAccess } from '@/lib/auth/session'
import { getSetting } from '@/lib/settings-store'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

let lastArchiveTime = 0

// ── Helpers ────────────────────────────────────────────────────

/**
 * Build a prompt that includes recent conversation history as context.
 * Used as fallback when SDK session resume fails.
 */
function buildPromptWithHistory(
  newMessage: string,
  recentMessages: Array<{ role: string; content: string }>,
): string {
  if (recentMessages.length === 0) return newMessage

  const history = recentMessages
    .slice(-20) // Last 20 messages for context
    .map((m) => {
      const role = m.role === 'user' ? 'Human' : 'Assistant'
      // For assistant messages stored as JSON blocks, summarize
      let content = m.content
      if (content.startsWith('[{') || content.startsWith('[')) {
        try {
          const blocks = JSON.parse(content)
          content = blocks
            .filter((b: Record<string, unknown>) => b.type === 'text' && b.text)
            .map((b: Record<string, unknown>) => b.text)
            .join(' ')
          if (!content) content = '[tool usage]'
        } catch {
          content = content.slice(0, 200)
        }
      }
      return `${role}: ${content}`
    })
    .join('\n\n')

  return `<conversation_history>\n${history}\n</conversation_history>\n\n${newMessage}`
}

export async function POST(req: Request) {
  let user
  try {
    user = await requireUser()
  } catch (err) {
    if (isAuthError(err)) return err
    throw err
  }
  let body: { sessionId?: string; message?: string; permissionMode?: string; thinkingMode?: string; attachments?: Array<{ name: string; filename: string; mimeType: string; tier: string }> }
  try {
    body = await req.json()
  } catch {
    return jsonError('Invalid JSON', 400)
  }

  const { sessionId, message } = body

  // Map raw attachment data to typed ForgeAttachment[]
  // Reconstruct serverPath from filename (never trust absolute paths from client)
  // Legacy tiers (pdf, text, binary) are mapped to 'file' — the Agent uses
  // its Read tool for all non-image files, no pre-classification needed.
  const uploadsDir = getUploadsDir()
  const attachments: ForgeAttachment[] = (body.attachments || []).map(a => {
    const safeFilename = a.filename.replace(/\.\./g, '').replace(/[/\\]/g, '_') // sanitize: strip path traversal + slashes, preserve dots in extensions
    const normalizedTier = a.tier === 'image' ? 'image' : 'file'
    return {
      name: a.name,
      serverPath: path.join(uploadsDir, safeFilename),
      mimeType: a.mimeType,
      tier: normalizedTier as ForgeAttachment['tier'],
    }
  })
  if (!sessionId || (!message && attachments.length === 0)) return jsonError('sessionId and message (or attachments) required', 400)
  // Get session
  const session = sessionRepo.findById(sessionId)
  if (!session) return jsonError('Session not found', 404)
  const engine = EngineRegistry.get(session.backend)
  if (session.userId && session.userId !== user.id) return jsonError('Forbidden', 403)
  if (session.workspace) {
    try { await requireWorkspaceWriteAccess(session.workspace) } catch (err) { if (isAuthError(err)) return err; throw err }
  }

  // Permission mode: per-session override from request body takes priority over global Settings
  const permissionMode = body.permissionMode || getSetting('desktop_permission_mode', user.id) || 'confirm'

  // Resolve provider type for thinking mode mapping
  const resolved = resolveProvider(session.model, user.id)
  const providerType = resolved.provider

  // Get thinking mode: per-request override > provider-specific setting > global default
  const providerThinkingKey = `thinking_mode_${providerType}`
  const thinkingMode = body.thinkingMode || getSetting(providerThinkingKey, user.id) || getSetting('thinking_mode', user.id) || 'auto'

  // Check if this session already has messages (for resume)
  const existingMsgCount = messageRepo.countBySession(sessionId)

  // Resolve effective message: if empty but has attachments, use file names
  // For non-image files, include disk paths so the Agent can use Read tool.
  // For images, include disk path as fallback (base64 vision is the primary path).
  const buildAttachmentPrompt = () => {
    const imageAtts = attachments.filter(a => a.tier === 'image')
    const fileAtts = attachments.filter(a => a.tier === 'file')
    const parts: string[] = []
    if (imageAtts.length > 0) {
      const refs = imageAtts.map(a => `[User attached image saved at: ${a.serverPath} (${a.name})]`).join('\n')
      parts.push(`${refs}\n(The image is also sent via vision above; use Read tool on this path if vision fails)`)
    }
    if (fileAtts.length > 0) {
      const refs = fileAtts.map(a => `[User attached file: ${a.serverPath} (${a.name})]`).join('\n')
      parts.push(`${refs}\nPlease read the attached file(s) using your Read tool, then respond.`)
    }
    return parts.join('\n\n')
  }
  const attachmentPrompt = buildAttachmentPrompt()
  const effectiveMessage = message || (attachments.length > 0
    ? attachments.map(a => `[${a.name}]`).join(' ')
    : '')
  const finalPrompt = attachmentPrompt
    ? `${attachmentPrompt}\n\n${effectiveMessage}`
    : effectiveMessage

  // Save user message
  const userMsgId = crypto.randomUUID()
  messageRepo.create({ id: userMsgId, sessionId, role: 'user', content: effectiveMessage })

  // Archive old daily memories (throttled: max once per hour)
  const now = Date.now()
  if (now - lastArchiveTime > 3600_000) {
    lastArchiveTime = now
    const retentionDays = parseInt(getSetting('memory_retention_days', user.id) || '7', 10)
    archiveOldMemories(session.workspace, retentionDays, user.id)
  }

  // Auto-clean old sessions (throttled shared function) + stale permission allowances
  runSessionCleanup()
  cleanupStaleSessionAllowances()

  const encoder = new TextEncoder()
  const abortController = new AbortController()

  // Use TransformStream for immediate chunk flushing — ReadableStream's async start()
  // can cause Next.js to buffer the entire response before sending.
  const { readable, writable } = new TransformStream()
  const writer = writable.getWriter()

  const emit = async (data: Record<string, unknown>) => {
    try {
      await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
    } catch { /* writer closed (client disconnected) */ }
  }

  // Run the stream processing in the background — Response is returned immediately
  ;(async () => {
    let result: AgentResult | undefined
    try {
      // Create permission bridge (only for 'confirm' mode)
      const canUseTool = permissionMode === 'confirm'
        ? createPermissionBridge(sessionId, emit as (data: Record<string, unknown>) => void)
        : undefined

      const isResume = existingMsgCount > 0

      const baseRequest = {
        prompt: finalPrompt,
        sessionId,
        model: session.model,
        workspaceId: session.workspace,
        userId: user.id,
        abortController,
        canUseTool,
        bypassPermissions: permissionMode === 'full',
        resumeSession: isResume,
        thinkingMode,
        providerType,
        attachments: attachments.length > 0 ? attachments : undefined,
      }

      // Consume engine stream via unified process() + SSE adapter (B-C).
      // Resume fallback: zero blocks (silent SDK error like "Session ID in use")
      // → retry with fresh session + conversation history.
      result = await engine.process(baseRequest, createSseCallbacks(emit))
      if (isResume && result.blocks.length === 0) {
        console.warn('[forge-chat] Resume produced zero blocks (likely SDK error result), will retry with fresh session')
        const recentMsgs = messageRepo.listRoleContentBySession(sessionId, userMsgId)
        const historyPrompt = buildPromptWithHistory(effectiveMessage, recentMsgs)
        result = await engine.process(
          { ...baseRequest, prompt: historyPrompt, sessionId: crypto.randomUUID(), resumeSession: false, attachments: undefined },
          createSseCallbacks(emit),
        )
      }

      // Detect file/image attachments from two sources:
      // 1. MEDIA: protocol lines in text blocks (explicit, preferred)
      // 2. Tool use heuristics (fallback)
      const blocks = result!.blocks

      // Parse MEDIA: lines from text blocks and strip them from displayed text
      const mediaPaths: string[] = []
      for (const block of blocks) {
        if ((block as Record<string, unknown>).type === 'text') {
          const b = block as { type: string; text: string }
          const parsed = parseMediaProtocol(b.text)
          mediaPaths.push(...parsed.mediaPaths)
          b.text = parsed.text // Strip MEDIA: lines from stored text
        }
      }

      // Merge MEDIA: paths with heuristic-detected paths, dedup
      const mediaAttachments = resolveFileAttachments(mediaPaths)
      const heuristicAttachments = resolveFileAttachments(extractFilePaths(blocks as Record<string, unknown>[]))
      const seenPaths = new Set(mediaAttachments.map(a => a.filePath))
      const detectedFiles = [...mediaAttachments, ...heuristicAttachments.filter(a => !seenPaths.has(a.filePath))]

      const uploadsDir = getUploadsDir()
      for (const att of detectedFiles) {
        try {
          // Copy to uploads dir so it's accessible via /api/upload/
          const destName = `agent_${Date.now()}_${att.name.replace(/[^a-zA-Z0-9._-]/g, '_')}`
          const destPath = path.join(uploadsDir, destName)
          fs.copyFileSync(att.filePath, destPath)
          if (att.isImage) {
            blocks.push({ type: 'image_attachment', url: `/api/upload/${destName}`, name: att.name })
          } else {
            blocks.push({ type: 'file_attachment', url: `/api/upload/${destName}`, name: att.name, size: att.size, mimeType: att.mimeType })
          }
          // Emit SSE event so frontend renders immediately
          await emit({ type: 'attachment', attachment: att.isImage
            ? { type: 'image_attachment', url: `/api/upload/${destName}`, name: att.name }
            : { type: 'file_attachment', url: `/api/upload/${destName}`, name: att.name, size: att.size, mimeType: att.mimeType }
          })
        } catch { /* skip unreadable files */ }
      }

      // Save assistant message to SQLite (dual persistence)
      const assistantMsgId = crypto.randomUUID()
      messageRepo.create({ id: assistantMsgId, sessionId, role: 'assistant', content: JSON.stringify(blocks) })

      // Update session title
      const msgCount = messageRepo.countBySession(sessionId)
      if (msgCount <= 2) {
        const title = effectiveMessage.length > 50 ? effectiveMessage.slice(0, 47) + '...' : effectiveMessage
        sessionRepo.updateTitle(sessionId, title)
      } else {
        sessionRepo.touch(sessionId)
      }

      await emit({
        type: 'done',
        messageId: assistantMsgId,
        userMessageId: userMsgId,
        inputTokens: result!.inputTokens,
        outputTokens: result!.outputTokens,
      })

      // Background memory flush — remind Agent to write memory if needed (every N messages)
      // Fire-and-forget: user doesn't wait for this
      const assistantText = blocks.filter((b: Record<string, unknown>) => b.type === 'text').map((b: Record<string, unknown>) => b.text).join(' ')
      maybeFlushMemory(sessionId, session.workspace, session.model, effectiveMessage, assistantText.slice(0, 200), user.id)
    } catch (err) {
      // Save partial response if any blocks were generated
      const blocks = result?.blocks ?? []
      if (blocks.length > 0) {
        const assistantMsgId = crypto.randomUUID()
        messageRepo.create({ id: assistantMsgId, sessionId, role: 'assistant', content: JSON.stringify(blocks) })
      } else {
        messageRepo.deleteById(userMsgId)
      }

      const errorMessage = err instanceof Error ? err.message : 'Unknown error'
      await emit({ type: 'error', error: errorMessage })
    } finally {
      try { await writer.close() } catch { /* already closed */ }
    }
  })()

  // Handle client disconnect
  req.signal.addEventListener('abort', () => {
    abortController.abort()
  })

  return new Response(readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no',
      'X-Content-Type-Options': 'nosniff',
    },
  })
}

function jsonError(msg: string, status: number) {
  return new Response(JSON.stringify({ error: msg }), {
    status,
    headers: { 'Content-Type': 'application/json' },
  })
}
