import chalk from 'chalk'
import * as path from 'path'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { logEvent } from 'src/services/analytics/index.js'
import type {
  CommandResultDisplay,
  LocalJSXCommandContext,
} from '../../commands.js'
import { Select } from '../../components/CustomSelect/index.js'
import { Dialog } from '@anthropic/ink'
import {
  IdeAutoConnectDialog,
  IdeDisableAutoConnectDialog,
  shouldShowAutoConnectDialog,
  shouldShowDisableAutoConnectDialog,
} from '../../components/IdeAutoConnectDialog.js'
import { Box, Text } from '@anthropic/ink'
import { clearServerCache } from '../../services/mcp/client.js'
import type { ScopedMcpServerConfig } from '../../services/mcp/types.js'
import { useAppState, useSetAppState } from '../../state/AppState.js'
import { getCwd } from '../../utils/cwd.js'
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
import {
  type DetectedIDEInfo,
  detectIDEs,
  detectRunningIDEs,
  type IdeType,
  isJetBrainsIde,
  isSupportedJetBrainsTerminal,
  isSupportedTerminal,
  toIDEDisplayName,
} from '../../utils/ide.js'
import { getCurrentWorktreeSession } from '../../utils/worktree.js'

type IDEScreenProps = {
  availableIDEs: DetectedIDEInfo[]
  unavailableIDEs: DetectedIDEInfo[]
  selectedIDE?: DetectedIDEInfo | null
  onClose: () => void
  onSelect: (ide?: DetectedIDEInfo) => void
}

function IDEScreen({
  availableIDEs,
  unavailableIDEs,
  selectedIDE,
  onClose,
  onSelect,
}: IDEScreenProps): React.ReactNode {
  const [selectedValue, setSelectedValue] = useState(
    selectedIDE?.port?.toString() ?? 'None',
  )
  const [showAutoConnectDialog, setShowAutoConnectDialog] = useState(false)
  const [showDisableAutoConnectDialog, setShowDisableAutoConnectDialog] =
    useState(false)

  const handleSelectIDE = useCallback(
    (value: string) => {
      if (value !== 'None' && shouldShowAutoConnectDialog()) {
        setShowAutoConnectDialog(true)
      } else if (value === 'None' && shouldShowDisableAutoConnectDialog()) {
        setShowDisableAutoConnectDialog(true)
      } else {
        onSelect(availableIDEs.find(ide => ide.port === parseInt(value, 10)))
      }
    },
    [availableIDEs, onSelect],
  )

  const ideCounts = availableIDEs.reduce<Record<string, number>>((acc, ide) => {
    acc[ide.name] = (acc[ide.name] || 0) + 1
    return acc
  }, {})

  const options = availableIDEs
    .map(ide => {
      const hasMultipleInstances = (ideCounts[ide.name] || 0) > 1
      const showWorkspace =
        hasMultipleInstances && ide.workspaceFolders.length > 0

      return {
        label: ide.name,
        value: ide.port.toString(),
        description: showWorkspace
          ? formatWorkspaceFolders(ide.workspaceFolders)
          : undefined,
      }
    })
    .concat([{ label: 'None', value: 'None', description: undefined }])

  if (showAutoConnectDialog) {
    return (
      <IdeAutoConnectDialog onComplete={() => handleSelectIDE(selectedValue)} />
    )
  }

  if (showDisableAutoConnectDialog) {
    return (
      <IdeDisableAutoConnectDialog
        onComplete={() => {
          // Always disconnect when user selects "None", regardless of their
          // choice about disabling auto-connect
          onSelect(undefined)
        }}
      />
    )
  }

  return (
    <Dialog
      title="Select IDE"
      subtitle="Connect to an IDE for integrated development features."
      onCancel={onClose}
      color="ide"
    >
      <Box flexDirection="column">
        {availableIDEs.length === 0 && (
          <Text dimColor>
            {isSupportedJetBrainsTerminal()
              ? 'No available IDEs detected. Please install the plugin and restart your IDE:\n' +
                'https://docs.claude.com/s/claude-code-jetbrains'
              : 'No available IDEs detected. Make sure your IDE has the Claude Code extension or plugin installed and is running.'}
          </Text>
        )}

        {availableIDEs.length !== 0 && (
          <Select
            defaultValue={selectedValue}
            defaultFocusValue={selectedValue}
            options={options}
            onChange={value => {
              setSelectedValue(value)
              handleSelectIDE(value)
            }}
          />
        )}
        {availableIDEs.length !== 0 &&
          availableIDEs.some(
            ide => ide.name === 'VS Code' || ide.name === 'Visual Studio Code',
          ) && (
            <Box marginTop={1}>
              <Text color="warning">
                Note: Only one Claude Code instance can be connected to VS Code
                at a time.
              </Text>
            </Box>
          )}
        {availableIDEs.length !== 0 && !isSupportedTerminal() && (
          <Box marginTop={1}>
            <Text dimColor>
              Tip: You can enable auto-connect to IDE in /config or with the
              --ide flag
            </Text>
          </Box>
        )}

        {unavailableIDEs.length > 0 && (
          <Box marginTop={1} flexDirection="column">
            <Text dimColor>
              Found {unavailableIDEs.length} other running IDE(s). However,
              their workspace/project directories do not match the current cwd.
            </Text>
            <Box marginTop={1} flexDirection="column">
              {unavailableIDEs.map((ide, index) => (
                <Box key={index} paddingLeft={3}>
                  <Text dimColor>
                    • {ide.name}: {formatWorkspaceFolders(ide.workspaceFolders)}
                  </Text>
                </Box>
              ))}
            </Box>
          </Box>
        )}
      </Box>
    </Dialog>
  )
}

