import type {
  ChatMessage,
  ConversationListItem,
  ConversationStatusRecord,
} from "./types"
import { SaeeolChatClient } from "./chat-client"
import { EventServiceClient } from "./event-service-client"
import * as Log from "@saeeol/core/util/log"
import {
  type ClawChatClient,
  type ConnectInput,
  type ConversationStatusListener,
  type ConversationsListener,
  type ActiveConversationListener,
  type TypingMembersListener,
  createTypingTracker,
  toChatMessage,
  emitSet,
  onAdd,
} from "./client-helpers"
import { setupEvents, trackLastSeen } from "./client-events"

export type {
  ConversationStatusListener,
  ConversationsListener,
  ActiveConversationListener,
  TypingMembersListener,
  ClawChatClient,
  ConnectInput,
} from "./client-helpers"

const log = Log.create({ service: "claw-chat" })

export async function connect(input: ConnectInput): Promise<ClawChatClient> {
  const events = new EventServiceClient({
    url: input.envelope.eventServiceUrl,
    getToken: async () => input.envelope.token,
  })
  const chat = new SaeeolChatClient({
    baseUrl: input.envelope.saeeolChatUrl,
    getToken: async () => input.envelope.token,
  })

  log.info("connecting to event-service")
  await events.connect()

  const sandboxCtx = `/saeeolclaw/${input.sandboxId}`
  events.subscribe([sandboxCtx])

  const senderCache = new Map<string, string>()
  let conversations: ConversationListItem[] = []
  let activeId: string | null = null
  let activeStatus: ConversationStatusRecord | null = null
  let activeCtx: string | null = null

  const typing = createTypingTracker((members) => emitSet(typingListeners, members))

  const messageListeners = new Set<(msg: ChatMessage) => void>()
  const messageUpdatedListeners = new Set<(msg: ChatMessage) => void>()
  const presenceListeners = new Set<(online: boolean) => void>()
  const conversationsListeners = new Set<ConversationsListener>()
  const activeListeners = new Set<ActiveConversationListener>()
  const statusListeners = new Set<ConversationStatusListener>()
  const typingListeners = new Set<TypingMembersListener>()

  async function loadConversations(): Promise<ConversationListItem[]> {
    const res = await chat.listConversations({ sandboxId: input.sandboxId, limit: 50 })
    conversations = res.conversations
    emitSet(conversationsListeners, conversations)
    return conversations
  }

  function setActive(id: string | null): void {
    if (activeId === id) return
    if (activeCtx) {
      events.unsubscribe([activeCtx])
      activeCtx = null
    }
    activeId = id
    activeStatus = null
    typing.clear()
    if (id) {
      activeCtx = `/saeeolclaw/${input.sandboxId}/${id}`
      events.subscribe([activeCtx])
    }
    emitSet(activeListeners, activeId)
    emitSet(statusListeners, activeStatus)
  }

  const initialOnline = await chat
    .getBotStatus(input.sandboxId)
    .then((s) => s.status?.online ?? false)
    .catch((err) => {
      log.warn("getBotStatus failed", { error: (err as Error)?.message ?? String(err) })
      return false
    })

  await loadConversations().catch((err) => {
    log.warn("listConversations failed", { error: (err as Error)?.message ?? String(err) })
  })

  const lastSeenByConv = new Map<string, string>()

  setupEvents({
    events,
    sandboxCtx,
    sandboxId: input.sandboxId,
    currentUserId: input.currentUserId,
    getActiveCtx: () => activeCtx,
    getActiveId: () => activeId,
    senderCache,
    lastSeenByConv,
    getConversations: () => conversations,
    setConversations: (c) => { conversations = c },
    setActiveStatus: (s) => { activeStatus = s },
    loadConversations,
    setActive,
    typing,
    messageListeners,
    messageUpdatedListeners,
    presenceListeners,
    conversationsListeners,
    statusListeners,
  })

  async function loadHistory(conversationId?: string): Promise<ChatMessage[]> {
    const id = conversationId ?? activeId
    if (!id) return []
    const res = await chat.listMessages(id, { limit: 50 })
    const ascending = [...res.messages].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
    for (const m of ascending) {
      senderCache.set(m.id, m.senderId)
      trackLastSeen({ lastSeenByConv }, id, m.id)
    }
    return ascending.map((m) => toChatMessage(m, input.currentUserId))
  }

  async function selectConversation(conversationId: string) {
    setActive(conversationId)
    const msgs = await loadHistory(conversationId)
    const status = await chat.getConversationStatus(conversationId).then(
      (res) => res.status ?? null,
      (err) => {
        log.warn("getConversationStatus failed", { error: (err as Error)?.message ?? String(err) })
        return null
      },
    )
    if (activeId === conversationId) {
      activeStatus = status
      emitSet(statusListeners, activeStatus)
    }
    const lastSeen = lastSeenByConv.get(conversationId)
    if (lastSeen) {
      await chat.markConversationRead(conversationId, { lastSeenMessageId: lastSeen }).catch((err) => {
        log.warn("markConversationRead failed", { error: (err as Error)?.message ?? String(err) })
      })
    }
    return { messages: msgs, status }
  }

  async function createConversation(title?: string): Promise<string> {
    const res = await chat.createConversation({ sandboxId: input.sandboxId, title })
    await loadConversations()
    return res.conversationId
  }

  return {
    async disconnect() {
      typing.dispose()
      events.disconnect()
    },
    listConversations() {
      return conversations
    },
    activeConversationId() {
      return activeId
    },
    typingMembers() {
      return typing.snapshot()
    },
    async refreshConversations() {
      return await loadConversations()
    },
    selectConversation,
    createConversation,
    async renameConversation(conversationId, title) {
      conversations = conversations.map((c) => (c.conversationId === conversationId ? { ...c, title } : c))
      emitSet(conversationsListeners, conversations)
      await chat.renameConversation(conversationId, title)
    },
    async deleteConversation(conversationId) {
      await chat.leaveConversation(conversationId)
      conversations = conversations.filter((c) => c.conversationId !== conversationId)
      emitSet(conversationsListeners, conversations)
      if (activeId === conversationId) setActive(null)
    },
    async send(text: string) {
      const trimmed = text.trim()
      if (!trimmed) return
      if (!activeId) {
        const id = await createConversation()
        await selectConversation(id)
      }
      if (!activeId) return
      await chat.sendMessage({
        conversationId: activeId,
        content: [{ type: "text", text: trimmed }],
      })
    },
    loadHistory,
    initialBotOnline() {
      return initialOnline
    },
    conversationStatus() {
      return activeStatus
    },
    onMessage: (cb) => onAdd(messageListeners, cb),
    onMessageUpdated: (cb) => onAdd(messageUpdatedListeners, cb),
    onPresence: (cb) => onAdd(presenceListeners, cb),
    onConversations: (cb) => onAdd(conversationsListeners, cb),
    onActiveConversation: (cb) => onAdd(activeListeners, cb),
    onConversationStatus: (cb) => onAdd(statusListeners, cb),
    onTypingMembers: (cb) => onAdd(typingListeners, cb),
  }
}
