/** @jsxImportSource @opentui/solid */
/**
 * Output Window Component
 * Ported from: src/ui/components/OutputWindow.tsx
 *
 * Display current agent's output with syntax highlighting
 * Shows last N lines with auto-scroll for running agents using OpenTUI scrollbox
 */

import type { ScrollBoxRenderable } from "@opentui/core"
import { Show, For, createMemo, createSignal, createEffect, onCleanup, on } from "solid-js"
import { useTheme } from "@tui/shared/context/theme"
import { debug } from "../../../../../../shared/logging/logger.js"
import { ShimmerText } from "./shimmer-text"
import { TypingText } from "./typing-text"
import { LogLine } from "../shared/log-line"
import { LogTable } from "../shared/log-table"
import { groupLinesWithTables } from "../shared/markdown-table"
import { PromptLine, type PromptLineState, type ChainConfirmRequest } from "./prompt-line"
import type { AgentState, SubAgentState, InputState, ControllerState } from "../../state/types"
import { getFace, getPhrase } from "../../../../shared/config/agent-characters.js"
import type { ActivityType } from "../../../../shared/config/agent-characters.types.js"

// Rotating messages shown while connecting to agent
const CONNECTING_MESSAGES = [
  "Initializing agent environment",
  "Loading agent configuration",
  "Starting execution engine",
  "Establishing log stream",
  "Agent starting up",
]

export interface OutputWindowProps {
  currentAgent: AgentState | SubAgentState | null
  controllerState?: ControllerState | null
  lines: string[]
  isLoading: boolean
  isConnecting: boolean
  error: string | null
  maxLines?: number
  connectingMessageIndex?: number
  latestThinking?: string | null
  currentActivity?: ActivityType | null
  // Prompt line props
  inputState?: InputState | null
  workflowStatus?: string
  isPromptBoxFocused?: boolean
  onPromptSubmit?: (prompt: string) => void
  onSkip?: () => void
  onPromptBoxFocusExit?: () => void
  // Responsive layout
  availableWidth?: number
  // Backward pagination
  hasMoreAbove?: boolean
  isLoadingEarlier?: boolean
  loadEarlierError?: string | null
  onLoadMore?: () => number
  onPauseTrimmingChange?: (paused: boolean) => void
  // Chain confirm modal (rendered at top level)
  onShowChainConfirm?: (request: ChainConfirmRequest) => void
}

/**
 * Output window showing current agent's output
 * Displays last N lines with syntax highlighting using scrollbox
 */
const OUTPUT_HEADER_HEIGHT_WIDE = 4  // Side curve header when wide (╭─ + info + thinking + ╰─)
const OUTPUT_HEADER_HEIGHT_NARROW = 5  // Side curve header when narrow (extra line for status)
const MIN_WIDTH_FOR_INLINE_STATUS = 75  // Minimum width to show status inline with name