async function findCurrentIDE(
  availableIDEs: DetectedIDEInfo[],
  dynamicMcpConfig?: Record<string, ScopedMcpServerConfig>,
): Promise<DetectedIDEInfo | null> {
  const currentConfig = dynamicMcpConfig?.ide
  if (
    !currentConfig ||
    (currentConfig.type !== 'sse-ide' && currentConfig.type !== 'ws-ide')
  ) {
    return null
  }
  for (const ide of availableIDEs) {
    if (ide.url === currentConfig.url) {
      return ide
    }
  }
  return null
}

type IDEOpenSelectionProps = {
  availableIDEs: DetectedIDEInfo[]
  onSelectIDE: (ide?: DetectedIDEInfo) => void
  onDone: (
    result?: string,
    options?: { display?: CommandResultDisplay },
  ) => void
}

function IDEOpenSelection({
  availableIDEs,
  onSelectIDE,
  onDone,
}: IDEOpenSelectionProps): React.ReactNode {
  const [selectedValue, setSelectedValue] = useState(
    availableIDEs[0]?.port?.toString() ?? '',
  )

  const handleSelectIDE = useCallback(
    (value: string) => {
      const selectedIDE = availableIDEs.find(
        ide => ide.port === parseInt(value, 10),
      )
      onSelectIDE(selectedIDE)
    },
    [availableIDEs, onSelectIDE],
  )

  const options = availableIDEs.map(ide => ({
    label: ide.name,
    value: ide.port.toString(),
  }))

  function handleCancel(): void {
    onDone('IDE selection cancelled', { display: 'system' })
  }

  return (
    <Dialog
      title="Select an IDE to open the project"
      onCancel={handleCancel}
      color="ide"
    >
      <Select
        defaultValue={selectedValue}
        defaultFocusValue={selectedValue}
        options={options}
        onChange={value => {
          setSelectedValue(value)
          handleSelectIDE(value)
        }}
      />
    </Dialog>
  )
}

