import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import type { McpStdioConfig } from './client'
import type { CallToolResultPayload, McpTool } from './types'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { McpClient } from './client'

/**
 * MCP Stdio Client
 *
 * Responsibilities:
 * - Manage stdio subprocess lifecycle for a single MCP server
 * - Communicate with MCP server via stdio
 * - Implement McpClient interface for ToolRegistry
 * - Handle process crashes and auto-restart with exponential backoff
 */
export class McpStdioClient extends McpClient {
  private client?: Client
  private transport?: StdioClientTransport
  private cachedTools: McpTool[] = []
  private _connected = false
  private lastError?: string
  private isDisposed = false

  // Restart strategy
  private retryCount = 0
  private readonly MAX_RETRY_DELAY_MS = 10000

  // Process health
  private lastUsedAt: number = Date.now()
  private createdAt: number = Date.now()
  private processPid: number | null = null
  private restartHistory: number[] = []

  constructor(public readonly config: McpStdioConfig) {
    super()
    if (this._enableLogging) {
      console.warn(`🔌 McpStdioClient created for: ${config.name}`)
    }
  }

  get connected(): boolean {
    return this._connected && !!this.client && !!this.transport
  }

  async initialize(): Promise<void> {
    if (this._connected) {
      return
    }

    if (this.isDisposed) {
      throw new Error(
        `Client "${this.config.name}" has been disposed and cannot be reinitialized`,
      )
    }

    try {
      await this.startProcess()
      this._connected = true
      this.retryCount = 0 // Reset retry count
      this.lastError = undefined
      if (this._enableLogging) {
        console.warn(
          `✅ McpStdioClient initialized: ${this.config.name} (${this.cachedTools.length} tools)`,
        )
      }
    }
    catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      this.lastError = msg
      if (this._enableLogging) {
        console.error(
          `Failed to initialize stdio MCP client "${this.config.name}":`,
          msg,
        )
      }
      throw error
    }
  }

  /**
   * Start subprocess and establish connection
   */
  private async startProcess(): Promise<void> {
    if (this._enableLogging) {
      console.warn(
        `🚀 Starting stdio MCP process: ${this.config.command} ${this.config.args.join(' ')}`,
      )
    }

    // Create stdio transport (it manages the process itself)
    this.transport = new StdioClientTransport({
      command: this.config.command,
      args: this.config.args,
      env: this.config.env,
      cwd: this.config.cwd,
      stderr: 'pipe', // Capture stderr for debugging
    })

    // Listen to stderr output
    const stderrStream = this.transport.stderr
    if (stderrStream) {
      stderrStream.on('data', (data: Buffer) => {
        const message = data.toString().trim()
        if (message && this._enableLogging) {
          console.error(`[${this.config.name} stderr]:`, message)
        }
      })
    }

    // Create MCP client
    this.client = new Client(
      {
        name: `stdio-mcp-client-${this.config.name}`,
        version: '1.0.0',
      },
      {
        capabilities: {
          roots: {},
          sampling: {},
        },
      },
    )

    // Connect client (this calls transport.start())
    await this.client.connect(this.transport)

    // Get process PID
    this.processPid = this.transport.pid

    if (this._enableLogging) {
      console.warn(
        `🔗 Connected to stdio MCP server: ${this.config.name} (PID: ${this.processPid})`,
      )
    }

    // Preload tools list
    await this.refreshToolCache()

    // Setup process monitoring
    this.setupProcessMonitoring()
  }

  /**
   * Setup process monitoring and auto-restart
   */
  private setupProcessMonitoring(): void {
    if (!this.transport) {
      return
    }

    // Listen to transport close
    this.transport.onclose = () => {
      if (this.isDisposed) {
        if (this._enableLogging) {
          console.warn(
            `📴 Stdio MCP process "${this.config.name}" closed as expected (disposed)`,
          )
        }
        return
      }

      if (this._enableLogging) {
        console.warn(
          `⚠️ Stdio MCP process "${this.config.name}" closed unexpectedly`,
        )
      }

      this._connected = false
      this.client = undefined
      this.transport = undefined
      this.processPid = null

      // Attempt restart if auto-restart is enabled
      if (this.config.autoRestart !== false) {
        this.attemptRestart()
      }
    }

    // Listen to transport errors
    this.transport.onerror = (error) => {
      if (this._enableLogging) {
        console.error(
          `❌ Stdio MCP transport "${this.config.name}" error:`,
          error,
        )
      }
      this.lastError = String(error)
    }
  }

  /**
   * Attempt to restart process (with exponential backoff)
   */
  private async attemptRestart(): Promise<void> {
    if (this.isDisposed) {
      return
    }

    const now = Date.now()
    const windowMs = this.config.restartWindowMs ?? 60_000
    const maxRestarts = this.config.maxRestarts ?? 5

    // Clean up old restart history
    this.restartHistory = this.restartHistory.filter(
      ts => now - ts < windowMs,
    )

    if (this.restartHistory.length >= maxRestarts) {
      this.lastError = `MCP process restart limit exceeded (${maxRestarts} in ${windowMs}ms)`
      if (this._enableLogging) {
        console.error(
          `❌ Max restart limit (${maxRestarts}) reached for "${this.config.name}". Giving up.`,
        )
      }
      return
    }

    this.restartHistory.push(now)
    this.retryCount++

    // Calculate backoff delay (exponential backoff)
    const defaultRestartDelay = 1000
    const delay = Math.min(
      (this.config.restartDelayMs ?? defaultRestartDelay)
      * 2 ** (this.retryCount - 1),
      this.MAX_RETRY_DELAY_MS,
    )

    if (this._enableLogging) {
      console.warn(
        `🔄 Attempting to restart "${this.config.name}" (attempt ${this.retryCount}/${maxRestarts}) after ${delay}ms...`,
      )
    }

    await new Promise(resolve => setTimeout(resolve, delay))

    try {
      await this.startProcess()
      if (this._enableLogging) {
        console.warn(
          `✅ Successfully restarted "${this.config.name}" after ${this.retryCount} attempts`,
        )
      }
      this.retryCount = 0 // Reset retry count
    }
    catch (error) {
      if (this._enableLogging) {
        console.error(
          `Failed to restart "${this.config.name}" (attempt ${this.retryCount}):`,
          error instanceof Error ? error.message : error,
        )
      }
      // Recursively retry
      await this.attemptRestart()
    }
  }

  /**
   * Refresh tool cache
   */
  private async refreshToolCache(): Promise<void> {
    if (!this.client) {
      throw new Error('Client not initialized. Call initialize() first.')
    }

    try {
      const response = await this.client.listTools()
      const tools = response.tools || []

      this.cachedTools = tools.map(tool => ({
        name: tool.name,
        title: tool.title,
        description: tool.description,
        inputSchema: tool.inputSchema as any,
      }))

      if (this._enableLogging) {
        console.warn(
          `🔄 Refreshed tool cache for ${this.config.name}: ${this.cachedTools.length} tools`,
        )
      }
    }
    catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      if (this._enableLogging) {
        console.error(
          `Failed to refresh tool cache for "${this.config.name}":`,
          msg,
        )
      }
      throw error
    }
  }

  async listTools(): Promise<McpTool[]> {
    if (!this._connected) {
      if (this._enableLogging) {
        console.warn(
          `McpStdioClient "${this.config.name}" not initialized, returning cached tools`,
        )
      }
      return this.cachedTools
    }

    try {
      await this.refreshToolCache()
    }
    catch (error) {
      if (this._enableLogging) {
        console.warn(`Failed to refresh tools, returning cached: ${error}`)
      }
    }

    return this.cachedTools
  }

  async callTool(
    name: string,
    args: Record<string, unknown>,
  ): Promise<CallToolResultPayload> {
    if (!this.client) {
      throw new Error(
        `Stdio MCP client for "${this.config.name}" not initialized. Call initialize() first.`,
      )
    }

    if (!this._connected) {
      throw new Error(
        `Stdio MCP client "${this.config.name}" is not ready. Process may be restarting.`,
      )
    }

    if (this._enableLogging) {
      console.warn(`🔧 Invoking stdio MCP tool: ${this.config.name}/${name}`, {
        args,
      })
    }

    // Update last used time
    this.lastUsedAt = Date.now()

    try {
      const result = await this.client.callTool({
        name,
        arguments: args,
      })

      return result as CallToolResult
    }
    catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      this.lastError = msg
      if (this._enableLogging) {
        console.error(
          `Failed to invoke stdio MCP tool "${this.config.name}/${name}": ${msg}`,
        )
      }
      throw error
    }
  }

  async disconnect(): Promise<void> {
    if (this.isDisposed) {
      return
    }

    this.isDisposed = true
    if (this._enableLogging) {
      console.warn(`🔌 Disposing stdio MCP client: ${this.config.name}`)
    }

    // Close client
    if (this.client) {
      try {
        await this.client.close()
        if (this._enableLogging) {
          console.warn(`🔌 Closed stdio MCP client: ${this.config.name}`)
        }
      }
      catch (error) {
        if (this._enableLogging) {
          console.error(
            `Error closing stdio MCP client "${this.config.name}":`,
            error instanceof Error ? error.message : error,
          )
        }
      }
      this.client = undefined
    }

    // Close transport (this also terminates subprocess)
    if (this.transport) {
      try {
        await this.transport.close()
        if (this._enableLogging) {
          console.warn(
            `🔌 Closed stdio MCP transport and process: ${this.config.name}`,
          )
        }
      }
      catch (error) {
        if (this._enableLogging) {
          console.error(
            `Error closing transport for "${this.config.name}":`,
            error instanceof Error ? error.message : error,
          )
        }
      }
      this.transport = undefined
    }

    this.processPid = null
    this._connected = false
    this.cachedTools = []
  }

  getStatus() {
    return {
      connected: this._connected,
      toolCount: this.cachedTools.length,
      error: this.lastError,
    }
  }

  /**
   * Get process health info
   */
  getHealthInfo(): {
    initialized: boolean
    pid?: number | null
    uptime: number
    idleTime: number
    retryCount: number
  } {
    return {
      initialized: this._connected,
      pid: this.processPid,
      uptime: Date.now() - this.createdAt,
      idleTime: Date.now() - this.lastUsedAt,
      retryCount: this.retryCount,
    }
  }
}
