import type { MutableRefObject } from 'react'
import { useCallback, useRef } from 'react'
import type { NavigateFunction } from 'react-router-dom'

import { deleteSession, getSessionMessages } from '@/hermes'
import { type ChatMessage, chatMessageText, toChatMessages } from '@/lib/chat-messages'
import { normalizePersonalityValue } from '@/lib/chat-runtime'
import { embeddedImageUrls, textWithoutEmbeddedImages } from '@/lib/embedded-images'
import { clearComposerAttachments, clearComposerDraft } from '@/store/composer'
import { $pinnedSessionIds } from '@/store/layout'
import { clearNotifications, notify, notifyError } from '@/store/notifications'
import { requestDesktopOnboarding } from '@/store/onboarding'
import {
  $messages,
  $sessions,
  setActiveSessionId,
  setAwaitingResponse,
  setBusy,
  setCurrentBranch,
  setCurrentCwd,
  setCurrentFastMode,
  setCurrentModel,
  setCurrentPersonality,
  setCurrentProvider,
  setCurrentReasoningEffort,
  setCurrentServiceTier,
  setCurrentUsage,
  setFreshDraftReady,
  setIntroSeed,
  setMessages,
  setSelectedStoredSessionId,
  setSessions,
  setSessionStartedAt,
  setTurnStartedAt
} from '@/store/session'
import type { SessionCreateResponse, SessionInfo, SessionResumeResponse, UsageStats } from '@/types/hermes'

import { NEW_CHAT_ROUTE, sessionRoute, SETTINGS_ROUTE } from '../../routes'
import type { ClientSessionState, SidebarNavItem } from '../../types'

interface SessionActionsOptions {
  activeSessionId: string | null
  activeSessionIdRef: MutableRefObject<string | null>
  busyRef: MutableRefObject<boolean>
  creatingSessionRef: MutableRefObject<boolean>
  ensureSessionState: (sessionId: string, storedSessionId?: string | null) => ClientSessionState
  getRouteToken: () => string
  navigate: NavigateFunction
  requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
  runtimeIdByStoredSessionIdRef: MutableRefObject<Map<string, string>>
  selectedStoredSessionId: string | null
  selectedStoredSessionIdRef: MutableRefObject<string | null>
  sessionStateByRuntimeIdRef: MutableRefObject<Map<string, ClientSessionState>>
  syncSessionStateToView: (sessionId: string, state: ClientSessionState) => void
  updateSessionState: (
    sessionId: string,
    updater: (state: ClientSessionState) => ClientSessionState,
    storedSessionId?: string | null
  ) => ClientSessionState
}

function withAppendedText(message: ChatMessage, suffix: string): ChatMessage {
  let appended = false

  const parts = message.parts.map(part => {
    if (part.type !== 'text' || appended) {
      return part
    }

    appended = true

    return { ...part, text: `${part.text}${suffix}` }
  })

  return appended ? { ...message, parts } : message
}

function preserveReasoningParts(message: ChatMessage, previous: ChatMessage): ChatMessage {
  if (message.parts.some(part => part.type === 'reasoning')) {
    return message
  }

  const reasoningParts = previous.parts.filter(part => part.type === 'reasoning')

  return reasoningParts.length ? { ...message, parts: [...reasoningParts, ...message.parts] } : message
}

function chatMessagesEquivalent(a: ChatMessage, b: ChatMessage): boolean {
  if (
    a.id !== b.id ||
    a.role !== b.role ||
    a.pending !== b.pending ||
    a.hidden !== b.hidden ||
    a.branchGroupId !== b.branchGroupId
  ) {
    return false
  }

  if (a.parts.length !== b.parts.length) {
    return false
  }

  return a.parts.every((part, index) => JSON.stringify(part) === JSON.stringify(b.parts[index]))
}

function chatMessageArraysEquivalent(a: ChatMessage[], b: ChatMessage[]): boolean {
  return a.length === b.length && a.every((message, index) => chatMessagesEquivalent(message, b[index]))
}

