/**
 * SSH Client for remote server management
 * Uses the ssh2 library for SSH connections - NOT child_process
 */
import { Client } from 'ssh2'
import {
  SSHConnectionConfig,
  SSHResult,
  SSHStreamCallback,
  SSHExecOptions,
  SSHClientOptions,
  SSHError,
} from './types'

import { SSH_PORT_BEFORE_PROVISION } from './constants'

const DEFAULT_PORT = SSH_PORT_BEFORE_PROVISION
const DEFAULT_TIMEOUT = 30000 // 30 seconds
const DEFAULT_KEEPALIVE_INTERVAL = 10000 // 10 seconds
const DEFAULT_KEEPALIVE_COUNT_MAX = 3

export class SSHClient {
  private client: Client
  private config: SSHConnectionConfig
  private options: SSHClientOptions
  private connected = false

  constructor(config: SSHConnectionConfig, options: SSHClientOptions = {}) {
    this.client = new Client()
    this.config = {
      port: DEFAULT_PORT,
      timeout: DEFAULT_TIMEOUT,
      ...config,
    }
    this.options = {
      keepaliveInterval: DEFAULT_KEEPALIVE_INTERVAL,
      keepaliveCountMax: DEFAULT_KEEPALIVE_COUNT_MAX,
      debug: false,
      ...options,
    }
  }