export function OutputWindow(props: OutputWindowProps) {
  const themeCtx = useTheme()
  const [scrollRef, setScrollRef] = createSignal<ScrollBoxRenderable | undefined>()
  const [userScrolledAway, setUserScrolledAway] = createSignal(false)
  // Scroll lock prevents ALL scroll handling during content prepend operations
  const [scrollLock, setScrollLock] = createSignal(false)
  // Saved scroll position to restore after content updates when user has scrolled away
  const [savedScrollTop, setSavedScrollTop] = createSignal<number | null>(null)

  // Reset userScrolledAway when agent changes
  // Use on() to explicitly track only the agent name, not other props
  createEffect(
    on(
      () => props.currentAgent?.name,
      (agentName) => {
        debug('[OutputWindow] Agent changed to: %s, resetting scroll state', agentName)
        setUserScrolledAway(false)
        setSavedScrollTop(null)
      }
    )
  )

  // Restore scroll position after content changes when user has scrolled away
  // This prevents the view from jumping when new lines are added
  createEffect(
    on(
      () => props.lines.length, // Track when lines change
      () => {
        const ref = scrollRef()
        const saved = savedScrollTop()
        if (ref && userScrolledAway() && saved !== null && !scrollLock()) {
          // Content changed while user is scrolled away - restore position
          if (ref.scrollTop !== saved) {
            debug('[OutputWindow] Restoring scroll position: current=%d, saved=%d', ref.scrollTop, saved)
            ref.scrollTop = saved
          }
        }
      }
    )
  )

  // Handle scroll events: load earlier lines + track if user scrolled away from bottom
  // Use on() to only track scrollRef changes, not other props
  createEffect(
    on(scrollRef, (ref) => {
      debug('[OutputWindow] Scroll effect running, ref exists: %s', !!ref)
      if (!ref) return

      // PRELOAD_THRESHOLD: Load earlier for smoother experience
      // When user is 15 lines from top, start loading
      const PRELOAD_THRESHOLD = 15

      const handleScrollChange = () => {
        // Absolute lock during content prepend operations - prevents ALL interference
        if (scrollLock()) {
          debug('[OutputWindow] Scroll event ignored - scroll lock active')
          return
        }

        const scrollTop = ref.scrollTop
        const scrollHeight = ref.scrollHeight
        const viewportHeight = ref.height
        const maxScroll = Math.max(0, scrollHeight - viewportHeight)
        const isAtBottom = scrollTop >= maxScroll - 3

        debug('[OutputWindow] scroll: top=%d, max=%d, atBottom=%s, hasMore=%s', scrollTop, maxScroll, isAtBottom, props.hasMoreAbove)

        // Track if user scrolled away from bottom (to disable stickyScroll)
        if (!isAtBottom && !userScrolledAway()) {
          debug('[OutputWindow] User scrolled away from bottom, saving scrollTop=%d', scrollTop)
          setUserScrolledAway(true)
          setSavedScrollTop(scrollTop) // Save position to restore after content updates
        } else if (isAtBottom && userScrolledAway()) {
          debug('[OutputWindow] User returned to bottom')
          setUserScrolledAway(false)
          setSavedScrollTop(null) // Clear saved position
        } else if (userScrolledAway()) {
          // User is scrolling while away from bottom - update saved position
          setSavedScrollTop(scrollTop)
        }

        // Preload when near the top
        if (scrollTop <= PRELOAD_THRESHOLD && props.hasMoreAbove && props.onLoadMore && !props.isLoadingEarlier) {
          debug('[OutputWindow] Preloading earlier lines (scrollTop=%d, threshold=%d)...', scrollTop, PRELOAD_THRESHOLD)

          // 1. Lock and detach listener to prevent any scroll events during operation
          setScrollLock(true)
          ref.verticalScrollBar?.off("change", handleScrollChange)

          // 2. Capture current scroll position BEFORE loading
          const originalScrollTop = scrollTop

          // 3. Load content (this updates state and triggers re-render)
          const linesLoaded = props.onLoadMore()
          debug('[OutputWindow] Lines loaded: %d', linesLoaded)

          if (linesLoaded > 0) {
            // 4. SYNCHRONOUS scroll adjustment - no async delay
            // Set scroll position immediately in the same execution frame
            // This prevents the flash by adjusting before the next paint
            const newScrollTop = originalScrollTop + linesLoaded
            ref.scrollTop = newScrollTop
            debug('[OutputWindow] Scroll restored (sync): original=%d, added=%d, new=%d',
              originalScrollTop, linesLoaded, ref.scrollTop)

            // 5. Use setImmediate only for re-attaching listener (after scroll is stable)
            setImmediate(() => {
              ref.verticalScrollBar?.on("change", handleScrollChange)
              setScrollLock(false)
              debug('[OutputWindow] Scroll lock released, listener re-attached')
            })
          } else {
            // No lines loaded, re-attach and unlock immediately
            ref.verticalScrollBar?.on("change", handleScrollChange)
            setScrollLock(false)
          }
        }
      }

      debug('[OutputWindow] Setting up scroll listener, verticalScrollBar exists: %s', !!ref.verticalScrollBar)
      ref.verticalScrollBar?.on("change", handleScrollChange)
      onCleanup(() => ref.verticalScrollBar?.off("change", handleScrollChange))
    })
  )

  // Compute whether stickyScroll should be active
  // Disable during scroll lock to prevent interference with scroll position restoration
  const shouldStickyScroll = () => !userScrolledAway() && !scrollLock()

  // Notify parent when user scrolls away (to pause trimming in log stream)
  createEffect(
    on(userScrolledAway, (scrolledAway) => {
      props.onPauseTrimmingChange?.(scrolledAway)
    })
  )

  // Check if we have enough width to show status inline (based on output section width)
  const isWideLayout = createMemo(() => (props.availableWidth ?? 80) >= MIN_WIDTH_FOR_INLINE_STATUS)

  const effectiveMaxLines = () => props.maxLines ?? 20

  // Scrollbox height accounts for the output header + prompt line (3 lines)
  const PROMPT_LINE_HEIGHT = 3
  const outputHeaderHeight = () => isWideLayout() ? OUTPUT_HEADER_HEIGHT_WIDE : OUTPUT_HEADER_HEIGHT_NARROW
  const scrollboxHeight = () => Math.max(3, effectiveMaxLines() - outputHeaderHeight() - PROMPT_LINE_HEIGHT)

  // Check if we're in controller view mode (no step agent, but controller has status)
  const isControllerViewMode = () => !props.currentAgent && props.controllerState?.status != null

  // Check if controller is active (delegated state or controller view mode)
  const isControllerActive = () =>
    (props.currentAgent?.status === "delegated" && props.controllerState != null) || isControllerViewMode()

  // Get the effective status (controller in controller view, step agent otherwise)
  const effectiveStatus = () => isControllerViewMode() ? props.controllerState?.status : props.currentAgent?.status

  // Check if agent is running
  const isRunning = () => effectiveStatus() === "running"

  // Get status color
  const statusColor = () => {
    const status = effectiveStatus()
    if (status === "completed") return themeCtx.theme.success
    if (status === "failed") return themeCtx.theme.error
    return themeCtx.theme.warning
  }

  // Get display name/engine/model/id (controller when delegated or controller view, step agent otherwise)
  const displayName = () => isControllerActive() ? props.controllerState!.name : props.currentAgent?.name
  const displayEngine = () => isControllerActive() ? props.controllerState!.engine : props.currentAgent?.engine
  const displayModel = () => isControllerActive() ? props.controllerState!.model : props.currentAgent?.model
  const displayAgentId = () => isControllerActive() ? props.controllerState!.id : props.currentAgent?.id

  // Derive activity from agent status, with log-based refinement when running
  const derivedActivity = (): ActivityType => {
    const status = effectiveStatus()

    // Error states
    if (status === "failed" || status === "retrying") {
      return "error"
    }

    // Running state - use log-based activity detection for granularity
    if (status === "running") {
      // If we have log-based activity (thinking vs tool), use it
      if (props.currentActivity && props.currentActivity !== "idle") {
        return props.currentActivity
      }
      // Default to thinking when running but no specific activity detected
      return "thinking"
    }

    // All other states (pending, completed, skipped, paused, awaiting, delegated)
    return "idle"
  }

  // Get character face based on agent ID and derived activity
  const currentFace = () => getFace(displayAgentId() ?? "default", derivedActivity())

  // Lock phrase when activity changes (avoid random re-selection on every render)
  const [lockedPhrase, setLockedPhrase] = createSignal("")
  let prevActivity: ActivityType | null = null
  let prevAgentId: string | null = null
  createEffect(() => {
    const activity = derivedActivity()
    const agentId = displayAgentId() ?? "default"
    // Only get new random phrase when activity or agent actually changes
    if (activity !== prevActivity || agentId !== prevAgentId) {
      prevActivity = activity
      prevAgentId = agentId
      setLockedPhrase(getPhrase(agentId, activity))
    }
  })
  const currentPhrase = () => lockedPhrase() || getPhrase(displayAgentId() ?? "default", derivedActivity())

  // Check if we have something to display (agent or controller in controller view)
  const hasDisplayContent = () => props.currentAgent != null || isControllerViewMode()

  // Get connecting message
  const connectingMessage = () => {
    const idx = props.connectingMessageIndex ?? 0
    return CONNECTING_MESSAGES[idx % CONNECTING_MESSAGES.length]
  }

  // Compute prompt line state from unified inputState
  const promptLineState = (): PromptLineState => {
    const status = props.workflowStatus
    const inputState = props.inputState

    // Workflow not running or completed
    if (!status || status === "completed" || status === "stopped" || status === "idle") {
      return { mode: "disabled" }
    }

    // Controller view mode: ignore queue, show simple active/passive state
    if (isControllerViewMode()) {
      if (inputState?.active) {
        return { mode: "active", reason: "paused" }
      }
      return { mode: "passive" }
    }

    // Input state active (unified pause/chained)
    if (inputState?.active) {
      // Has queued prompts = show chained UI
      if (inputState.queuedPrompts && inputState.queuedPrompts.length > 0) {
        const idx = inputState.currentIndex ?? 0
        const nextPrompt = inputState.queuedPrompts[idx]
        // Current step: "Init" for first step, or previous queued prompt name
        const currentRawName = idx === 0 ? "Init" : (inputState.queuedPrompts[idx - 1]?.name ?? "current step")
        // Strip "Step XX - " prefix to avoid redundancy with "Step X/Y:"
        const currentCleanName = currentRawName.replace(/^Step\s+\d+\s*[-:]\s*/i, "")
        const nextRawName = nextPrompt?.name ?? "next step"
        const nextCleanName = nextRawName.replace(/^Step\s+\d+\s*[-:]\s*/i, "")
        return {
          mode: "chained",
          name: currentCleanName,
          nextStepName: nextCleanName,
          description: nextPrompt?.label ?? "",
          index: idx,  // 0-based: Init = step 0
          total: inputState.queuedPrompts.length,  // Only count chained prompts
        }
      }
      // No queue = simple pause/steering
      return { mode: "active", reason: "paused" }
    }

    // Running but not waiting for input
    if (status === "running" || status === "stopping") {
      // Preserve chained step info even when agent is working
      if (inputState?.queuedPrompts && inputState.queuedPrompts.length > 0) {
        const idx = inputState.currentIndex ?? 0
        // Current step: "Init" for first step, or previous queued prompt name
        const rawName = idx === 0 ? "Init" : (inputState.queuedPrompts[idx - 1]?.name ?? "current step")
        // Strip "Step XX - " prefix to avoid redundancy with "Step X/Y:"
        const cleanName = rawName.replace(/^Step\s+\d+\s*[-:]\s*/i, "")
        return {
          mode: "passive",
          chainedStep: {
            name: cleanName,
            index: idx,  // 0-based: Init = step 0
            total: inputState.queuedPrompts.length,  // Only count chained prompts
          },
        }
      }
      return { mode: "passive" }
    }

    return { mode: "disabled" }
  }

  return (
    <box flexDirection="column" flexGrow={1}>
      {/* Header */}
      <Show
        when={hasDisplayContent()}
        fallback={
          <box
            flexDirection="column"
            flexGrow={1}
            height={scrollboxHeight()}
            justifyContent="center"
            alignItems="center"
          >
            <text fg={themeCtx.theme.textMuted}>No agent selected</text>
          </box>
        }
      >
        {/* Side Curve Header - Responsive layout */}
        <box flexDirection="column" paddingLeft={1} height={outputHeaderHeight()} flexShrink={0}>
          <text fg={themeCtx.theme.border}>╭─</text>

          {/* Wide layout: name and status on same line */}
          <Show when={isWideLayout()}>
            <box flexDirection="row" justifyContent="space-between" paddingRight={2}>
              <box flexDirection="row">
                <text fg={themeCtx.theme.border}>│  </text>
                <text fg={themeCtx.theme.text}>{currentFace()}</text>
                <text>  </text>
                <text fg={themeCtx.theme.text} attributes={1}>{displayName()}</text>
              </box>
              <box flexDirection="row" gap={1}>
                <text fg={themeCtx.theme.info}>{displayEngine()}</text>
                <Show when={displayModel()}>
                  <text fg={themeCtx.theme.textMuted}>{displayModel()}</text>
                </Show>
                <text fg={statusColor()}>● {effectiveStatus()}</text>
              </box>
            </box>
          </Show>

          {/* Narrow layout: name on one line, status on next line, telemetry on third */}
          <Show when={!isWideLayout()}>
            <box flexDirection="row">
              <text fg={themeCtx.theme.border}>│  </text>
              <text fg={themeCtx.theme.text}>{currentFace()}</text>
              <text>  </text>
              <text fg={themeCtx.theme.text} attributes={1}>{displayName()}</text>
            </box>
            <box flexDirection="row">
              <text fg={themeCtx.theme.border}>│  </text>
              <text fg={themeCtx.theme.info}>{displayEngine()}</text>
              <Show when={displayModel()}>
                <text fg={themeCtx.theme.textMuted}> {displayModel()}</text>
              </Show>
              <text fg={statusColor()}> ● {effectiveStatus()}</text>
            </box>
          </Show>

          <box flexDirection="row" justifyContent="space-between" paddingRight={2}>
            <box flexDirection="row">
              <text fg={themeCtx.theme.border}>│  </text>
              <Show when={derivedActivity() !== "idle"} fallback={<text fg={themeCtx.theme.textMuted}>↳ {currentPhrase()}</text>}>
                <TypingText text={`↳ ${currentPhrase()}`} speed={30} />
              </Show>
            </box>
            <text fg={themeCtx.theme.textMuted}>
              {(() => {
                const formatTokens = (total: number) => {
                  if (total >= 1000000) return `${(total / 1000000).toFixed(1)}M`
                  if (total >= 1000) return `${(total / 1000).toFixed(1)}K`
                  return `${total}`
                }
                const controllerTotal = (props.controllerState?.telemetry?.tokensIn ?? 0) + (props.controllerState?.telemetry?.tokensOut ?? 0)
                const stepTotal = (props.currentAgent?.telemetry.tokensIn ?? 0) + (props.currentAgent?.telemetry.tokensOut ?? 0)

                // Controller view mode - show only controller
                if (isControllerViewMode()) {
                  return `controller: ${formatTokens(controllerTotal)}`
                }
                // Workflow has controller configured - show both
                if (props.controllerState != null) {
                  return `controller: ${formatTokens(controllerTotal)} │ step: ${formatTokens(stepTotal)}`
                }
                // No controller - show step only
                return `context: ${formatTokens(stepTotal)}`
              })()}
            </text>
          </box>
          <text fg={themeCtx.theme.border}>╰─</text>
        </box>
        {/* Output area */}
        <box paddingLeft={1} paddingRight={1} flexDirection="column" flexGrow={1}>
          <Show when={props.isLoading}>
            <text fg={themeCtx.theme.textMuted}>Waiting for agent output...</text>
          </Show>

          <Show when={props.isConnecting && !props.isLoading}>
            <box flexDirection="row">
              <text fg={themeCtx.theme.text}>● </text>
              <ShimmerText text={connectingMessage()} />
            </box>
          </Show>

          <Show when={props.error}>
            <text fg={themeCtx.theme.error}>Error loading logs: {props.error}</text>
          </Show>

          <Show when={!props.isLoading && !props.isConnecting && !props.error && props.lines.length === 0}>
            <text fg={themeCtx.theme.textMuted}>Waiting for output...</text>
          </Show>

          <Show when={!props.isLoading && !props.isConnecting && !props.error && props.lines.length > 0}>
            {/* Loading earlier lines indicator */}
            <Show when={props.isLoadingEarlier}>
              <text fg={themeCtx.theme.info}>↑ Loading earlier lines...</text>
            </Show>
            {/* Error loading earlier lines */}
            <Show when={props.loadEarlierError}>
              <text fg={themeCtx.theme.error}>↑ Error: {props.loadEarlierError}</text>
            </Show>
            <scrollbox
              ref={(r: ScrollBoxRenderable) => setScrollRef(r)}
              flexGrow={1}
              width="100%"
              stickyScroll={shouldStickyScroll()}
              stickyStart="bottom"
              scrollbarOptions={{
                showArrows: true,
                trackOptions: {
                  foregroundColor: themeCtx.theme.info,
                  backgroundColor: themeCtx.theme.borderSubtle,
                },
              }}
              viewportCulling={true}
              focused={!props.isPromptBoxFocused}
            >
              <For each={groupLinesWithTables(props.lines)}>
                {(group) => (
                  <Show
                    when={group.type === 'table'}
                    fallback={<LogLine line={group.lines[0]} />}
                  >
                    <LogTable lines={group.lines} />
                  </Show>
                )}
              </For>
            </scrollbox>
          </Show>

        </box>

        {/* Always-present prompt line */}
        <PromptLine
          state={promptLineState()}
          isFocused={props.isPromptBoxFocused ?? false}
          onSubmit={props.onPromptSubmit ?? (() => {})}
          onSkip={props.onSkip}
          onFocusExit={props.onPromptBoxFocusExit ?? (() => {})}
          onShowChainConfirm={props.onShowChainConfirm}
        />
      </Show>
    </box>
  )
}
