/**
 * Streaming card controller for Feishu CardKit v2.
 *
 * Manages the full lifecycle of a streaming card:
 *   idle → creating → streaming → completed / aborted / creation_failed
 *
 * Uses FlushController for throttled updates and delegates
 * CardKit API calls through the FeishuAdapter.
 *
 * Adapted from openclaw-lark's card/streaming-card-controller.ts.
 */

import type { ChannelAdapter } from '../base'
import type { FeishuAdapter } from './adapter'
import type { CardPhase, CardKitState, FeishuCardConfig } from '../../types'
import type { ToolUseStartInfo, ToolUseEndInfo } from '../../types'
import { CARD_PHASES, TERMINAL_PHASES, PHASE_TRANSITIONS } from '../../types'
import {
  STREAMING_ELEMENT_ID,
  buildCardContent,
  buildStreamingThinkingCard,
  toCardKit2,
  buildMarkdownCard,
} from './card-builder'
import type { ToolUseDisplayStep } from './card-builder'
import { isCardRateLimitError, isCardTableLimitError, sanitizeTextForCard } from './card-error'
import { optimizeMarkdownStyle } from './markdown-style'
import { FlushController, THROTTLE_MS } from './flush-controller'
import { UnavailableGuard } from './unavailable-guard'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface StreamingTextState {
  accumulatedText: string
  completedText: string
  streamingPrefix: string
  lastPartialText: string
  lastFlushedText: string
}

interface ReasoningState {
  accumulatedReasoningText: string
  startTime: number | null
  elapsedMs: number
  isActive: boolean
}

export interface StreamingCardDeps {
  adapter: FeishuAdapter
  chatId: string
  footer: FeishuCardConfig['footer']
}

type TerminalReason = 'normal' | 'error' | 'abort' | 'unavailable' | 'creation_failed'

const EMPTY_REPLY_FALLBACK = '_(No response)_'

/** Map tool names to user-friendly titles and icon tokens. */
const TOOL_ICONS: Record<string, { title: string; iconToken: string }> = {
  read: { title: 'Read', iconToken: 'file-link-text_outlined' },
  write: { title: 'Edit', iconToken: 'edit_outlined' },
  edit: { title: 'Edit', iconToken: 'edit_outlined' },
  bash: { title: 'Run command', iconToken: 'setting_outlined' },
  web_search: { title: 'Search web', iconToken: 'search_outlined' },
  search: { title: 'Search', iconToken: 'search_outlined' },
  skill: { title: 'Load skill', iconToken: 'app-default_outlined' },
}

// ---------------------------------------------------------------------------
// StreamingCardController
// ---------------------------------------------------------------------------

export class StreamingCardController {
  private phase: CardPhase = CARD_PHASES.idle
  private cardKit: CardKitState = {
    cardKitCardId: null,
    originalCardKitCardId: null,
    cardKitSequence: 0,
    cardMessageId: null,
  }
  private text: StreamingTextState = {
    accumulatedText: '',
    completedText: '',
    streamingPrefix: '',
    lastPartialText: '',
    lastFlushedText: '',
  }
  private reasoning: ReasoningState = {
    accumulatedReasoningText: '',
    startTime: null,
    elapsedMs: 0,
    isActive: false,
  }
  private toolUseSteps: ToolUseDisplayStep[] = []
  private toolUseStartTime: number | null = null
  private readonly guard = new UnavailableGuard({
    onTerminate: () => {
      console.log('[StreamingCard] UnavailableGuard terminated, no more updates will be sent')
    },
  })
  private readonly flush: FlushController
  private createEpoch = 0
  private cardCreationPromise: Promise<void> | null = null
  private _terminalReason: TerminalReason | null = null
  private dispatchFullyComplete = false
  private readonly startTime = Date.now()
  private readonly deps: StreamingCardDeps

  private elapsed(): number {
    return Date.now() - this.startTime
  }

  constructor(deps: StreamingCardDeps) {
    this.deps = deps
    this.flush = new FlushController(() => this.performFlush())
  }

  // ---------------------------------------------------------------------------
  // Public accessors
  // ---------------------------------------------------------------------------

  get cardMessageId(): string | null {
    return this.cardKit.cardMessageId
  }

  get isTerminalPhase(): boolean {
    return TERMINAL_PHASES.has(this.phase)
  }

  get isAborted(): boolean {
    return this.phase === CARD_PHASES.aborted
  }

  get currentPhase(): CardPhase {
    return this.phase
  }

  get terminalReason(): TerminalReason | null {
    return this._terminalReason
  }

  // ---------------------------------------------------------------------------
  // State machine
  // ---------------------------------------------------------------------------

