import type { FastifyInstance } from 'fastify'
import { Client, type ClientChannel } from 'ssh2'
import { z } from 'zod'
import { loadSshHost, connectTarget } from '../ssh/connect.js'
import {
  withSshClient,
  listContainers,
  containerLogs,
  containerAction,
  removeContainer,
  buildExecShellCommand,
  isDockerAction,
  isValidContainerRef,
} from '../ssh/docker.js'

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

interface ExecMessage {
  type: 'connect' | 'input' | 'resize' | 'disconnect'
  integrationId?: number
  containerId?: string
  cols?: number
  rows?: number
  data?: string
}

/**
 * "Docker over SSH" REST routes. These target an SSH integration (not the
 * Docker TCP integration) and shell out to the `docker` CLI on the remote host.
 * Mutating actions are admin-only, matching the policy for the TCP Docker routes
 * and the rest of the shared-config surface.
 */
export async function dockerSshRoutes(app: FastifyInstance) {
  app.addHook('onRequest', app.authenticate)

  app.get('/api/docker-ssh/:integrationId/containers', async (req, reply) => {
    const integrationId = Number((req.params as { integrationId: string }).integrationId)
    const result = await withSshClient(integrationId, (client) => listContainers(client))
    if (!result.ok) return reply.code(502).send({ error: result.error })
    return { containers: result.value }
  })

  app.get('/api/docker-ssh/:integrationId/containers/:id/logs', async (req, reply) => {
    const { integrationId, id } = req.params as { integrationId: string; id: string }
    if (!isValidContainerRef(id)) return reply.code(400).send({ error: 'Invalid container reference' })
    const tail = Number((req.query as { tail?: string }).tail ?? '200')
    const result = await withSshClient(Number(integrationId), (client) => containerLogs(client, id, tail))
    if (!result.ok) return reply.code(502).send({ error: result.error })
    return { logs: result.value }
  })

  app.post('/api/docker-ssh/:integrationId/containers/:id/:action', { onRequest: [app.adminOnly] }, async (req, reply) => {
    const { integrationId, id, action } = req.params as { integrationId: string; id: string; action: string }
    if (!isValidContainerRef(id)) return reply.code(400).send({ error: 'Invalid container reference' })
    if (!isDockerAction(action)) return reply.code(400).send({ error: 'Invalid action' })
    const result = await withSshClient(Number(integrationId), (client) => containerAction(client, id, action))
    if (!result.ok) return reply.code(502).send({ error: result.error })
    return { ok: true }
  })

  app.post('/api/docker-ssh/:integrationId/containers/:id/remove', { onRequest: [app.adminOnly] }, async (req, reply) => {
    const { integrationId, id } = req.params as { integrationId: string; id: string }
    if (!isValidContainerRef(id)) return reply.code(400).send({ error: 'Invalid container reference' })
    const parsed = z.object({ force: z.boolean().default(false) }).safeParse(req.body ?? {})
    if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
    const result = await withSshClient(Number(integrationId), (client) => removeContainer(client, id, parsed.data.force))
    if (!result.ok) return reply.code(502).send({ error: result.error })
    return { ok: true }
  })
}

/**
 * Interactive `docker exec` shell over a PTY, wired to a WebSocket. Models the
 * terminal route's plumbing but runs the exec command on the SSH host instead
 * of opening a login shell.
 */
export async function dockerSshExecRoutes(app: FastifyInstance) {
  app.get('/api/docker-ssh/exec', { websocket: true }, (socket, req) => {
    let conn: Client | null = null
    let jumpConn: Client | null = null
    let stream: ClientChannel | null = null

    const cleanup = () => {
      stream?.end()
      conn?.end()
      jumpConn?.end()
      stream = null
      conn = null
      jumpConn = null
    }
    socket.on('close', cleanup)

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

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

        const target = msg.integrationId !== undefined ? loadSshHost(msg.integrationId) : null
        if (!target) {
          sendJson(socket, { type: 'error', message: 'SSH integration not found' })
          return
        }
        if (!msg.containerId || !isValidContainerRef(msg.containerId)) {
          sendJson(socket, { type: 'error', message: 'Invalid container reference' })
          return
        }

        let command: string
        try {
          command = buildExecShellCommand(msg.containerId)
        } catch (err) {
          sendJson(socket, { type: 'error', message: err instanceof Error ? err.message : 'Invalid container reference' })
          return
        }

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

        const startSession = (client: Client) => {
          conn = client
          client.exec(command, { pty: { cols, rows, term: 'xterm-256color' } }, (err, ch) => {
            if (err) {
              sendJson(socket, { type: 'error', message: err.message })
              return
            }
            stream = ch
            sendJson(socket, { type: 'ready' })
            ch.on('data', (chunk: Buffer) => sendJson(socket, { type: 'data', data: chunk.toString('utf8') }))
            ch.stderr.on('data', (chunk: Buffer) => sendJson(socket, { type: 'data', data: chunk.toString('utf8') }))
            ch.on('close', () => {
              sendJson(socket, { type: 'exit' })
              cleanup()
            })
          })
        }

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

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

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

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