/**
 * Channel Router (Layer 3).
 *
 * Maps (channelId, chatId) → sessionId with per-session binding.
 * Handles auto-creation of new sessions and stale session detection.
 */

import { imChannelRepo } from '@/lib/database/repositories/im-channel.repo'
import { channelBindingRepo } from '@/lib/database/repositories/channel-binding.repo'
import { sessionRepo } from '@/lib/database/repositories/session.repo'
import { externalUserService } from '@/lib/external/external-user-service'
import crypto from 'crypto'
import type { ChannelType, IncomingMessage } from '../types'
import { getSetting } from '@/lib/settings-store'
import {
  getAccessibleWorkspaceByPath,
  listAccessibleWorkspaces,
  userCanAccessWorkspace,
} from '@/lib/auth/workspace-queries'

/** Session resolution result */
export interface SessionResolution {
  sessionId: string
  isNew: boolean
  workspace: string
  userId?: string
}

export class ChannelRouter {
  /**
   * Resolve the session for an incoming message.
   *
   * Sessions never auto-expire. Once a binding exists,
   * it persists until the user explicitly runs /new or /bind.
   *
   * Priority:
   * 1. Existing binding with session_id → always reuse (no expiry)
   * 2. Existing binding with workspace (legacy) → create new session in that workspace
   * 3. No binding → create new session + binding (auto-bind)
   */
  resolveSession(msg: IncomingMessage): SessionResolution {
    const userId = this.getChannelOwnerUserId(msg.channelId)

    // Check for existing binding
    const binding = channelBindingRepo.findByChannelAndChat(msg.channelId, msg.chatId)

    if (binding?.sessionId) {
      // Verify session still exists in DB
      const session = sessionRepo.findById(binding.sessionId)

      if (session && this.canUseSession(msg.channelId, binding.sessionId, 'write')) {
        return { sessionId: binding.sessionId, isNew: false, workspace: session.workspace, userId }
      }

      // Session was deleted or no longer authorized — create a new one in an accessible workspace.
      const workspace = this.resolveWritableWorkspace(
        msg.channelId,
        binding.workspaceId || binding.workspace,
      )
      const newSessionId = this.createSession(msg, workspace, userId)

      channelBindingRepo.updateSessionAndWorkspace(binding.id, newSessionId, workspace, workspace)

      return { sessionId: newSessionId, isNew: true, workspace, userId }
    }

    if (binding) {
      // Legacy binding (workspace only, no session_id)
      const workspace = this.resolveWritableWorkspace(
        msg.channelId,
        binding.workspaceId || binding.workspace,
      )
      const newSessionId = this.createSession(msg, workspace, userId)

      // Upgrade binding with session_id
      channelBindingRepo.updateSession(binding.id, newSessionId, workspace)

      return { sessionId: newSessionId, isNew: true, workspace, userId }
    }

    // No binding — auto-create
    return this.createAutoBinding(msg)
  }

  /**
   * Explicitly bind a chat to a specific session.
   * Used by /bind command.
   */
  bindSession(channelId: string, _channelType: ChannelType, chatId: string, sessionId: string): void {
    const userId = this.getChannelOwnerUserId(channelId)

    // Verify session exists
    const session = sessionRepo.findById(sessionId)
    if (!session) throw new Error(`Session not found: ${sessionId}`)
    if (userId && session.userId !== userId) throw new Error(`Session not found: ${sessionId}`)
    if (!userCanAccessWorkspace(session.workspace, userId, 'write')) {
      throw new Error(`Session not found: ${sessionId}`)
    }

    // Upsert binding
    const existing = channelBindingRepo.findIdByChannelAndChat(channelId, chatId)

    if (existing) {
      channelBindingRepo.updateSessionAndWorkspace(existing.id, sessionId, session.workspace, session.workspace)
    } else {
      channelBindingRepo.create({
        id: crypto.randomUUID(),
        channelId,
        chatId,
        workspace: session.workspace,
        workspaceId: session.workspace,
        sessionId,
      })
    }
  }

  /**
   * Unbind a chat (removes the binding entirely).
   * Next message from this chat will auto-create a new session.
   */
  unbindSession(channelId: string, _channelType: ChannelType, chatId: string): void {
    channelBindingRepo.deleteByChannelAndChat(channelId, chatId)
  }

  /**
   * Create a new session and binding for a chat (used by /new command).
   * Returns the new session ID.
  */
  createNewSession(msg: IncomingMessage): string {
    const userId = this.getChannelOwnerUserId(msg.channelId)

    // Get current workspace from existing binding or use most recent
    const binding = channelBindingRepo.findWorkspaceByChannelAndChat(msg.channelId, msg.chatId)

    const workspace = this.resolveWritableWorkspace(msg.channelId, binding?.workspaceId || binding?.workspace)
    const newSessionId = this.createSession(msg, workspace, userId)

    if (binding) {
      channelBindingRepo.updateSession(binding.id, newSessionId, workspace)
    } else {
      channelBindingRepo.create({
        id: crypto.randomUUID(),
        channelId: msg.channelId,
        chatId: msg.chatId,
        workspace,
        workspaceId: workspace,
        sessionId: newSessionId,
      })
    }

    return newSessionId
  }