function reconcileResumeMessages(nextMessages: ChatMessage[], previousMessages: ChatMessage[]): ChatMessage[] {
  if (!previousMessages.length) {
    return nextMessages
  }

  const previousByRoleOrdinal = new Map<string, ChatMessage>()
  const previousRoleCounts = new Map<string, number>()

  for (const message of previousMessages) {
    const ordinal = previousRoleCounts.get(message.role) ?? 0
    previousRoleCounts.set(message.role, ordinal + 1)
    previousByRoleOrdinal.set(`${message.role}:${ordinal}`, message)
  }

  const nextRoleCounts = new Map<string, number>()

  return nextMessages.map(message => {
    const ordinal = nextRoleCounts.get(message.role) ?? 0
    nextRoleCounts.set(message.role, ordinal + 1)

    const previous = previousByRoleOrdinal.get(`${message.role}:${ordinal}`)

    if (!previous) {
      return message
    }

    const nextText = chatMessageText(message).trim()
    const previousText = chatMessageText(previous)
    const previousVisibleText = textWithoutEmbeddedImages(previousText)
    let preserved = message

    if (nextText === previousVisibleText || nextText === previousText.trim()) {
      preserved = preserveReasoningParts(preserved, previous)
    }

    const previousImages = embeddedImageUrls(previousText)

    if (!previousImages.length || embeddedImageUrls(chatMessageText(preserved)).length) {
      return preserved
    }

    if (nextText !== previousVisibleText) {
      return preserved
    }

    return withAppendedText(preserved, previousImages.map(url => `\n${url}`).join(''))
  })
}

function upsertOptimisticSession(
  created: SessionCreateResponse,
  id: string,
  title: string | null = null,
  preview: string | null = null
) {
  const now = Date.now() / 1000

  const session: SessionInfo = {
    ended_at: null,
    id,
    input_tokens: 0,
    is_active: true,
    last_active: now,
    message_count: created.message_count ?? created.messages?.length ?? 0,
    model: created.info?.model ?? null,
    output_tokens: 0,
    preview,
    source: 'tui',
    started_at: now,
    title,
    tool_call_count: 0
  }

  setSessions(prev => [session, ...prev.filter(s => s.id !== id)])
}

function applyRuntimeInfo(info: SessionCreateResponse['info'] | undefined) {
  if (!info) {
    return
  }

  if (info.credential_warning) {
    requestDesktopOnboarding(info.credential_warning)
  }

  if (info.model) {
    setCurrentModel(info.model)
  }

  if (info.provider) {
    setCurrentProvider(info.provider)
  }

  if (info.cwd) {
    setCurrentCwd(info.cwd)
  }

  if (info.branch !== undefined) {
    setCurrentBranch(info.branch || '')
  }

  if (typeof info.personality === 'string') {
    setCurrentPersonality(normalizePersonalityValue(info.personality))
  }

  if (typeof info.reasoning_effort === 'string') {
    setCurrentReasoningEffort(info.reasoning_effort)
  }

  if (typeof info.service_tier === 'string') {
    setCurrentServiceTier(info.service_tier)
  }

  if (typeof info.fast === 'boolean') {
    setCurrentFastMode(info.fast)
  }

  if (info.usage) {
    setCurrentUsage(current => ({ ...current, ...info.usage }))
  }
}

