import type { FastifyInstance } from 'fastify'
import { Client, type ClientChannel } from 'ssh2'
import { spawn as spawnPty, type IPty } from 'node-pty'
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { loadSshHost, connectTarget, type SshHost } from '../ssh/connect.js'

interface ClientMessage {
  type: 'connect' | 'input' | 'resize' | 'disconnect' | 'list_tmux'
  integrationId?: number
  cols?: number
  rows?: number
  data?: string
  tmuxSession?: string
}

const TMUX_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/

const SESSION_LOG_DIR = process.env.ARCHNEST_SESSION_LOG_DIR ?? './data/session-logs'

function send(socket: { send: (data: string) => void }, payload: Record<string, unknown>) {
  socket.send(JSON.stringify(payload))
}

/** OPKSSH / certificate auth has no support in the ssh2 library, so this path shells out to the
 * system `ssh` binary (which natively understands OpenSSH certificates via CertificateFile) under
 * a real pty. Jump-host chaining is not supported on this path. */
function connectWithCertificate(
  target: SshHost,
  cols: number,
  rows: number,
  onReady: (pty: IPty, keyDir: string) => void,
  onError: (message: string) => void,
) {
  const keyDir = mkdtempSync(join(tmpdir(), 'archnest-ssh-'))
  const keyFile = join(keyDir, 'id_key')
  const certFile = join(keyDir, 'id_key-cert.pub')
  try {
    writeFileSync(keyFile, target.secrets.privateKey ?? '', { mode: 0o600 })
    writeFileSync(certFile, target.secrets.certificate ?? '', { mode: 0o600 })
  } catch (err) {
    rmSync(keyDir, { recursive: true, force: true })
    onError(err instanceof Error ? err.message : 'Failed to write certificate files')
    return
  }

  const args = [
    '-tt',
    '-p', String(Number(target.config.port) || 22),
    '-i', keyFile,
    '-o', `CertificateFile=${certFile}`,
    '-o', 'StrictHostKeyChecking=accept-new',
    '-o', `UserKnownHostsFile=${join(keyDir, 'known_hosts')}`,
    `${target.config.username}@${target.config.host}`,
  ]

  let pty: IPty
  try {
    pty = spawnPty('ssh', args, { name: 'xterm-256color', cols, rows })
  } catch (err) {
    rmSync(keyDir, { recursive: true, force: true })
    onError(err instanceof Error ? `Failed to spawn ssh: ${err.message}` : 'Failed to spawn ssh')
    return
  }
  onReady(pty, keyDir)
}

function sessionLogPath(integrationId: number) {
  mkdirSync(SESSION_LOG_DIR, { recursive: true })
  const stamp = new Date().toISOString().replace(/[:.]/g, '-')
  return join(SESSION_LOG_DIR, `${integrationId}_${stamp}.log`)
}