function RunningIDESelector({
  runningIDEs,
  onSelectIDE,
  onDone,
}: {
  runningIDEs: IdeType[]
  onSelectIDE: (ide: IdeType) => void
  onDone: (
    result?: string,
    options?: { display?: CommandResultDisplay },
  ) => void
}): React.ReactNode {
  const [selectedValue, setSelectedValue] = useState(runningIDEs[0] ?? '')

  const handleSelectIDE = useCallback(
    (value: string) => {
      onSelectIDE(value as IdeType)
    },
    [onSelectIDE],
  )

  const options = runningIDEs.map(ide => ({
    label: toIDEDisplayName(ide),
    value: ide,
  }))

  function handleCancel(): void {
    onDone('IDE selection cancelled', { display: 'system' })
  }

  return (
    <Dialog
      title="Select IDE to install extension"
      onCancel={handleCancel}
      color="ide"
    >
      <Select
        defaultFocusValue={selectedValue}
        options={options}
        onChange={value => {
          setSelectedValue(value)
          handleSelectIDE(value)
        }}
      />
    </Dialog>
  )
}

function InstallOnMount({
  ide,
  onInstall,
}: {
  ide: IdeType
  onInstall: (ide: IdeType) => void
}): React.ReactNode {
  useEffect(() => {
    onInstall(ide)
  }, [ide, onInstall])
  return null
}

export async function call(
  onDone: (
    result?: string,
    options?: { display?: CommandResultDisplay },
  ) => void,
  context: LocalJSXCommandContext,
  args: string,
): Promise<React.ReactNode | null> {
  logEvent('tengu_ext_ide_command', {})
  const {
    options: { dynamicMcpConfig },
    onChangeDynamicMcpConfig,
  } = context

  // Handle 'open' argument
  if (args?.trim() === 'open') {
    const worktreeSession = getCurrentWorktreeSession()
    const targetPath = worktreeSession ? worktreeSession.worktreePath : getCwd()

    // Detect available IDEs
    const detectedIDEs = await detectIDEs(true)
    const availableIDEs = detectedIDEs.filter(ide => ide.isValid)

    if (availableIDEs.length === 0) {
      onDone('No IDEs with Claude Code extension detected.')
      return null
    }

    // Return IDE selection component
    return (
      <IDEOpenSelection
        availableIDEs={availableIDEs}
        onSelectIDE={async (selectedIDE?: DetectedIDEInfo) => {
          if (!selectedIDE) {
            onDone('No IDE selected.')
            return
          }

          // Try to open the project in the selected IDE
          if (
            selectedIDE.name.toLowerCase().includes('vscode') ||
            selectedIDE.name.toLowerCase().includes('cursor') ||
            selectedIDE.name.toLowerCase().includes('windsurf')
          ) {
            // VS Code-based IDEs
            const { code } = await execFileNoThrow('code', [targetPath])
            if (code === 0) {
              onDone(
                `Opened ${worktreeSession ? 'worktree' : 'project'} in ${chalk.bold(selectedIDE.name)}`,
              )
            } else {
              onDone(
                `Failed to open in ${selectedIDE.name}. Try opening manually: ${targetPath}`,
              )
            }
          } else if (isSupportedJetBrainsTerminal()) {
            // JetBrains IDEs - they usually open via their CLI tools
            onDone(
              `Please open the ${worktreeSession ? 'worktree' : 'project'} manually in ${chalk.bold(selectedIDE.name)}: ${targetPath}`,
            )
          } else {
            onDone(
              `Please open the ${worktreeSession ? 'worktree' : 'project'} manually in ${chalk.bold(selectedIDE.name)}: ${targetPath}`,
            )
          }
        }}
        onDone={() => {
          onDone('Exited without opening IDE', { display: 'system' })
        }}
      />
    )
  }

  const detectedIDEs = await detectIDEs(true)

  // If no IDEs with extensions detected, check for running IDEs and offer to install
  if (
    detectedIDEs.length === 0 &&
    context.onInstallIDEExtension &&
    !isSupportedTerminal()
  ) {
    const runningIDEs = await detectRunningIDEs()

    const onInstall = (ide: IdeType) => {
      if (context.onInstallIDEExtension) {
        context.onInstallIDEExtension(ide)
        // The completion message will be shown after installation
        if (isJetBrainsIde(ide)) {
          onDone(
            `Installed plugin to ${chalk.bold(toIDEDisplayName(ide))}\n` +
              `Please ${chalk.bold('restart your IDE')} completely for it to take effect`,
          )
        } else {
          onDone(`Installed extension to ${chalk.bold(toIDEDisplayName(ide))}`)
        }
      }
    }

    if (runningIDEs.length > 1) {
      // Show selector when multiple IDEs are running
      return (
        <RunningIDESelector
          runningIDEs={runningIDEs}
          onSelectIDE={onInstall}
          onDone={() => {
            onDone('No IDE selected.', { display: 'system' })
          }}
        />
      )
    } else if (runningIDEs.length === 1) {
      return <InstallOnMount ide={runningIDEs[0]!} onInstall={onInstall} />
    }
  }

  const availableIDEs = detectedIDEs.filter(ide => ide.isValid)
  const unavailableIDEs = detectedIDEs.filter(ide => !ide.isValid)

  const currentIDE = await findCurrentIDE(availableIDEs, dynamicMcpConfig)

  return (
    <IDECommandFlow
      availableIDEs={availableIDEs}
      unavailableIDEs={unavailableIDEs}
      currentIDE={currentIDE}
      dynamicMcpConfig={dynamicMcpConfig}
      onChangeDynamicMcpConfig={onChangeDynamicMcpConfig}
      onDone={onDone}
    />
  )
}