  private transition(to: CardPhase, source: string, reason?: TerminalReason): boolean {
    const from = this.phase
    if (from === to) return false
    const allowed = PHASE_TRANSITIONS[from]
    if (!allowed.has(to)) {
      console.warn(`[StreamingCard] phase transition rejected: ${from} → ${to} (${source})`)
      return false
    }
    this.phase = to
    console.log(`[StreamingCard] phase: ${from} → ${to} (${source}${reason ? `, reason=${reason}` : ''})`)
    if (TERMINAL_PHASES.has(to)) {
      this._terminalReason = reason ?? null
      this.onEnterTerminalPhase()
    }
    return true
  }

  private onEnterTerminalPhase(): void {
    this.createEpoch += 1
    this.flush.cancelPendingFlush()
    this.flush.complete()
  }

  private isStaleCreate(epoch: number): boolean {
    return epoch !== this.createEpoch
  }

  // ---------------------------------------------------------------------------
  // SDK callback bindings
  // ---------------------------------------------------------------------------

  /**
   * Handle onTyping — create thinking card placeholder.
   */
  async onTyping(): Promise<void> {
    if (this.isTerminalPhase) return
    await this.ensureCardCreated()
  }

  /**
   * Handle reasoning/thinking text from the SDK stream.
   * Accumulates reasoning text and triggers a card flush.
   */
  async onReasoning(text: string): Promise<void> {
    if (this.isTerminalPhase) return
    if (!this.reasoning.startTime) {
      this.reasoning.startTime = Date.now()
    }
    this.reasoning.accumulatedReasoningText = text
    // Keep isActive true as long as we're receiving reasoning text.
    // It will be set to false in onFinal when the reply completes.
    this.reasoning.isActive = true

    await this.ensureCardCreated()
    if (!this.cardKit.cardMessageId || this.isTerminalPhase) return

    const throttleMs = this.cardKit.cardKitCardId ? THROTTLE_MS.cardkit : THROTTLE_MS.imPatch
    await this.flush.throttledUpdate(throttleMs)
  }

  /**
   * Handle tool use start event — add a "running" step.
   */
  async onToolUseStart(tool: ToolUseStartInfo): Promise<void> {
    if (this.isTerminalPhase) return
    if (!this.toolUseStartTime) {
      this.toolUseStartTime = Date.now()
    }

    // Find the icon token and build a readable title for the tool
    const { title, iconToken } = this.formatToolStep(tool.name, tool.input)

    this.toolUseSteps.push({
      title,
      status: 'running',
      iconToken,
      detail: this.formatToolDetail(tool.name, tool.input),
    })

    await this.ensureCardCreated()
    if (!this.cardKit.cardMessageId || this.isTerminalPhase) return

    // In CardKit streaming mode, rebuild the full card with tool use panel
    if (this.cardKit.cardKitCardId) {
      await this.rebuildCardKitCardWithToolUse()
    } else {
      const throttleMs = THROTTLE_MS.imPatch
      await this.flush.throttledUpdate(throttleMs)
    }
  }

  /**
   * Handle tool use end event — update step to success/error.
   */
  async onToolUseEnd(tool: ToolUseEndInfo): Promise<void> {
    if (this.isTerminalPhase) return

    // Find the running step with this tool name and update it
    const step = [...this.toolUseSteps].reverse().find(
      s => s.status === 'running' && s.title === this.formatToolStep(tool.name, {}).title
    )
    if (step) {
      step.status = tool.status === 'error' ? 'error' : 'success'
      if (tool.result) {
        step.resultBlock = { content: this.truncateToolResult(tool.result), language: 'text' }
      }
      if (tool.error) {
        step.errorBlock = { content: this.truncateToolResult(tool.error), language: 'text' }
      }
    }

    if (!this.cardKit.cardMessageId || this.isTerminalPhase) return

    // In CardKit streaming mode, rebuild the full card with tool use panel
    if (this.cardKit.cardKitCardId) {
      await this.rebuildCardKitCardWithToolUse()
    } else {
      const throttleMs = THROTTLE_MS.imPatch
      await this.flush.throttledUpdate(throttleMs)
    }
  }