export async function terminalRoutes(app: FastifyInstance) {
  app.get('/api/terminal', { websocket: true }, (socket, req) => {
    let conn: Client | null = null
    let jumpConn: Client | null = null
    let stream: ClientChannel | null = null
    let pty: IPty | null = null
    let ptyKeyDir: string | null = null
    let logPath: string | null = null

    const logData = (data: string) => {
      if (logPath) appendFileSync(logPath, data)
    }

    const cleanup = () => {
      stream?.end()
      conn?.end()
      jumpConn?.end()
      pty?.kill()
      if (ptyKeyDir) rmSync(ptyKeyDir, { recursive: true, force: true })
      stream = null
      conn = null
      jumpConn = null
      pty = null
      ptyKeyDir = null
      logPath = null
    }

    socket.on('close', cleanup)

    socket.on('message', async (raw: Buffer) => {
      let msg: ClientMessage
      try {
        msg = JSON.parse(raw.toString())
      } catch {
        send(socket, { type: 'error', message: 'Invalid JSON' })
        return
      }

      if (msg.type === 'connect' || msg.type === 'list_tmux') {
        const query = req.query as { token?: string }
        try {
          await app.jwt.verify(query.token ?? '')
        } catch {
          send(socket, { type: 'error', message: 'Unauthorized' })
          socket.close()
          return
        }

        const target = msg.integrationId !== undefined ? loadSshHost(msg.integrationId) : null
        if (!target) {
          send(socket, { type: 'error', message: 'SSH integration not found' })
          return
        }

        if (msg.type === 'list_tmux') {
          const { conn: ephemeralConn, jumpConn: ephemeralJump } = connectTarget(
            target,
            (client) => {
              client.exec("command -v tmux >/dev/null && tmux list-sessions -F '#S' 2>/dev/null", (err, ch) => {
                if (err) {
                  send(socket, { type: 'tmux_sessions', sessions: [] })
                  client.end()
                  ephemeralJump?.end()
                  return
                }
                let out = ''
                ch.on('data', (chunk: Buffer) => (out += chunk.toString('utf8')))
                ch.on('close', () => {
                  const sessions = out.split('\n').map((s) => s.trim()).filter(Boolean)
                  send(socket, { type: 'tmux_sessions', sessions })
                  client.end()
                  ephemeralJump?.end()
                })
              })
            },
            (message) => send(socket, { type: 'tmux_sessions', sessions: [], error: message }),
          )
          void ephemeralConn
          return
        }

        const cols = msg.cols ?? 80
        const rows = msg.rows ?? 24

        if (target.secrets.certificate) {
          connectWithCertificate(
            target,
            cols,
            rows,
            (p, keyDir) => {
              pty = p
              ptyKeyDir = keyDir
              if (target.config.sessionLogging === 'true') logPath = sessionLogPath(target.id)
              send(socket, { type: 'connected' })
              p.onData((data) => {
                logData(data)
                send(socket, { type: 'data', data })
              })
              p.onExit(() => {
                send(socket, { type: 'closed' })
                cleanup()
              })
            },
            (message) => send(socket, { type: 'error', message }),
          )
          return
        }

        const startSession = (client: Client) => {
          conn = client
          const tmuxSession = msg.tmuxSession && TMUX_NAME_RE.test(msg.tmuxSession) ? msg.tmuxSession : null
          const onChannel = (err: Error | undefined, ch: ClientChannel) => {
            if (err) {
              send(socket, { type: 'error', message: err.message })
              return
            }
            stream = ch
            if (target.config.sessionLogging === 'true') logPath = sessionLogPath(target.id)
            send(socket, { type: 'connected' })
            ch.on('data', (chunk: Buffer) => {
              const text = chunk.toString('utf8')
              logData(text)
              send(socket, { type: 'data', data: text })
            })
            ch.stderr.on('data', (chunk: Buffer) => send(socket, { type: 'data', data: chunk.toString('utf8') }))
            ch.on('close', () => {
              send(socket, { type: 'closed' })
              cleanup()
            })
          }
          if (tmuxSession) {
            client.exec(`tmux attach -t ${tmuxSession} || tmux new-session -s ${tmuxSession}`, {
              pty: { cols, rows, term: 'xterm-256color' },
            }, onChannel)
          } else {
            client.shell({ cols, rows, term: 'xterm-256color' }, onChannel)
          }
        }

        const result = connectTarget(target, startSession, (message) => send(socket, { type: 'error', message }))
        conn = result.conn
        jumpConn = result.jumpConn
        return
      }

      if (msg.type === 'input') {
        if (pty) pty.write(msg.data ?? '')
        else stream?.write(msg.data ?? '')
        return
      }

      if (msg.type === 'resize') {
        if (pty) pty.resize(msg.cols ?? 80, msg.rows ?? 24)
        else stream?.setWindow(msg.rows ?? 24, msg.cols ?? 80, 0, 0)
        return
      }

      if (msg.type === 'disconnect') {
        cleanup()
      }
    })
  })
}