// Connection timeout slightly longer than the 30s MCP connection timeout
const IDE_CONNECTION_TIMEOUT_MS = 35000

type IDECommandFlowProps = {
  availableIDEs: DetectedIDEInfo[]
  unavailableIDEs: DetectedIDEInfo[]
  currentIDE: DetectedIDEInfo | null
  dynamicMcpConfig?: Record<string, ScopedMcpServerConfig>
  onChangeDynamicMcpConfig?: (
    config: Record<string, ScopedMcpServerConfig>,
  ) => void
  onDone: (
    result?: string,
    options?: { display?: CommandResultDisplay },
  ) => void
}

function IDECommandFlow({
  availableIDEs,
  unavailableIDEs,
  currentIDE,
  dynamicMcpConfig,
  onChangeDynamicMcpConfig,
  onDone,
}: IDECommandFlowProps): React.ReactNode {
  const [connectingIDE, setConnectingIDE] = useState<DetectedIDEInfo | null>(
    null,
  )
  const ideClient = useAppState(s => s.mcp.clients.find(c => c.name === 'ide'))
  const setAppState = useSetAppState()
  const isFirstCheckRef = useRef(true)

  // Watch for connection result
  useEffect(() => {
    if (!connectingIDE) return
    // Skip the first check — it reflects stale state from before the
    // config change was dispatched
    if (isFirstCheckRef.current) {
      isFirstCheckRef.current = false
      return
    }
    if (!ideClient || ideClient.type === 'pending') return
    if (ideClient.type === 'connected') {
      onDone(`Connected to ${connectingIDE.name}.`)
    } else if (ideClient.type === 'failed') {
      onDone(`Failed to connect to ${connectingIDE.name}.`)
    }
  }, [ideClient, connectingIDE, onDone])

  // Timeout fallback
  useEffect(() => {
    if (!connectingIDE) return
    const timer = setTimeout(
      onDone,
      IDE_CONNECTION_TIMEOUT_MS,
      `Connection to ${connectingIDE.name} timed out.`,
    )
    return () => clearTimeout(timer)
  }, [connectingIDE, onDone])

  const handleSelectIDE = useCallback(
    (selectedIDE?: DetectedIDEInfo) => {
      if (!onChangeDynamicMcpConfig) {
        onDone('Error connecting to IDE.')
        return
      }
      const newConfig = { ...(dynamicMcpConfig || {}) }
      if (currentIDE) {
        delete newConfig.ide
      }
      if (!selectedIDE) {
        // Close the MCP transport and remove the client from state
        if (ideClient && ideClient.type === 'connected' && currentIDE) {
          // Null out onclose to prevent auto-reconnection
          ideClient.client.onclose = () => {}
          void clearServerCache('ide', ideClient.config)
          setAppState(prev => ({
            ...prev,
            mcp: {
              ...prev.mcp,
              clients: prev.mcp.clients.filter(c => c.name !== 'ide'),
              tools: prev.mcp.tools.filter(
                t => !t.name?.startsWith('mcp__ide__'),
              ),
              commands: prev.mcp.commands.filter(
                c => !c.name?.startsWith('mcp__ide__'),
              ),
            },
          }))
        }
        onChangeDynamicMcpConfig(newConfig)
        onDone(
          currentIDE
            ? `Disconnected from ${currentIDE.name}.`
            : 'No IDE selected.',
        )
        return
      }
      const url = selectedIDE.url
      newConfig.ide = {
        type: url.startsWith('ws:') ? 'ws-ide' : 'sse-ide',
        url: url,
        ideName: selectedIDE.name,
        authToken: selectedIDE.authToken,
        ideRunningInWindows: selectedIDE.ideRunningInWindows,
        scope: 'dynamic' as const,
      } as ScopedMcpServerConfig
      isFirstCheckRef.current = true
      setConnectingIDE(selectedIDE)
      onChangeDynamicMcpConfig(newConfig)
    },
    [
      dynamicMcpConfig,
      currentIDE,
      ideClient,
      setAppState,
      onChangeDynamicMcpConfig,
      onDone,
    ],
  )

  if (connectingIDE) {
    return <Text dimColor>Connecting to {connectingIDE.name}…</Text>
  }

  return (
    <IDEScreen
      availableIDEs={availableIDEs}
      unavailableIDEs={unavailableIDEs}
      selectedIDE={currentIDE}
      onClose={() => onDone('IDE selection cancelled', { display: 'system' })}
      onSelect={handleSelectIDE}
    />
  )
}