  /**
   * Handle onDraft partial text update.
   */
  async onDraft(partialText: string): Promise<void> {
    if (this.isTerminalPhase) return
    if (!partialText) return

    // Detect reply boundary: text length decreased → new reply starts
    if (this.text.lastPartialText && partialText.length < this.text.lastPartialText.length) {
      this.text.streamingPrefix += (this.text.streamingPrefix ? '\n\n' : '') + this.text.lastPartialText
    }
    this.text.lastPartialText = partialText
    this.text.accumulatedText = this.text.streamingPrefix
      ? this.text.streamingPrefix + '\n\n' + partialText
      : partialText

    await this.ensureCardCreated()
    if (!this.cardKit.cardMessageId || this.isTerminalPhase) return

    const throttleMs = this.cardKit.cardKitCardId ? THROTTLE_MS.cardkit : THROTTLE_MS.imPatch
    await this.flush.throttledUpdate(throttleMs)
  }

  /**
   * Handle onFinal — complete the card with final text.
   */
  async onFinal(text: string): Promise<void> {
    if (this.isTerminalPhase) return

    this.text.completedText = text
    if (!this.text.accumulatedText) {
      this.text.accumulatedText = text
    }
    this.dispatchFullyComplete = true

    // Mark reasoning as complete and compute elapsed time
    this.reasoning.elapsedMs = this.reasoning.startTime
      ? Date.now() - this.reasoning.startTime
      : 0
    this.reasoning.isActive = false

    // Mark all running tool use steps as success (they should have been completed by now)
    for (const step of this.toolUseSteps) {
      if (step.status === 'running') {
        step.status = 'success'
      }
    }

    this.transition(CARD_PHASES.completed, 'onFinal', 'normal')
    this.flush.cancelPendingFlush()
    this.flush.complete()

    await this.flush.waitForFlush()
    if (this.cardCreationPromise) await this.cardCreationPromise

    const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId

    // Prepare final card params with reasoning and tool use data
    const finalCardParams = {
      text: sanitizeTextForCard(this.text.completedText || this.text.accumulatedText || EMPTY_REPLY_FALLBACK),
      elapsedMs: this.elapsed(),
      reasoningText: this.reasoning.accumulatedReasoningText || undefined,
      reasoningElapsedMs: this.reasoning.elapsedMs || undefined,
      showToolUse: true as const,
      toolUseSteps: this.toolUseSteps.length > 0 ? this.toolUseSteps : undefined,
      toolUseElapsedMs: this.toolUseStartTime ? Date.now() - this.toolUseStartTime : undefined,
      footer: this.deps.footer,
    }

    if (this.cardKit.cardMessageId && effectiveCardId) {
      // CardKit flow: close streaming + final update
      const finalCard = buildCardContent('complete', finalCardParams)

      try {
        this.cardKit.cardKitSequence += 1
        await this.deps.adapter.cardKitSetStreamingMode(effectiveCardId, false, this.cardKit.cardKitSequence)
        this.cardKit.cardKitSequence += 1
        await this.deps.adapter.cardKitUpdateCard(effectiveCardId, toCardKit2(finalCard), this.cardKit.cardKitSequence)
        console.log(`[StreamingCard] Final card updated via CardKit (cardId=${effectiveCardId})`)
      } catch (err) {
        console.warn('[StreamingCard] CardKit final update failed:', err instanceof Error ? err.message : err)
      }
    } else if (this.cardKit.cardMessageId) {
      // IM PATCH fallback
      const finalCard = buildCardContent('complete', finalCardParams)
      try {
        await this.deps.adapter.updateCard(this.cardKit.cardMessageId, finalCard as unknown as Record<string, unknown>)
        console.log(`[StreamingCard] Final card updated via IM PATCH (msgId=${this.cardKit.cardMessageId})`)
      } catch (err) {
        console.warn('[StreamingCard] IM PATCH final update failed:', err instanceof Error ? err.message : err)
      }
    }
  }