export function useSessionActions({
  activeSessionId,
  activeSessionIdRef,
  busyRef,
  creatingSessionRef,
  ensureSessionState,
  getRouteToken,
  navigate,
  requestGateway,
  runtimeIdByStoredSessionIdRef,
  selectedStoredSessionId,
  selectedStoredSessionIdRef,
  sessionStateByRuntimeIdRef,
  syncSessionStateToView,
  updateSessionState
}: SessionActionsOptions) {
  const resumeRequestRef = useRef(0)

  const startFreshSessionDraft = useCallback(
    (replaceRoute = false) => {
      busyRef.current = false
      setBusy(false)
      setAwaitingResponse(false)
      clearNotifications()
      setIntroSeed(seed => seed + 1)
      navigate(NEW_CHAT_ROUTE, { replace: replaceRoute })
      setActiveSessionId(null)
      activeSessionIdRef.current = null
      setSelectedStoredSessionId(null)
      selectedStoredSessionIdRef.current = null
      setMessages([])
      setCurrentUsage({
        calls: 0,
        input: 0,
        output: 0,
        total: 0
      })
      setSessionStartedAt(null)
      setTurnStartedAt(null)
      clearComposerDraft()
      clearComposerAttachments()
      setFreshDraftReady(true)
    },
    [activeSessionIdRef, busyRef, navigate, selectedStoredSessionIdRef]
  )

  const createBackendSessionForSend = useCallback(async (): Promise<string | null> => {
    const startingActiveSessionId = activeSessionIdRef.current
    const startingStoredSessionId = selectedStoredSessionIdRef.current
    const startingRouteToken = getRouteToken()

    creatingSessionRef.current = true

    try {
      const created = await requestGateway<SessionCreateResponse>('session.create', { cols: 96 })
      const stored = created.stored_session_id ?? null

      if (
        activeSessionIdRef.current !== startingActiveSessionId ||
        selectedStoredSessionIdRef.current !== startingStoredSessionId ||
        getRouteToken() !== startingRouteToken
      ) {
        await requestGateway('session.close', { session_id: created.session_id }).catch(() => undefined)

        return null
      }

      activeSessionIdRef.current = created.session_id
      selectedStoredSessionIdRef.current = stored
      ensureSessionState(created.session_id, stored)

      if (stored) {
        upsertOptimisticSession(created, stored)
        navigate(sessionRoute(stored), { replace: true })
      }

      setFreshDraftReady(false)
      setActiveSessionId(created.session_id)
      setSelectedStoredSessionId(stored)
      setSessionStartedAt(Date.now())
      applyRuntimeInfo(created.info)

      return created.session_id
    } finally {
      window.setTimeout(() => {
        creatingSessionRef.current = false
      }, 0)
    }
  }, [
    activeSessionIdRef,
    creatingSessionRef,
    ensureSessionState,
    getRouteToken,
    navigate,
    requestGateway,
    selectedStoredSessionIdRef
  ])

  const selectSidebarItem = useCallback(
    (item: SidebarNavItem) => {
      if (item.action === 'new-session') {
        startFreshSessionDraft()

        return
      }

      if (item.route) {
        navigate(item.route)
      }
    },
    [navigate, startFreshSessionDraft]
  )

  const openSettings = useCallback(() => {
    navigate(SETTINGS_ROUTE)
  }, [navigate])

  const closeSettings = useCallback(() => {
    if (selectedStoredSessionId) {
      navigate(sessionRoute(selectedStoredSessionId))

      return
    }

    navigate(NEW_CHAT_ROUTE)
  }, [navigate, selectedStoredSessionId])

  const resumeSession = useCallback(
    async (storedSessionId: string, replaceRoute = false) => {
      const requestId = resumeRequestRef.current + 1
      resumeRequestRef.current = requestId

      const isCurrentResume = () =>
        resumeRequestRef.current === requestId && selectedStoredSessionIdRef.current === storedSessionId

      const cachedRuntimeId = runtimeIdByStoredSessionIdRef.current.get(storedSessionId)
      const cachedState = cachedRuntimeId && sessionStateByRuntimeIdRef.current.get(cachedRuntimeId)

      if (cachedRuntimeId && cachedState) {
        setFreshDraftReady(false)
        clearNotifications()
        setSelectedStoredSessionId(storedSessionId)
        selectedStoredSessionIdRef.current = storedSessionId
        setActiveSessionId(cachedRuntimeId)
        activeSessionIdRef.current = cachedRuntimeId
        syncSessionStateToView(cachedRuntimeId, cachedState)
        setSessionStartedAt(Date.now())
        clearComposerDraft()
        clearComposerAttachments()

        void requestGateway<UsageStats>('session.usage', { session_id: cachedRuntimeId })
          .then(usage => {
            if (isCurrentResume() && usage) {
              setCurrentUsage(current => ({ ...current, ...usage }))
            }
          })
          .catch(() => undefined)

        return
      }

      setFreshDraftReady(false)
      setActiveSessionId(null)
      activeSessionIdRef.current = null
      busyRef.current = true
      setBusy(true)
      setAwaitingResponse(false)
      clearNotifications()
      setSelectedStoredSessionId(storedSessionId)
      selectedStoredSessionIdRef.current = storedSessionId
      setSessionStartedAt(Date.now())
      const stored = $sessions.get().find(session => session.id === storedSessionId)

      if (stored) {
        setCurrentUsage(current => ({
          ...current,
          input: stored.input_tokens || 0,
          output: stored.output_tokens || 0,
          total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
        }))
      }

      try {
        // Load the local snapshot first, then ask the gateway to resume.
        // Previously these raced:
        //   1. clear messages to []
        //   2. local getSessionMessages -> 45 msgs
        //   3. a second resume path cleared [] again
        //   4. gateway resume -> 43 msgs
        // That is the ctrl+R flash chain. Avoid showing an empty thread
        // while we already have a route-scoped session id, and don't race the
        // local snapshot against gateway resume.
        let localSnapshot = $messages.get()

        try {
          const storedMessages = await getSessionMessages(storedSessionId)

          if (isCurrentResume()) {
            localSnapshot = toChatMessages(storedMessages.messages)

            if (!chatMessageArraysEquivalent($messages.get(), localSnapshot)) {
              setMessages(localSnapshot)
            }
          }
        } catch {
          // Non-fatal: gateway resume below can still hydrate the session.
        }

        const resumed = await requestGateway<SessionResumeResponse>('session.resume', {
          session_id: storedSessionId,
          cols: 96
        })

        if (!isCurrentResume()) {
          return
        }

        const currentMessages = $messages.get()
        const resumedMessages = reconcileResumeMessages(toChatMessages(resumed.messages), currentMessages)
        // Avoid a second visible transcript rebuild on resume/switch.
        // `getSessionMessages()` is the stable stored transcript snapshot and
        // paints first; `session.resume` can return a slightly different
        // runtime-shaped projection (e.g. tool/system coalescing), which was
        // causing a second full message-list replacement a second later.
        // Keep the already-painted local snapshot for the view/cache when it
        // exists; use gateway messages only as a fallback when no local
        // snapshot was available.

        const messagesForView =
          localSnapshot.length > 0
            ? localSnapshot
            : chatMessageArraysEquivalent(currentMessages, resumedMessages)
              ? currentMessages
              : resumedMessages

        setActiveSessionId(resumed.session_id)
        activeSessionIdRef.current = resumed.session_id
        updateSessionState(
          resumed.session_id,
          state => ({
            ...state,
            messages: messagesForView,
            busy: false,
            awaitingResponse: false
          }),
          storedSessionId
        )
        clearComposerDraft()
        clearComposerAttachments()
        applyRuntimeInfo(resumed.info)
      } catch (err) {
        if (!isCurrentResume()) {
          return
        }

        const fallback = await getSessionMessages(storedSessionId)

        if (!isCurrentResume()) {
          return
        }

        setMessages(toChatMessages(fallback.messages))
        notifyError(err, 'Resume failed')
      } finally {
        if (isCurrentResume()) {
          busyRef.current = false
          setBusy(false)
          setAwaitingResponse(false)
        }
      }
    },
    [
      activeSessionIdRef,
      busyRef,
      requestGateway,
      runtimeIdByStoredSessionIdRef,
      selectedStoredSessionIdRef,
      sessionStateByRuntimeIdRef,
      syncSessionStateToView,
      updateSessionState
    ]
  )

  const branchCurrentSession = useCallback(
    async (messageId?: string): Promise<boolean> => {
      const sourceSessionId = activeSessionIdRef.current

      if (!sourceSessionId) {
        notify({
          kind: 'warning',
          title: 'Nothing to branch',
          message: 'Start or resume a chat before branching.'
        })

        return false
      }

      if (busyRef.current) {
        notify({
          kind: 'warning',
          title: 'Session busy',
          message: 'Stop the current turn before branching this chat.'
        })

        return false
      }

      creatingSessionRef.current = true

      try {
        const currentMessages = $messages.get()

        const targetIndex = messageId
          ? currentMessages.findIndex(message => message.id === messageId)
          : currentMessages.findLastIndex(message => message.role === 'assistant' || message.role === 'user')

        const branchStart = targetIndex >= 0 ? targetIndex : Math.max(currentMessages.length - 1, 0)
        const branchEnd = targetIndex >= 0 ? targetIndex + 1 : currentMessages.length

        const branchMessages = currentMessages
          .slice(branchStart, branchEnd)
          .map(message => ({
            content: chatMessageText(message),
            source: message,
            role: message.role
          }))
          .filter(message => message.content.trim() && ['assistant', 'user'].includes(message.role))

        if (!branchMessages.length) {
          notify({
            kind: 'warning',
            title: 'Nothing to branch',
            message: 'This message has no text to branch from.'
          })

          return false
        }

        clearNotifications()

        const branched = await requestGateway<SessionCreateResponse>('session.create', {
          cols: 96,
          messages: branchMessages.map(({ content, role }) => ({ content, role })),
          title: 'Branch'
        })

        const routedSessionId = branched.stored_session_id ?? branched.session_id
        const preview = branchMessages.map(({ content }) => content).find(Boolean) ?? null

        setFreshDraftReady(false)
        upsertOptimisticSession(branched, routedSessionId, 'Branch', preview)
        ensureSessionState(branched.session_id, routedSessionId)
        setActiveSessionId(branched.session_id)
        activeSessionIdRef.current = branched.session_id
        updateSessionState(
          branched.session_id,
          state => ({
            ...state,
            messages: branchMessages.map(({ source }) => source),
            busy: false,
            awaitingResponse: false
          }),
          routedSessionId
        )
        setSelectedStoredSessionId(routedSessionId)
        selectedStoredSessionIdRef.current = routedSessionId
        navigate(sessionRoute(routedSessionId))

        clearComposerDraft()
        clearComposerAttachments()
        applyRuntimeInfo(branched.info)

        return true
      } catch (err) {
        notifyError(err, 'Branch failed')

        return false
      } finally {
        window.setTimeout(() => {
          creatingSessionRef.current = false
        }, 0)
      }
    },
    [
      activeSessionIdRef,
      busyRef,
      creatingSessionRef,
      ensureSessionState,
      navigate,
      requestGateway,
      selectedStoredSessionIdRef,
      updateSessionState
    ]
  )

  const removeSession = useCallback(
    async (storedSessionId: string) => {
      clearNotifications()

      const removed = $sessions.get().find(s => s.id === storedSessionId)
      const wasSelected = selectedStoredSessionId === storedSessionId
      const closingRuntimeId = wasSelected ? activeSessionId : null
      const previousMessages = $messages.get()
      const previousPinned = $pinnedSessionIds.get()

      setSessions(prev => prev.filter(s => s.id !== storedSessionId))
      $pinnedSessionIds.set(previousPinned.filter(id => id !== storedSessionId))

      // Tear down before awaiting so the route effect can't resume the
      // doomed session via the stale /<sid> URL.
      if (wasSelected) {
        startFreshSessionDraft(true)
      }

      try {
        if (closingRuntimeId) {
          await requestGateway('session.close', { session_id: closingRuntimeId }).catch(() => undefined)
        }

        await deleteSession(storedSessionId)
      } catch (err) {
        if (removed) {
          setSessions(prev => [removed, ...prev])
        }

        $pinnedSessionIds.set(previousPinned)

        if (wasSelected) {
          setFreshDraftReady(false)
          setSelectedStoredSessionId(storedSessionId)
          selectedStoredSessionIdRef.current = storedSessionId
          const stored = $sessions.get().find(session => session.id === storedSessionId)

          if (stored) {
            setCurrentUsage(current => ({
              ...current,
              input: stored.input_tokens || 0,
              output: stored.output_tokens || 0,
              total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
            }))
          }

          setMessages(previousMessages)
          navigate(sessionRoute(storedSessionId), { replace: true })

          if (closingRuntimeId) {
            setActiveSessionId(closingRuntimeId)
            activeSessionIdRef.current = closingRuntimeId
          }
        }

        notifyError(err, 'Delete failed')
      }
    },
    [
      activeSessionId,
      activeSessionIdRef,
      navigate,
      requestGateway,
      selectedStoredSessionId,
      selectedStoredSessionIdRef,
      startFreshSessionDraft
    ]
  )

  return {
    branchCurrentSession,
    closeSettings,
    createBackendSessionForSend,
    openSettings,
    removeSession,
    resumeSession,
    selectSidebarItem,
    startFreshSessionDraft
  }
}