  /**
   * Get binding info for a chat (for /status command).
   */
  getBindingInfo(channelId: string, _channelType: ChannelType, chatId: string): {
    sessionId: string | null
    workspace: string
  } | null {
    const binding = channelBindingRepo.findSessionByChannelAndChat(channelId, chatId)

    if (!binding) return null
    return { sessionId: binding.sessionId, workspace: binding.workspaceId || binding.workspace }
  }

  // ---------------------------------------------------------------------------
  // Private helpers
  // ---------------------------------------------------------------------------

  private createAutoBinding(msg: IncomingMessage): SessionResolution {
    const userId = this.getChannelOwnerUserId(msg.channelId)
    const workspace = this.getDefaultWorkspace(msg.channelId)
    const sessionId = this.createSession(msg, workspace, userId)

    channelBindingRepo.create({
      id: crypto.randomUUID(),
      channelId: msg.channelId,
      chatId: msg.chatId,
      workspace,
      workspaceId: workspace,
      sessionId,
    })

    return { sessionId, isNew: true, workspace, userId }
  }

  private createSession(msg: IncomingMessage, workspace: string, userId?: string): string {
    const sessionId = crypto.randomUUID()
    const defaultModel = this.getDefaultModel(msg.channelId)

    // 解析/创建统一外部用户身份
    const externalUserId = userId
      ? externalUserService.resolveOrCreate({
          channelType: `im:${msg.channelType}`,
          channelId: msg.channelId,
          platformUserId: msg.senderId,
          platformName: msg.senderName,
          ownerUserId: userId,
        })
      : null

    sessionRepo.create({
      id: sessionId,
      title: `[${msg.channelType}] ${msg.senderName || 'New conversation'}`,
      workspace,
      model: defaultModel,
      userId: userId ?? '',
      externalUserId,
    })

    return sessionId
  }

  private resolveWritableWorkspace(channelId: string | undefined, candidate?: string): string {
    const userId = this.getChannelOwnerUserId(channelId)
    if (candidate && userCanAccessWorkspace(candidate, userId, 'write')) return candidate
    return this.getDefaultWorkspace(channelId)
  }

  private getChannelOwnerUserId(channelId?: string): string | undefined {
    if (!channelId) return undefined
    const channel = imChannelRepo.findOwnerUserId(channelId)
    return channel?.ownerUserId || undefined
  }

  /**
   * Get the default workspace for new sessions.
   * Priority: bridge_default_work_dir setting → most recently opened workspace.
   */
  getDefaultWorkspace(channelId?: string): string {
    const userId = this.getChannelOwnerUserId(channelId)

    if (channelId) {
      const channelDefault = imChannelRepo.findDefaultWorkspaceId(channelId)
      if (
        channelDefault?.defaultWorkspaceId
        && userCanAccessWorkspace(channelDefault.defaultWorkspaceId, userId, 'write')
      ) {
        return channelDefault.defaultWorkspaceId
      }
    }

    // Check bridge default setting first
    const defaultDir = getSetting('bridge_default_work_dir', userId)
    if (defaultDir) {
      // Look up workspace by path
      const ws = getAccessibleWorkspaceByPath(defaultDir, userId)
      if (ws && userCanAccessWorkspace(ws.id, userId, 'write')) return ws.id
    }

    // Fall back to most recently opened writable workspace
    const ws = listAccessibleWorkspaces(userId)
      .find(workspace => userCanAccessWorkspace(workspace.id, userId, 'write'))
    return ws?.id || ''
  }

  /**
   * Get the default model for new sessions.
   * Priority: bridge_default_model setting → 'claude-sonnet-4-6'.
   */
  private getDefaultModel(channelId?: string): string {
    if (channelId) {
      const channelDefault = imChannelRepo.findDefaultModel(channelId)
      if (channelDefault?.defaultModel) return channelDefault.defaultModel
    }
    return getSetting('bridge_default_model', this.getChannelOwnerUserId(channelId)) || 'claude-sonnet-4-6'
  }

  /**
   * List recent sessions (for /sessions command).
   */
  canUseSession(channelId: string, sessionId: string, access: 'read' | 'write' = 'read'): boolean {
    const userId = this.getChannelOwnerUserId(channelId)
    const session = sessionRepo.findById(sessionId)
    if (!session) return false
    if (userId && session.userId !== userId) return false
    return userCanAccessWorkspace(session.workspace, userId, access)
  }

  listRecentSessions(
    limit = 10,
    channelId?: string,
  ): Array<{ id: string; title: string; workspace: string; updatedAt: string }> {
    const userId = this.getChannelOwnerUserId(channelId)
    const rows = sessionRepo.listRecent(limit, userId)
    return rows.filter(session => userCanAccessWorkspace(session.workspace, userId, 'read'))
  }
}