  /**
   * Abort the card — used for timeout or /stop command.
   */
  async abortCard(): Promise<void> {
    if (!this.transition(CARD_PHASES.aborted, 'abortCard', 'abort')) return

    await this.flush.waitForFlush()
    if (this.cardCreationPromise) await this.cardCreationPromise

    const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId
    const displayText = this.text.accumulatedText || 'Aborted.'
    const sanitized = sanitizeTextForCard(displayText)

    if (effectiveCardId) {
      const abortCard = buildCardContent('complete', {
        text: sanitized,
        elapsedMs: this.elapsed(),
        isAborted: true,
        footer: this.deps.footer,
      })
      try {
        await this.closeStreamingAndUpdate(effectiveCardId, abortCard)
      } catch (err) {
        console.warn('[StreamingCard] abortCard closeStreamingAndUpdate failed:', err instanceof Error ? err.message : err)
      }
    } else if (this.cardKit.cardMessageId) {
      const abortCard = buildCardContent('complete', {
        text: sanitized,
        elapsedMs: this.elapsed(),
        isAborted: true,
        footer: this.deps.footer,
      })
      try {
        await this.deps.adapter.updateCard(this.cardKit.cardMessageId, abortCard as unknown as Record<string, unknown>)
      } catch (err) {
        console.warn('[StreamingCard] abortCard IM PATCH failed:', err instanceof Error ? err.message : err)
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Internal: card creation
  // ---------------------------------------------------------------------------

  private async ensureCardCreated(): Promise<void> {
    if (this.cardKit.cardMessageId || this.phase === CARD_PHASES.creation_failed || this.isTerminalPhase) return
    if (this.cardCreationPromise) {
      await this.cardCreationPromise
      return
    }
    if (!this.transition(CARD_PHASES.creating, 'ensureCardCreated')) return
    this.createEpoch += 1
    const epoch = this.createEpoch

    this.cardCreationPromise = (async () => {
      try {
        // Step 1: Create CardKit entity
        const thinkingCard = buildStreamingThinkingCard()
        const cardId = await this.deps.adapter.cardKitCreateCardEntity(thinkingCard)

        if (this.isStaleCreate(epoch)) return

        if (cardId) {
          // CardKit flow
          this.cardKit.cardKitCardId = cardId
          this.cardKit.originalCardKitCardId = cardId
          this.cardKit.cardKitSequence = 1
          console.log(`[StreamingCard] CardKit entity created: cardId=${cardId}`)

          // Step 2: Send IM message referencing card_id
          const result = await this.deps.adapter.cardKitSendCardByCardId(this.deps.chatId, cardId)

          if (this.isStaleCreate(epoch)) return

          if (result?.messageId) {
            this.cardKit.cardMessageId = result.messageId
            this.flush.setCardMessageReady(true)
            this.transition(CARD_PHASES.streaming, 'ensureCardCreated.cardkit')
            console.log(`[StreamingCard] Card sent: msgId=${result.messageId}`)
          } else {
            throw new Error('sendCardByCardId returned no messageId')
          }
        } else {
          // CardKit creation failed — fall back to IM interactive card
          console.warn('[StreamingCard] CardKit create returned null, falling back to IM card')
          this.cardKit.cardKitCardId = null
          this.cardKit.originalCardKitCardId = null

          const fallbackCard = buildCardContent('streaming')
          const msgId = await this.deps.adapter.sendCard({
            chatId: this.deps.chatId,
            text: '',
            card: fallbackCard as unknown as Record<string, unknown>,
          })

          if (this.isStaleCreate(epoch)) return

          if (msgId) {
            this.cardKit.cardMessageId = msgId
            this.flush.setCardMessageReady(true)
            this.transition(CARD_PHASES.streaming, 'ensureCardCreated.imFallback')
            console.log(`[StreamingCard] IM fallback card sent: msgId=${msgId}`)
          }
        }
      } catch (err) {
        if (this.isStaleCreate(epoch)) return
        console.warn('[StreamingCard] Card creation failed:', err instanceof Error ? err.message : err)
        this.transition(CARD_PHASES.creation_failed, 'ensureCardCreated', 'creation_failed')
      }
    })()

    await this.cardCreationPromise
  }

  // ---------------------------------------------------------------------------
  // Internal: flush
  // ---------------------------------------------------------------------------

  private async performFlush(): Promise<void> {
    if (this.guard.isTerminated) return
    if (!this.cardKit.cardMessageId || this.isTerminalPhase) return

    // If CardKit streaming was disabled (table limit), skip intermediate updates
    if (!this.cardKit.cardKitCardId && this.cardKit.originalCardKitCardId) {
      return
    }

    try {
      const displayText = this.buildDisplayText()
      const optimized = optimizeMarkdownStyle(displayText)

      // Determine card params based on current state
      // reasonText: show reasoning panel whenever we have accumulated text
      // (during thinking phase AND after thinking completes, until final card)
      const reasonText = this.reasoning.accumulatedReasoningText || undefined
      const steps = this.toolUseSteps.length > 0 ? this.toolUseSteps : undefined

      if (this.cardKit.cardKitCardId) {
        // CardKit streaming: when reasoning or tool use is active, rebuild full card
        // so that tool use panel and reasoning panel are visible.
        // Otherwise, use fast element content API for text-only updates.
        if (reasonText || steps) {
          this.cardKit.cardKitSequence += 1
          const streamingCard = buildCardContent('streaming', {
            text: optimized,
            reasoningText: reasonText,
            showToolUse: true,
            toolUseSteps: steps,
          })
          await this.deps.adapter.cardKitUpdateCard(
            this.cardKit.cardKitCardId,
            toCardKit2(streamingCard),
            this.cardKit.cardKitSequence,
          )
          this.text.lastFlushedText = optimized
        } else if (optimized !== this.text.lastFlushedText) {
          this.cardKit.cardKitSequence += 1
          await this.deps.adapter.cardKitStreamCardContent(
            this.cardKit.cardKitCardId,
            STREAMING_ELEMENT_ID,
            optimized,
            this.cardKit.cardKitSequence,
          )
          this.text.lastFlushedText = optimized
        }
      } else {
        // IM PATCH fallback: rebuild entire card
        const card = buildCardContent('streaming', {
          text: optimized,
          reasoningText: reasonText,
          showToolUse: true,
          toolUseSteps: steps,
        })
        await this.deps.adapter.updateCard(this.cardKit.cardMessageId, card as unknown as Record<string, unknown>)
      }
    } catch (err: unknown) {
      // Check for message unavailable (deleted/recalled)
      if (this.guard.terminate('performFlush', err)) {
        this.transition(CARD_PHASES.terminated, 'performFlush', 'unavailable')
        return
      }
      // Rate limit (230020) — skip this frame, don't degrade
      if (isCardRateLimitError(err)) {
        console.info('[StreamingCard] Rate limited (230020), skipping frame')
        return
      }
      // Table limit (230099/11310) — disable CardKit streaming, keep originalCardKitCardId for final update
      if (isCardTableLimitError(err)) {
        console.warn('[StreamingCard] Card table limit exceeded, disabling CardKit streaming')
        this.cardKit.cardKitCardId = null
        return
      }
      console.error('[StreamingCard] Flush failed:', err instanceof Error ? err.message : err)
      // Disable CardKit on unknown errors
      if (this.cardKit.cardKitCardId) {
        console.warn('[StreamingCard] Disabling CardKit streaming, falling back to IM PATCH')
        this.cardKit.cardKitCardId = null
      }
    }
  }

  private buildDisplayText(): string {
    // During streaming, the text element always shows the answer text.
    // Reasoning is shown as a separate panel (not embedded in text),
    // so buildDisplayText returns only the answer text.
    return this.text.accumulatedText
  }

  // ---------------------------------------------------------------------------
  // Internal: lifecycle helpers
  // ---------------------------------------------------------------------------

  private async closeStreamingAndUpdate(cardId: string, card: ReturnType<typeof buildCardContent>): Promise<void> {
    this.cardKit.cardKitSequence += 1
    await this.deps.adapter.cardKitSetStreamingMode(cardId, false, this.cardKit.cardKitSequence)
    this.cardKit.cardKitSequence += 1
    await this.deps.adapter.cardKitUpdateCard(cardId, toCardKit2(card), this.cardKit.cardKitSequence)
  }

  /** Clean up all resources. */
  destroy(): void {
    this.flush.destroy()
  }

  // ---------------------------------------------------------------------------
  // Internal: tool use formatting helpers
  // ---------------------------------------------------------------------------

  /** Map tool names to user-friendly titles and icon tokens. */
  private formatToolStep(name: string, _input: Record<string, unknown>): { title: string; iconToken: string } {
    const entry = TOOL_ICONS[name.toLowerCase()]
    if (entry) return entry

    // Fallback: use the raw tool name as title with a generic icon
    const title = name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' ')
    return { title, iconToken: 'setting_outlined' }
  }

  /** Format a short detail line for a tool use step (e.g. file path or search query). */
  private formatToolDetail(name: string, input: Record<string, unknown>): string | undefined {
    const inputStrs: string[] = []
    // Show first 1-2 meaningful params
    for (const [key, val] of Object.entries(input)) {
      if (key === 'command') {
        // Truncate long commands
        const cmd = String(val)
        inputStrs.push(cmd.length > 80 ? cmd.slice(0, 77) + '...' : cmd)
        break
      }
      if (key === 'file_path' || key === 'path' || key === 'query' || key === 'pattern') {
        inputStrs.push(String(val))
        if (inputStrs.length >= 2) break
      }
    }
    return inputStrs.join(' · ') || undefined
  }

  /** Truncate large tool results for display in cards. */
  private truncateToolResult(text: string, maxLength = 500): string {
    if (text.length <= maxLength) return text
    return text.slice(0, maxLength - 3) + '...'
  }

  /** Rebuild the CardKit card with tool use panel during streaming. */
  private async rebuildCardKitCardWithToolUse(): Promise<void> {
    // Delegate to performFlush which already handles building the card
    // with reasonText + toolUseSteps for CardKit streaming mode.
    await this.performFlush()
  }
}