import { Client } from 'ssh2'
import { connectTarget, loadSshHost, type SshHost } from './connect.js'
import { execCommand } from './metrics/common.js'

/**
 * "Docker over SSH": instead of talking to the Docker Engine TCP API, run the
 * `docker` CLI on a remote SSH host. This reuses the existing SSH transport
 * (jump-host chaining, host-key verification, cert/key/password auth) so no
 * dockerd TCP socket has to be exposed — the mesh + SSH auth are the gate.
 *
 * Container ids/names come from the client and are interpolated into shell
 * commands, so every one is validated against this strict allowlist and passed
 * single-quoted. Anything outside this set is rejected before a command runs.
 */
const CONTAINER_REF_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/

export function isValidContainerRef(ref: string): boolean {
  return CONTAINER_REF_RE.test(ref)
}

/** Single-quote a value for safe use as one shell argument. */
function shQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`
}

export const DOCKER_ACTIONS = ['start', 'stop', 'restart', 'pause', 'unpause'] as const
export type DockerAction = (typeof DOCKER_ACTIONS)[number]

export function isDockerAction(value: string): value is DockerAction {
  return (DOCKER_ACTIONS as readonly string[]).includes(value)
}

export interface SshContainer {
  id: string
  name: string
  image: string
  state: string
  status: string
  ports: string
}

/**
 * Connects to the SSH host for `integrationId`, runs `fn` with a ready ssh2
 * Client, and tears the connection (and any jump host) down afterwards.
 * Mirrors the connect-once pattern used by the metrics route.
 */
export async function withSshClient<T>(
  integrationId: number,
  fn: (client: Client) => Promise<T>,
): Promise<{ ok: true; value: T } | { ok: false; error: string }> {
  const target: SshHost | null = loadSshHost(integrationId)
  if (!target) return { ok: false, error: 'SSH integration not found' }

  const jumpRef: { current: Client | null } = { current: null }
  const client = await new Promise<Client | null>((resolve) => {
    const { jumpConn } = connectTarget(
      target,
      (c) => resolve(c),
      () => {
        jumpConn?.end()
        resolve(null)
      },
    )
    jumpRef.current = jumpConn
  })

  if (!client) return { ok: false, error: 'Failed to connect to host' }

  try {
    const value = await fn(client)
    return { ok: true, value }
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : 'Command failed' }
  } finally {
    client.end()
    jumpRef.current?.end()
  }
}

/**
 * Lists containers via `docker ps`. Uses `--format '{{json .}}'` which emits one
 * JSON object per line (the documented stable CLI format), avoiding fragile
 * column parsing.
 */
export async function listContainers(client: Client): Promise<SshContainer[]> {
  const { stdout, stderr, code } = await execCommand(
    client,
    "docker ps --all --no-trunc --format '{{json .}}'",
  )
  if (code !== 0) {
    throw new Error(stderr.trim() || `docker ps exited with code ${code}`)
  }
  const containers: SshContainer[] = []
  for (const line of stdout.split('\n')) {
    const trimmed = line.trim()
    if (!trimmed) continue
    try {
      const row = JSON.parse(trimmed) as Record<string, string>
      containers.push({
        id: row.ID ?? '',
        name: row.Names ?? '',
        image: row.Image ?? '',
        state: row.State ?? '',
        status: row.Status ?? '',
        ports: row.Ports ?? '',
      })
    } catch {
      // Skip any line that isn't valid JSON rather than failing the whole list.
    }
  }
  return containers
}

export async function containerLogs(client: Client, ref: string, tail: number): Promise<string> {
  if (!isValidContainerRef(ref)) throw new Error('Invalid container reference')
  const safeTail = Number.isFinite(tail) && tail > 0 ? Math.min(Math.floor(tail), 5000) : 200
  const { stdout, stderr, code } = await execCommand(
    client,
    `docker logs --tail ${safeTail} ${shQuote(ref)} 2>&1`,
  )
  if (code !== 0 && !stdout) {
    throw new Error(stderr.trim() || `docker logs exited with code ${code}`)
  }
  // `2>&1` folds stderr into stdout so interleaved container logs are preserved.
  return stdout
}

export async function containerAction(client: Client, ref: string, action: DockerAction): Promise<void> {
  if (!isValidContainerRef(ref)) throw new Error('Invalid container reference')
  const { stderr, code } = await execCommand(client, `docker ${action} ${shQuote(ref)}`)
  if (code !== 0) {
    throw new Error(stderr.trim() || `docker ${action} exited with code ${code}`)
  }
}

export async function removeContainer(client: Client, ref: string, force: boolean): Promise<void> {
  if (!isValidContainerRef(ref)) throw new Error('Invalid container reference')
  const flag = force ? '--force ' : ''
  const { stderr, code } = await execCommand(client, `docker rm ${flag}${shQuote(ref)}`)
  if (code !== 0) {
    throw new Error(stderr.trim() || `docker rm exited with code ${code}`)
  }
}

/** Builds the remote command for an interactive `docker exec` shell (used over a PTY). */
export function buildExecShellCommand(ref: string): string {
  if (!isValidContainerRef(ref)) throw new Error('Invalid container reference')
  // Try bash first, fall back to sh, so it works on minimal images too. The
  // ref is validated + single-quoted; the trailing snippet is a fixed string.
  const quoted = shQuote(ref)
  return `docker exec -it ${quoted} bash 2>/dev/null || docker exec -it ${quoted} sh`
}