  /**
   * Connect to the remote SSH server
   */
  async connect(): Promise<void> {
    if (this.connected) {
      return
    }

    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new SSHError('Connection timeout', 'TIMEOUT'))
      }, this.config.timeout)

      this.client.on('ready', () => {
        clearTimeout(timeout)
        this.connected = true
        if (this.options.debug) {
          console.log('[SSH] Connected to', this.config.host)
        }
        resolve()
      })

      this.client.on('error', (err: Error & { code?: string; level?: string }) => {
        clearTimeout(timeout)
        this.connected = false
        reject(new SSHError(err.message, err.code || 'UNKNOWN', err.level))
      })

      this.client.on('close', () => {
        this.connected = false
        if (this.options.debug) {
          console.log('[SSH] Connection closed')
        }
      })

      this.client.connect({
        host: this.config.host,
        port: this.config.port,
        username: this.config.username,
        password: this.config.password,
        privateKey: this.config.privateKey,
        passphrase: this.config.passphrase,
        readyTimeout: this.config.timeout,
        keepaliveInterval: this.options.keepaliveInterval,
        keepaliveCountMax: this.options.keepaliveCountMax,
      })
    })
  }

  /**
   * Disconnect from the remote SSH server
   */
  disconnect(): void {
    if (this.connected) {
      this.client.end()
      this.connected = false
    }
  }

  /**
   * Test if the SSH connection is valid
   */
  async testConnection(): Promise<{ success: boolean; error?: string; latency?: number }> {
    const startTime = Date.now()
    try {
      await this.connect()
      const result = await this.runCommand('echo "connection_test"')
      const latency = Date.now() - startTime

      if (result.success && result.stdout.includes('connection_test')) {
        return { success: true, latency }
      }
      return { success: false, error: 'Echo test failed' }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      }
    } finally {
      this.disconnect()
    }
  }

  /**
   * Execute a command on the remote server via SSH
   * Note: This uses ssh2's Client.exec(), NOT child_process
   */
  async runCommand(command: string, options: SSHExecOptions = {}): Promise<SSHResult> {
    if (!this.connected) {
      await this.connect()
    }

    const startTime = Date.now()
    const timeout = options.timeout || DEFAULT_TIMEOUT

    return new Promise((resolve, reject) => {
      let stdout = ''
      let stderr = ''

      const timeoutHandle = setTimeout(() => {
        reject(new SSHError('Command execution timeout', 'EXEC_TIMEOUT'))
      }, timeout)

      // Build command with optional working directory
      const fullCommand = options.cwd ? `cd ${options.cwd} && ${command}` : command

      // ssh2 Client.exec() - executes command on REMOTE server over SSH
      const execOptions = options.env ? { env: options.env as NodeJS.ProcessEnv } : {}
      this.client.exec(fullCommand, execOptions, (err, stream) => {
        if (err) {
          clearTimeout(timeoutHandle)
          reject(new SSHError(err.message, 'EXEC_ERROR'))
          return
        }

        stream.on('close', (code: number) => {
          clearTimeout(timeoutHandle)
          resolve({
            success: code === 0,
            exitCode: code,
            stdout: stdout.trim(),
            stderr: stderr.trim(),
            duration: Date.now() - startTime,
          })
        })

        stream.on('data', (data: Buffer) => {
          stdout += data.toString()
        })

        stream.stderr.on('data', (data: Buffer) => {
          stderr += data.toString()
        })
      })
    })
  }

  /**
   * Execute a command on the remote server with streaming output
   * Note: This uses ssh2's Client.exec(), NOT child_process
   */
  async streamCommand(
    command: string,
    callback: SSHStreamCallback,
    options: SSHExecOptions = {}
  ): Promise<SSHResult> {
    if (!this.connected) {
      await this.connect()
    }

    const startTime = Date.now()
    const timeout = options.timeout || DEFAULT_TIMEOUT

    return new Promise((resolve, reject) => {
      let stdout = ''
      let stderr = ''

      const timeoutHandle = setTimeout(() => {
        callback({
          type: 'error',
          error: 'Command execution timeout',
          timestamp: new Date(),
        })
        reject(new SSHError('Command execution timeout', 'EXEC_TIMEOUT'))
      }, timeout)

      const fullCommand = options.cwd ? `cd ${options.cwd} && ${command}` : command

      // ssh2 Client.exec() - executes command on REMOTE server over SSH
      const execOptions = options.env ? { env: options.env as NodeJS.ProcessEnv } : {}
      this.client.exec(fullCommand, execOptions, (err, stream) => {
        if (err) {
          clearTimeout(timeoutHandle)
          callback({
            type: 'error',
            error: err.message,
            timestamp: new Date(),
          })
          reject(new SSHError(err.message, 'EXEC_ERROR'))
          return
        }

        stream.on('close', (code: number) => {
          clearTimeout(timeoutHandle)
          callback({
            type: 'exit',
            exitCode: code,
            timestamp: new Date(),
          })
          resolve({
            success: code === 0,
            exitCode: code,
            stdout: stdout.trim(),
            stderr: stderr.trim(),
            duration: Date.now() - startTime,
          })
        })

        stream.on('data', (data: Buffer) => {
          const text = data.toString()
          stdout += text
          callback({
            type: 'stdout',
            data: text,
            timestamp: new Date(),
          })
        })

        stream.stderr.on('data', (data: Buffer) => {
          const text = data.toString()
          stderr += text
          callback({
            type: 'stderr',
            data: text,
            timestamp: new Date(),
          })
        })
      })
    })
  }

  /**
   * Upload a file to the remote server via SFTP
   */
  async uploadFile(localPath: string, remotePath: string): Promise<void> {
    if (!this.connected) {
      await this.connect()
    }

    return new Promise((resolve, reject) => {
      this.client.sftp((err, sftp) => {
        if (err) {
          reject(new SSHError(err.message, 'SFTP_ERROR'))
          return
        }

        sftp.fastPut(localPath, remotePath, (err) => {
          if (err) {
            reject(new SSHError(err.message, 'UPLOAD_ERROR'))
            return
          }
          resolve()
        })
      })
    })
  }

  /**
   * Upload content directly as a file to the remote server
   */
  async uploadContent(content: string, remotePath: string): Promise<void> {
    if (!this.connected) {
      await this.connect()
    }

    return new Promise((resolve, reject) => {
      this.client.sftp((err, sftp) => {
        if (err) {
          reject(new SSHError(err.message, 'SFTP_ERROR'))
          return
        }

        const writeStream = sftp.createWriteStream(remotePath)

        writeStream.on('error', (err: Error) => {
          reject(new SSHError(err.message, 'WRITE_ERROR'))
        })

        writeStream.on('close', () => {
          resolve()
        })

        writeStream.end(content)
      })
    })
  }

  /**
   * Download a file from the remote server
   */
  async downloadFile(remotePath: string, localPath: string): Promise<void> {
    if (!this.connected) {
      await this.connect()
    }

    return new Promise((resolve, reject) => {
      this.client.sftp((err, sftp) => {
        if (err) {
          reject(new SSHError(err.message, 'SFTP_ERROR'))
          return
        }

        sftp.fastGet(remotePath, localPath, (err) => {
          if (err) {
            reject(new SSHError(err.message, 'DOWNLOAD_ERROR'))
            return
          }
          resolve()
        })
      })
    })
  }

  /**
   * Read a file from the remote server
   */
  async readFile(remotePath: string): Promise<string> {
    if (!this.connected) {
      await this.connect()
    }

    return new Promise((resolve, reject) => {
      this.client.sftp((err, sftp) => {
        if (err) {
          reject(new SSHError(err.message, 'SFTP_ERROR'))
          return
        }

        let content = ''
        const readStream = sftp.createReadStream(remotePath)

        readStream.on('data', (chunk: Buffer) => {
          content += chunk.toString()
        })

        readStream.on('error', (err: Error) => {
          reject(new SSHError(err.message, 'READ_ERROR'))
        })

        readStream.on('close', () => {
          resolve(content)
        })
      })
    })
  }

  /**
   * Check if the client is currently connected
   */
  isConnected(): boolean {
    return this.connected
  }
}

/**
 * Convenience function to test SSH connection without creating a client instance
 */
export async function testSSHConnection(
  config: SSHConnectionConfig
): Promise<{ success: boolean; error?: string; latency?: number }> {
  const client = new SSHClient(config)
  return client.testConnection()
}

/**
 * Convenience function to run a single command on a remote server
 */
export async function runSSHCommand(
  config: SSHConnectionConfig,
  command: string,
  options: SSHExecOptions = {}
): Promise<SSHResult> {
  const client = new SSHClient(config)
  try {
    await client.connect()
    return await client.runCommand(command, options)
  } finally {
    client.disconnect()
  }
}