/**
 * Formats workspace folders for display, stripping cwd and showing tail end of paths
 * @param folders Array of folder paths
 * @param maxLength Maximum total length of the formatted string
 * @returns Formatted string with folder paths
 */
export function formatWorkspaceFolders(
  folders: string[],
  maxLength: number = 100,
): string {
  if (folders.length === 0) return ''

  const cwd = getCwd()

  // Only show first 2 workspaces
  const foldersToShow = folders.slice(0, 2)
  const hasMore = folders.length > 2

  // Account for ", …" if there are more folders
  const ellipsisOverhead = hasMore ? 3 : 0 // ", …"

  // Account for commas and spaces between paths (", " = 2 chars per separator)
  const separatorOverhead = (foldersToShow.length - 1) * 2
  const availableLength = maxLength - separatorOverhead - ellipsisOverhead

  const maxLengthPerPath = Math.floor(availableLength / foldersToShow.length)

  const cwdNFC = cwd.normalize('NFC')
  const formattedFolders = foldersToShow.map(folder => {
    // Strip cwd from the beginning if present
    // Normalize both to NFC for consistent comparison (macOS uses NFD paths)
    const folderNFC = folder.normalize('NFC')
    if (folderNFC.startsWith(cwdNFC + path.sep)) {
      folder = folderNFC.slice(cwdNFC.length + 1)
    }

    if (folder.length <= maxLengthPerPath) {
      return folder
    }
    return '…' + folder.slice(-(maxLengthPerPath - 1))
  })

  let result = formattedFolders.join(', ')
  if (hasMore) {
    result += ', …'
  }

  return result
}
