#!/usr/bin/env bun

/**
 * Docker Sandbox Creator for AI Coding Agents
 *
 * Creates and manages Docker containers with Claude Code, Codex, and Gemini CLI installed.
 * Supports Docker-in-Docker for containerized development workflows.
 */

import {parseArgs} from "util"
import {existsSync, readdirSync} from "fs"
import {join} from "path"
import {$} from "bun"
import {createHash} from "crypto"

// ============================================================================
// LOGGING SYSTEM
// ============================================================================

type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SUCCESS" | "PHASE" | "STEP"

interface LogEntry {
  timestamp: string
  level: LogLevel
  phase?: string
  step?: number
  totalSteps?: number
  message: string
  details?: Record<string, unknown>
  durationMs?: number
}

class Logger {
  private startTime: number
  private currentPhase: string = "";
  private stepCount: number = 0;
  private totalSteps: number = 0;
  private entries: LogEntry[] = [];

  constructor() {
    this.startTime = Date.now()
  }

  private getTimestamp(): string {
    return new Date().toISOString()
  }

  private formatLog(entry: LogEntry): string {
    const icons: Record<LogLevel, string> = {
      DEBUG: "🔍",
      INFO: "ℹ️ ",
      WARN: "⚠️ ",
      ERROR: "❌",
      SUCCESS: "✅",
      PHASE: "📌",
      STEP: "  →",
    }

    const levelColors: Record<LogLevel, string> = {
      DEBUG: "\x1b[90m",
      INFO: "\x1b[36m",
      WARN: "\x1b[33m",
      ERROR: "\x1b[31m",
      SUCCESS: "\x1b[32m",
      PHASE: "\x1b[35m",
      STEP: "\x1b[37m",
    }

    const reset = "\x1b[0m"
    const dim = "\x1b[2m"
    const icon = icons[entry.level]
    const color = levelColors[entry.level]

    let line = `${dim}[${entry.timestamp.split("T")[1].slice(0, 8)}]${reset} `

    if (entry.level === "PHASE") {
      line += `\n${color}════════════════════════════════════════════════════════════${reset}\n`
      line += `${icon} ${color}PHASE: ${entry.message}${reset}\n`
      line += `${color}════════════════════════════════════════════════════════════${reset}`
    } else if (entry.level === "STEP") {
      const progress = entry.totalSteps ? ` [${entry.step}/${entry.totalSteps}]` : ""
      line += `${icon} ${entry.message}${progress}`
    } else {
      line += `${icon} ${color}${entry.message}${reset}`
    }

    if (entry.durationMs !== undefined) {
      line += ` ${dim}(${entry.durationMs}ms)${reset}`
    }

    if (entry.details && Object.keys(entry.details).length > 0) {
      const detailStr = Object.entries(entry.details)
        .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
        .join(" ")
      line += `\n     ${dim}${detailStr}${reset}`
    }

    return line
  }

  private log(level: LogLevel, message: string, details?: Record<string, unknown>): void {
    const entry: LogEntry = {
      timestamp: this.getTimestamp(),
      level,
      phase: this.currentPhase || undefined,
      step: this.stepCount || undefined,
      totalSteps: this.totalSteps || undefined,
      message,
      details,
    }
    this.entries.push(entry)
    console.log(this.formatLog(entry))
  }

  phase(name: string, totalSteps?: number): void {
    this.currentPhase = name
    this.stepCount = 0
    this.totalSteps = totalSteps || 0
    this.log("PHASE", name)
  }

  step(message: string, details?: Record<string, unknown>): void {
    this.stepCount++
    const entry: LogEntry = {
      timestamp: this.getTimestamp(),
      level: "STEP",
      phase: this.currentPhase,
      step: this.stepCount,
      totalSteps: this.totalSteps,
      message,
      details,
    }
    this.entries.push(entry)
    console.log(this.formatLog(entry))
  }

  debug(message: string, details?: Record<string, unknown>): void {
    if (Bun.env.DEBUG) {
      this.log("DEBUG", message, details)
    }
  }

  info(message: string, details?: Record<string, unknown>): void {
    this.log("INFO", message, details)
  }

  warn(message: string, details?: Record<string, unknown>): void {
    this.log("WARN", message, details)
  }

  error(message: string, details?: Record<string, unknown>): void {
    this.log("ERROR", message, details)
  }

  success(message: string, details?: Record<string, unknown>): void {
    this.log("SUCCESS", message, details)
  }

  timedOperation<T>(operation: string, fn: () => Promise<T>): Promise<T>
  timedOperation<T>(operation: string, fn: () => T): T
  timedOperation<T>(operation: string, fn: () => T | Promise<T>): T | Promise<T> {
    const start = Date.now()
    const result = fn()

    if (result instanceof Promise) {
      return result.then((value) => {
        const duration = Date.now() - start
        this.debug(`${operation} completed`, {durationMs: duration})
        return value
      })
    }

    const duration = Date.now() - start
    this.debug(`${operation} completed`, {durationMs: duration})
    return result
  }

  summary(): void {
    const totalTime = Date.now() - this.startTime
    const errors = this.entries.filter(e => e.level === "ERROR").length
    const warnings = this.entries.filter(e => e.level === "WARN").length
    const phases = this.entries.filter(e => e.level === "PHASE").length

    console.log("\n" + "─".repeat(60))
    console.log("📊 EXECUTION SUMMARY")
    console.log("─".repeat(60))
    console.log(`   Total time:    ${(totalTime / 1000).toFixed(2)}s`)
    console.log(`   Phases:        ${phases}`)
    console.log(`   Warnings:      ${warnings}`)
    console.log(`   Errors:        ${errors}`)
    console.log("─".repeat(60) + "\n")
  }

  getEntries(): LogEntry[] {
    return [...this.entries]
  }

  toJSON(): string {
    return JSON.stringify({
      summary: {
        totalTimeMs: Date.now() - this.startTime,
        totalEntries: this.entries.length,
        errors: this.entries.filter(e => e.level === "ERROR").length,
        warnings: this.entries.filter(e => e.level === "WARN").length,
      },
      entries: this.entries,
    }, null, 2)
  }
}

const log = new Logger()

const HELP_TEXT = `
Docker Sandbox Creator for AI Coding Agents

USAGE:
  bun docker-sandbox.ts [OPTIONS] <WORKING_DIRECTORY>

ARGUMENTS:
  <WORKING_DIRECTORY>    Path to the directory to mount in the container

OPTIONS:
  --dind, -d            Enable Docker-in-Docker support
  --help, -h            Show this help message
  --name, -n <name>     Custom container name (optional)
  --stop                Stop the container instead of starting it
  --remove              Remove the container
  --teardown            Complete teardown: remove container AND image
  --rebuild             Force rebuild the image (ignores cache)
  --logs                Show YOLO session logs (from /workspace/.yolo-logs/)
  --shell               Start an interactive shell in the container
  --claude              Run Claude Code with --dangerously-skip-permissions (sandboxed)
  --no-network          Disable network access (full isolation)
  --ports, -p <ports>   Expose ports (e.g., "3000,5173,8080")
  --prompt <prompt>     Pass a prompt directly to Claude (requires --claude)
  --memory <limit>      Container memory limit (e.g., "2g", "4g"). Default: 4g
  --model <model>       Claude model: "sonnet" (default), "haiku" (lighter), "opus"

PARALLEL EXECUTION OPTIONS:
  --background, -b      Run Claude in background (non-blocking, returns immediately)
  --instance-id <id>    Unique instance ID for parallel containers (auto-generated if not provided)
  --auto-port           Automatically find and allocate free ports (starting from 3000)
  --status              Show status of all running YOLO instances

DESCRIPTION:
  This script creates a Docker container with:
  - Ubuntu base image with Node.js 24, TypeScript, Git, and Bun
  - Claude Code, OpenAI Codex, and Gemini CLI pre-installed
  - Custom dotfiles from github.com/PrashamTrivedi/gitpod-dotfiles
  - Your ~/.claude, ~/.claude.json, ~/.codex, ~/.gemini configs mounted
  - Optional Docker-in-Docker support for containerized workflows
  - Persistent containers that are reused when run with same arguments
  - Support for multiple parallel containers from same directory

EXAMPLES:
  # Create a basic sandbox for a project
  bun docker-sandbox.ts ~/projects/my-app

  # Run Claude Code in sandbox (safe --dangerously-skip-permissions)
  bun docker-sandbox.ts --claude ~/projects/my-app

  # Run Claude with a specific prompt
  bun docker-sandbox.ts --claude --prompt "fix the auth bug" ~/projects/my-app

  # Run Claude in full isolation (no network)
  bun docker-sandbox.ts --claude --no-network ~/projects/my-app

  # Run Claude with dev server ports exposed
  bun docker-sandbox.ts --claude --ports 3000,5173,8080 ~/projects/my-app

  # Create a sandbox with Docker-in-Docker support
  bun docker-sandbox.ts --dind ~/projects/my-docker-app

  # Start an interactive shell in existing container
  bun docker-sandbox.ts --shell ~/projects/my-app

  # Stop a running container
  bun docker-sandbox.ts --stop ~/projects/my-app

  # Remove container only
  bun docker-sandbox.ts --remove ~/projects/my-app

  # Complete teardown (container + image)
  bun docker-sandbox.ts --teardown ~/projects/my-app

  # Force rebuild image
  bun docker-sandbox.ts --rebuild ~/projects/my-app

PARALLEL EXECUTION EXAMPLES:
  # Spawn multiple containers in background with auto-allocated ports
  bun docker-sandbox.ts --claude --background --auto-port --prompt "task 1" ~/project &
  bun docker-sandbox.ts --claude --background --auto-port --prompt "task 2" ~/project &
  bun docker-sandbox.ts --claude --background --auto-port --prompt "task 3" ~/project &

  # With explicit instance IDs
  bun docker-sandbox.ts --claude --background --instance-id ticket-42 --ports 3000 ~/project
  bun docker-sandbox.ts --claude --background --instance-id ticket-43 --ports 3001 ~/project

  # Check status of all instances
  bun docker-sandbox.ts --status

  # Monitor specific instance
  docker logs -f ai-sandbox-xxxx-ticket-42

NOTES:
  - Containers are named based on arguments to enable reuse
  - Different working directories create different containers
  - Image is cached and reused for same Docker-in-Docker setting
  - Use --teardown to completely clean up when not working
  - Use --background with --instance-id for true parallel execution
  - Use --auto-port to automatically find available ports
`

interface Args {
  dind: boolean
  help: boolean
  name?: string
  stop: boolean
  remove: boolean
  teardown: boolean
  rebuild: boolean
  logs: boolean
  shell: boolean
  claude: boolean
  noNetwork: boolean
  ports?: string
  prompt?: string
  workingDir?: string
  // New parallel execution options
  background: boolean      // Run in background, don't block
  instanceId?: string      // Unique instance identifier
  autoPort: boolean        // Auto-allocate ports (find free ports)
  status: boolean          // Show all running instances
  // Resource management
  memory?: string          // Container memory limit (e.g., "4g")
  model?: string           // Claude model: sonnet, haiku, opus
}

function parseArguments(): Args {
  try {
    const {values, positionals} = parseArgs({
      args: Bun.argv.slice(2),
      options: {
        dind: {type: "boolean", short: "d", default: false},
        help: {type: "boolean", short: "h", default: false},
        name: {type: "string", short: "n"},
        stop: {type: "boolean", default: false},
        remove: {type: "boolean", default: false},
        teardown: {type: "boolean", default: false},
        rebuild: {type: "boolean", default: false},
        logs: {type: "boolean", default: false},
        shell: {type: "boolean", default: false},
        claude: {type: "boolean", default: false},
        "no-network": {type: "boolean", default: false},
        ports: {type: "string", short: "p"},
        prompt: {type: "string"},
        // New parallel execution options
        background: {type: "boolean", short: "b", default: false},
        "instance-id": {type: "string"},
        "auto-port": {type: "boolean", default: false},
        status: {type: "boolean", default: false},
        // Resource management
        memory: {type: "string"},
        model: {type: "string"},
      },
      strict: true,
      allowPositionals: true,
    })

    return {
      dind: values.dind as boolean,
      help: values.help as boolean,
      name: values.name as string | undefined,
      stop: values.stop as boolean,
      remove: values.remove as boolean,
      teardown: values.teardown as boolean,
      rebuild: values.rebuild as boolean,
      logs: values.logs as boolean,
      shell: values.shell as boolean,
      claude: values.claude as boolean,
      noNetwork: values["no-network"] as boolean,
      ports: values.ports as string | undefined,
      prompt: values.prompt as string | undefined,
      workingDir: positionals[0],
      // New parallel execution options
      background: values.background as boolean,
      instanceId: values["instance-id"] as string | undefined,
      autoPort: values["auto-port"] as boolean,
      status: values.status as boolean,
      // Resource management
      memory: values.memory as string | undefined,
      model: values.model as string | undefined,
    }
  } catch (error) {
    console.error(`Error: ${error.message}\n`)
    console.log(HELP_TEXT)
    process.exit(1)
  }
}

function generateImageName(dind: boolean): string {
  return dind ? "ai-sandbox:dind" : "ai-sandbox:base"
}

function generateContainerName(
  dind: boolean,
  workingDir: string,
  customName?: string,
  instanceId?: string
): string {
  if (customName) return customName

  const hash = createHash("md5")
    .update(`${dind}:${workingDir}`)
    .digest("hex")
    .substring(0, 8)

  const baseName = `ai-sandbox-${hash}`
  return instanceId ? `${baseName}-${instanceId}` : baseName
}

function generateInstanceId(): string {
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 6)
}

// ============================================================================
// INSTANCE TRACKING SYSTEM
// ============================================================================

const TRACKER_FILE = "/tmp/yolo-instances.json"

interface InstanceInfo {
  containerName: string
  workingDir: string
  pid?: number
  startedAt: string
  prompt?: string
  ports?: string
  background: boolean
}

async function readInstanceTracker(): Promise<Record<string, InstanceInfo>> {
  try {
    if (existsSync(TRACKER_FILE)) {
      const content = await Bun.file(TRACKER_FILE).text()
      return JSON.parse(content)
    }
  } catch {
    // File doesn't exist or is corrupted
  }
  return {}
}

async function writeInstanceTracker(name: string, info: InstanceInfo): Promise<void> {
  const instances = await readInstanceTracker()
  instances[name] = info
  await Bun.write(TRACKER_FILE, JSON.stringify(instances, null, 2))
}

async function removeInstanceTracker(name: string): Promise<void> {
  const instances = await readInstanceTracker()
  delete instances[name]
  await Bun.write(TRACKER_FILE, JSON.stringify(instances, null, 2))
}

async function showInstanceStatus(): Promise<void> {
  log.phase("YOLO Instance Status")

  // Get all running ai-sandbox containers
  const result = await $`docker ps --filter "name=ai-sandbox" --format "{{.Names}}\t{{.Status}}\t{{.Ports}}"`.quiet().text()
  const runningContainers = result.trim().split("\n").filter(Boolean)

  // Read tracked instances
  const tracked = await readInstanceTracker()

  if (runningContainers.length === 0 && Object.keys(tracked).length === 0) {
    log.info("No running YOLO instances found")
    return
  }

  console.log("\n" + "─".repeat(100))
  console.log("| Container".padEnd(30) + "| Directory".padEnd(35) + "| Status".padEnd(15) + "| Ports".padEnd(20) + "|")
  console.log("─".repeat(100))

  for (const line of runningContainers) {
    const [name, status, ports] = line.split("\t")
    const info = tracked[name]
    const dir = info?.workingDir || "N/A"
    const statusShort = status?.substring(0, 12) || "Running"
    const portsShort = ports?.substring(0, 17) || "-"

    console.log(`| ${name.padEnd(28)}| ${dir.substring(0, 33).padEnd(33)}| ${statusShort.padEnd(13)}| ${portsShort.padEnd(18)}|`)
  }

  console.log("─".repeat(100))

  // Show background instances from tracker that might not be running
  const trackedNames = Object.keys(tracked)
  const runningNames = runningContainers.map(l => l.split("\t")[0])
  const staleInstances = trackedNames.filter(n => !runningNames.includes(n))

  if (staleInstances.length > 0) {
    console.log("\n⚠️  Stale tracked instances (containers no longer running):")
    for (const name of staleInstances) {
      console.log(`   - ${name}`)
      await removeInstanceTracker(name)
    }
    console.log("   (Cleaned up tracker file)")
  }

  console.log("\n📋 Commands:")
  console.log("   View logs:     docker logs -f <container-name>")
  console.log("   Stop all:      docker stop $(docker ps -q --filter 'name=ai-sandbox')")
  console.log("   Remove all:    docker rm -f $(docker ps -aq --filter 'name=ai-sandbox')")
}

// ============================================================================
// SYSTEM MEMORY CHECK
// ============================================================================

async function getAvailableMemoryGB(): Promise<number> {
  try {
    const result = await $`free -g | awk '/^Mem:/ {print $7}'`.quiet().text()
    return parseInt(result.trim(), 10) || 0
  } catch {
    return 0  // Can't determine, proceed anyway
  }
}

async function checkMemoryBeforeSpawn(requestedMemory: string, containerCount: number = 1): Promise<void> {
  const availableGB = await getAvailableMemoryGB()
  if (availableGB === 0) return  // Can't check, skip

  // Parse requested memory (e.g., "4g" -> 4)
  const match = requestedMemory.match(/^(\d+)g?$/i)
  const requestedGB = match ? parseInt(match[1], 10) : 4

  const runningContainers = await $`docker ps --filter "name=ai-sandbox" -q | wc -l`.quiet().text()
  const existingContainers = parseInt(runningContainers.trim(), 10) || 0
  const totalUsedByExisting = existingContainers * requestedGB

  // Calculate safe parallel capacity
  const safeParallelCount = Math.max(1, Math.floor((availableGB - 2) / requestedGB))  // Keep 2GB for host

  log.info(`System memory: ${availableGB}GB available`)
  log.info(`Running YOLO containers: ${existingContainers}`)
  log.info(`Safe parallel capacity: ${safeParallelCount} containers @ ${requestedGB}GB each`)

  if (existingContainers >= safeParallelCount) {
    log.warn(`At capacity! Consider stopping existing containers or using --memory 2g`)
  }

  // Recommend memory settings based on desired parallelism
  if (availableGB < requestedGB + 2) {
    log.warn(`Low memory for ${requestedGB}GB container`)
    log.info("Recommendations:")
    log.info(`  - 2 parallel: --memory ${Math.floor((availableGB - 2) / 2)}g`)
    log.info(`  - 3 parallel: --memory ${Math.floor((availableGB - 2) / 3)}g`)
  }
}

// ============================================================================
// AUTO PORT ALLOCATION
// ============================================================================

async function isPortInUse(port: number): Promise<boolean> {
  try {
    // Check if port is in use by any process
    const result = await $`lsof -i :${port} 2>/dev/null`.quiet().nothrow()
    if (result.exitCode === 0) return true

    // Also check docker port bindings
    const dockerResult = await $`docker ps --format "{{.Ports}}" | grep -q ":${port}->"`.quiet().nothrow()
    return dockerResult.exitCode === 0
  } catch {
    return false
  }
}

async function findFreePorts(count: number, startFrom: number = 3000): Promise<number[]> {
  const freePorts: number[] = []
  let port = startFrom

  while (freePorts.length < count && port < 65535) {
    const inUse = await isPortInUse(port)
    if (!inUse) {
      freePorts.push(port)
    }
    port++
  }

  if (freePorts.length < count) {
    throw new Error(`Could not find ${count} free ports starting from ${startFrom}`)
  }

  return freePorts
}

async function allocatePorts(requestedPorts?: string): Promise<string | undefined> {
  if (!requestedPorts) return undefined

  const portList = requestedPorts.split(",").map(p => p.trim())
  const allocatedPorts: number[] = []

  for (const portStr of portList) {
    const port = parseInt(portStr, 10)
    if (isNaN(port)) {
      log.warn(`Invalid port: ${portStr}`)
      continue
    }

    const inUse = await isPortInUse(port)
    if (inUse) {
      // Find next free port
      const [freePort] = await findFreePorts(1, port + 1)
      log.warn(`Port ${port} in use, allocated ${freePort} instead`)
      allocatedPorts.push(freePort)
    } else {
      allocatedPorts.push(port)
    }
  }

  return allocatedPorts.join(",")
}

async function autoAllocatePorts(count: number = 1): Promise<string> {
  const ports = await findFreePorts(count, 3000)
  return ports.join(",")
}

async function imageExists(imageName: string): Promise<boolean> {
  try {
    const result = await $`docker images -q ${imageName}`.quiet()
    return result.stdout.toString().trim().length > 0
  } catch {
    return false
  }
}

async function containerExists(containerName: string): Promise<boolean> {
  try {
    const result = await $`docker ps -a -q -f name=^${containerName}$`.quiet()
    return result.stdout.toString().trim().length > 0
  } catch {
    return false
  }
}

async function containerRunning(containerName: string): Promise<boolean> {
  try {
    const result = await $`docker ps -q -f name=^${containerName}$`.quiet()
    return result.stdout.toString().trim().length > 0
  } catch {
    return false
  }
}

function createDockerfile(dind: boolean): string {
  return `FROM ubuntu:22.04

# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=24

# Terminal environment for TTY support
ENV TERM=xterm-256color
ENV COLORTERM=truecolor
ENV FORCE_COLOR=1

# Install basic tools, Git, terminal utilities, and Node.js 24
RUN apt-get update && apt-get install -y \\
    curl \\
    git \\
    wget \\
    unzip \\
    ca-certificates \\
    gnupg \\
    build-essential \\
    ncurses-base \\
    ncurses-term \\
    less \\
    vim \\
    locales \\
    sudo \\
    && locale-gen en_US.UTF-8 \\
    && curl -fsSL https://deb.nodesource.com/setup_\${NODE_VERSION}.x | bash - \\
    && apt-get install -y nodejs \\
    && npm install -g typescript \\
    && apt-get clean \\
    && rm -rf /var/lib/apt/lists/*

# Set locale for proper terminal rendering
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Create non-root user for Claude Code (--dangerously-skip-permissions doesn't work as root)
RUN useradd -m -s /bin/bash -G sudo sandbox \\
    && echo "sandbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Install Bun globally first, then for sandbox user
RUN curl -fsSL https://bun.sh/install | bash \\
    && ln -s /root/.bun/bin/bun /usr/local/bin/bun

${dind ? `
# Install Docker for Docker-in-Docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh \\
    && sh get-docker.sh \\
    && rm get-docker.sh \\
    && usermod -aG docker sandbox \\
    && apt-get clean \\
    && rm -rf /var/lib/apt/lists/*
` : ''}

# Install AI Coding Agents globally
RUN npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli

# Switch to sandbox user for dotfiles setup
USER sandbox
WORKDIR /home/sandbox

# Clone and setup dotfiles as sandbox user
ENV GITHUB_USER_EMAIL="dev@sandbox.local"
ENV GITHUB_USER_NAME="AI Sandbox"
RUN git clone https://github.com/PrashamTrivedi/gitpod-dotfiles.git /tmp/dotfiles \\
    && cd /tmp/dotfiles \\
    && chmod +x setup.sh \\
    && ./setup.sh || true \\
    && rm -rf /tmp/dotfiles

# Set Fish as default shell for sandbox user
RUN sudo chsh -s /usr/bin/fish sandbox || true

# Create workspace directory with sandbox ownership
RUN sudo mkdir -p /workspace && sudo chown sandbox:sandbox /workspace
WORKDIR /workspace

# Create entrypoint script to fix permissions on mounted configs
RUN echo '#!/bin/bash\\n\\
# Fix ownership of mounted config files for sandbox user\\n\\
for dir in /home/sandbox/.claude* /home/sandbox/.codex /home/sandbox/.gemini; do\\n\\
  if [ -e "$dir" ]; then\\n\\
    sudo chown -R sandbox:sandbox "$dir" 2>/dev/null || true\\n\\
  fi\\n\\
done\\n\\
# Fix workspace ownership\\n\\
sudo chown -R sandbox:sandbox /workspace 2>/dev/null || true\\n\\
exec "$@"' | sudo tee /usr/local/bin/entrypoint.sh > /dev/null \\
    && sudo chmod +x /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# Keep container running
CMD ${dind ? '["sh", "-c", "sudo dockerd & tail -f /dev/null"]' : '["tail", "-f", "/dev/null"]'}
`
}

async function buildImage(imageName: string, dind: boolean, rebuild: boolean = false): Promise<void> {
  log.phase("Building Docker Image", 4)

  log.step("Preparing build configuration", {
    image: imageName,
    dind: dind ? "enabled" : "disabled",
    rebuild: rebuild ? "yes" : "no",
  })

  log.step("Generating Dockerfile")
  const dockerfile = createDockerfile(dind)
  const tmpDir = await $`mktemp -d`.text()
  const dockerfilePath = join(tmpDir.trim(), "Dockerfile")

  log.step("Writing Dockerfile to temporary directory", {path: dockerfilePath})
  await Bun.write(dockerfilePath, dockerfile)

  try {
    log.step("Executing docker build (this may take several minutes)")
    log.info("Installing: Ubuntu 22.04, Node.js 24, Bun, TypeScript, Git")
    log.info("Installing: Claude Code, Codex CLI, Gemini CLI")
    log.info("Configuring: dotfiles, user permissions, workspace")

    const buildCmd = rebuild
      ? `docker build --no-cache -t ${imageName} -f ${dockerfilePath} ${tmpDir.trim()}`
      : `docker build -t ${imageName} -f ${dockerfilePath} ${tmpDir.trim()}`
    await $`sh -c ${buildCmd}`
    log.success(`Image built successfully: ${imageName}`)
  } catch (error) {
    log.error(`Failed to build image`, {error: String(error)})
    process.exit(1)
  } finally {
    await $`rm -rf ${tmpDir.trim()}`
    log.debug("Cleaned up temporary build directory")
  }
}

interface ContainerOptions {
  imageName: string
  containerName: string
  workingDir: string
  dind: boolean
  noNetwork: boolean
  ports?: string
  memory?: string  // Memory limit (e.g., "4g")
}

async function createContainer(options: ContainerOptions): Promise<void> {
  const {imageName, containerName, workingDir, dind, noNetwork, ports, memory} = options
  log.phase("Creating Container", 5)

  log.step("Validating environment")
  const homeDir = process.env.HOME || process.env.USERPROFILE
  if (!homeDir) {
    log.error("Could not determine home directory")
    process.exit(1)
  }

  if (!existsSync(workingDir)) {
    log.error(`Working directory does not exist: ${workingDir}`)
    process.exit(1)
  }
  log.debug("Environment validated", {homeDir, workingDir})

  log.step("Configuring container options")
  const dockerArgs = [
    `-t`,                              // Allocate TTY for interactive tools
    `--init`,                          // Proper PID 1 and signal handling
    `-e TERM=xterm-256color`,          // Terminal type for colors
    `-v "${workingDir}:/workspace"`,
  ]

  // Container runs as non-root 'sandbox' user
  const containerHome = "/home/sandbox"

  log.step("Mounting configuration files")
  const mountedConfigs: string[] = []

  // Mount all .claude* files and directories from home
  try {
    const homeEntries = readdirSync(homeDir)
    const claudeEntries = homeEntries.filter((entry: string) => entry.startsWith(".claude"))
    for (const entry of claudeEntries) {
      const hostPath = join(homeDir, entry)
      const containerPath = `${containerHome}/${entry}`
      dockerArgs.push(`-v "${hostPath}:${containerPath}"`)
      mountedConfigs.push(entry)
    }
  } catch (error) {
    log.warn("Could not read home directory for .claude* files")
  }

  // Mount other AI tool configs if they exist
  const otherConfigs = [
    {name: ".codex", path: join(homeDir, ".codex"), mount: `${containerHome}/.codex`},
    {name: ".gemini", path: join(homeDir, ".gemini"), mount: `${containerHome}/.gemini`},
  ]

  for (const {name, path, mount} of otherConfigs) {
    if (existsSync(path)) {
      dockerArgs.push(`-v "${path}:${mount}"`)
      mountedConfigs.push(name)
    }
  }

  if (mountedConfigs.length > 0) {
    log.info(`Mounted configs: ${mountedConfigs.join(", ")}`)
  }

  // Mount additionalDirectories from Claude settings (so container has same context)
  log.step("Mounting additional directories from Claude settings")
  const additionalDirs: string[] = []
  try {
    const settingsPath = join(homeDir, ".claude", "settings.json")
    if (existsSync(settingsPath)) {
      const settingsContent = await Bun.file(settingsPath).text()
      const settings = JSON.parse(settingsContent)
      const dirs = settings?.permissions?.additionalDirectories || []
      for (const dir of dirs) {
        if (typeof dir === "string" && existsSync(dir) && dir !== workingDir) {
          // Mount at same path so Claude's references work
          dockerArgs.push(`-v "${dir}:${dir}:ro"`)
          additionalDirs.push(dir)
        }
      }
    }
  } catch (error) {
    log.debug("Could not read additionalDirectories from settings", {error: String(error)})
  }

  if (additionalDirs.length > 0) {
    log.info(`Mounted additional dirs: ${additionalDirs.join(", ")}`)
  }

  // Add Docker socket for Docker-in-Docker
  if (dind) {
    dockerArgs.push(`--privileged`)
    log.info("Docker-in-Docker: enabled (privileged mode)")
  }

  // Network isolation
  if (noNetwork) {
    dockerArgs.push(`--network=none`)
    log.info("Network isolation: enabled")
  }

  // Port forwarding
  if (ports) {
    const portList = ports.split(",").map(p => p.trim())
    for (const port of portList) {
      dockerArgs.push(`-p ${port}:${port}`)
    }
    log.info(`Exposed ports: ${portList.join(", ")}`)
  }

  // Memory limit (default 4g to prevent OOM when running multiple containers)
  const memoryLimit = memory || "4g"
  dockerArgs.push(`--memory=${memoryLimit}`)
  dockerArgs.push(`--memory-swap=${memoryLimit}`)  // Disable swap to fail fast
  log.info(`Memory limit: ${memoryLimit}`)

  log.step("Starting container")
  const dockerCmd = `docker run -d --name ${containerName} ${dockerArgs.join(" ")} ${imageName}`
  log.debug("Docker command", {cmd: dockerCmd})

  try {
    await $`sh -c ${dockerCmd}`
    log.success(`Container created and started: ${containerName}`)
  } catch (error) {
    log.error(`Failed to create container`, {error: String(error)})
    process.exit(1)
  }
}

async function startContainer(containerName: string): Promise<void> {
  log.info(`Starting container: ${containerName}`)
  try {
    await $`docker start ${containerName}`
    log.success(`Container started: ${containerName}`)
  } catch (error) {
    log.error(`Failed to start container`, {error: String(error)})
    process.exit(1)
  }
}

async function stopContainer(containerName: string): Promise<void> {
  log.info(`Stopping container: ${containerName}`)
  try {
    await $`docker stop ${containerName}`
    log.success(`Container stopped: ${containerName}`)
  } catch (error) {
    log.error(`Failed to stop container`, {error: String(error)})
    process.exit(1)
  }
}

async function removeContainer(containerName: string): Promise<void> {
  log.info(`Removing container: ${containerName}`)

  if (await containerRunning(containerName)) {
    await stopContainer(containerName)
  }

  try {
    await $`docker rm ${containerName}`
    log.success(`Container removed: ${containerName}`)
  } catch (error) {
    log.error(`Failed to remove container`, {error: String(error)})
    process.exit(1)
  }
}

async function removeImage(imageName: string): Promise<void> {
  log.info(`Removing image: ${imageName}`)
  try {
    await $`docker rmi ${imageName}`
    log.success(`Image removed: ${imageName}`)
  } catch (error) {
    log.error(`Failed to remove image`, {error: String(error)})
    process.exit(1)
  }
}

async function teardown(imageName: string, containerName: string): Promise<void> {
  log.phase("Complete Teardown", 2)

  // Remove container if it exists
  log.step("Checking container")
  if (await containerExists(containerName)) {
    await removeContainer(containerName)
  } else {
    log.info(`Container does not exist: ${containerName}`)
  }

  // Remove image if it exists
  log.step("Checking image")
  if (await imageExists(imageName)) {
    await removeImage(imageName)
  } else {
    log.info(`Image does not exist: ${imageName}`)
  }

  log.success("Teardown complete!")
}

async function showLogs(containerName: string): Promise<void> {
  log.phase("YOLO Session Logs")
  log.info(`Container: ${containerName}`)
  log.info(`Log directory: ${YOLO_LOGS_DIR}`)

  try {
    // List available log files
    const result = await $`docker exec ${containerName} ls -1t ${YOLO_LOGS_DIR} 2>/dev/null`.quiet().text()
    const files = result.trim().split("\n").filter((f: string) => f.endsWith(".log"))

    if (files.length === 0) {
      log.warn("No session logs found yet. Run a Claude session first.")
      return
    }

    log.info(`Found ${files.length} session log(s)`)
    console.log("\n📋 Available logs (newest first):")
    files.forEach((f: string, i: number) => {
      console.log(`   ${i + 1}. ${f}`)
    })

    // Show the most recent log
    const latestLog = files[0]
    console.log(`\n📄 Latest session: ${latestLog}`)
    console.log("─".repeat(60))

    await $`docker exec ${containerName} cat ${YOLO_LOGS_DIR}/${latestLog}`

    console.log("─".repeat(60))
    log.info(`To watch live: docker exec ${containerName} tail -f ${YOLO_LOGS_DIR}/${latestLog}`)
    log.info(`To view older: docker exec ${containerName} cat ${YOLO_LOGS_DIR}/<filename>`)
  } catch (error) {
    // Check if directory doesn't exist
    const dirExists = await $`docker exec ${containerName} test -d ${YOLO_LOGS_DIR}`.quiet().nothrow()
    if (dirExists.exitCode !== 0) {
      log.warn("No session logs found yet. Run a Claude session first.")
    } else {
      log.error(`Failed to get logs`, {error: String(error)})
      process.exit(1)
    }
  }
}

async function startShell(containerName: string): Promise<void> {
  log.phase("Starting Interactive Shell")
  log.info(`Container: ${containerName}`)
  log.info("User: sandbox (non-root)")

  try {
    // Try fish shell first (from dotfiles), fallback to bash
    const fishExists = await $`docker exec -u sandbox ${containerName} which fish`.quiet().nothrow()
    const shell = fishExists.exitCode === 0 ? "/usr/bin/fish" : "/bin/bash"
    log.info(`Shell: ${shell}`)
    console.log("") // Blank line before shell
    await $`docker exec -u sandbox -it ${containerName} ${shell}`
  } catch (error) {
    log.error(`Failed to start shell`, {error: String(error)})
    process.exit(1)
  }
}

// Persistent logs directory in workspace (survives container restarts, visible on host)
const YOLO_LOGS_DIR = "/workspace/.yolo-logs"
const MAX_LOG_FILES = 20

async function ensureLogsDir(containerName: string): Promise<void> {
  // Create directory and verify it exists
  await $`docker exec ${containerName} mkdir -p ${YOLO_LOGS_DIR}`.quiet()
  // Verify creation
  const result = await $`docker exec ${containerName} test -d ${YOLO_LOGS_DIR} && echo "ok"`.quiet().text()
  if (!result.includes("ok")) {
    throw new Error(`Failed to create logs directory: ${YOLO_LOGS_DIR}`)
  }
}

async function rotateOldLogs(containerName: string): Promise<void> {
  try {
    const result = await $`docker exec ${containerName} ls -1t ${YOLO_LOGS_DIR}`.quiet().text()
    const files = result.trim().split("\n").filter((f: string) => f.endsWith(".log"))
    if (files.length > MAX_LOG_FILES) {
      const toDelete = files.slice(MAX_LOG_FILES)
      for (const file of toDelete) {
        await $`docker exec ${containerName} rm -f ${YOLO_LOGS_DIR}/${file}`.quiet()
      }
      log.debug(`Rotated ${toDelete.length} old log files`)
    }
  } catch {
    // Directory might not exist yet
  }
}

interface RunClaudeOptions {
  containerName: string
  prompt?: string
  background?: boolean
  workingDir?: string
  ports?: string
  model?: string  // Claude model: sonnet, haiku, opus
}

async function runClaude(options: RunClaudeOptions): Promise<string | void> {
  const {containerName, prompt, background = false, workingDir, ports, model} = options
  const hasTTY = process.stdin.isTTY && process.stdout.isTTY

  // Build model flag if specified (pass through as-is, don't force format)
  const modelFlag = model ? `--model ${model}` : ""

  log.phase("Running Claude Code in Sandbox", prompt ? 4 : 3)

  log.step("Configuring execution environment", {
    container: containerName,
    user: "sandbox",
    mode: "--dangerously-skip-permissions --output-format stream-json",
    model: model || "default",
    tty: hasTTY ? "available" : "not available",
    background: background ? "yes" : "no",
  })

  if (prompt) {
    log.step("Preparing prompt for Claude")
    log.info(`Prompt: "${prompt.length > 100 ? prompt.substring(0, 100) + "..." : prompt}"`)
  }

  // Setup persistent logging - MUST complete before spawning Claude
  log.step("Setting up logging infrastructure")
  try {
    await ensureLogsDir(containerName)
    log.debug("Logs directory verified")
  } catch (error) {
    log.error("Failed to create logs directory", {error: String(error)})
    process.exit(1)
  }

  await rotateOldLogs(containerName)

  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
  const logFile = `${YOLO_LOGS_DIR}/session-${timestamp}.log`

  // Pre-create the log file to ensure tee can write to it
  await $`docker exec ${containerName} touch ${logFile}`.quiet()
  const fileCheck = await $`docker exec ${containerName} test -f ${logFile} && echo "ok"`.quiet().text()
  if (!fileCheck.includes("ok")) {
    log.error("Failed to create log file")
    process.exit(1)
  }

  log.step("Launching Claude Code")
  log.info("Claude is now running inside the container...")
  log.info(`Log file: ${logFile}`)
  log.info(`Watch live: docker exec ${containerName} tail -f ${logFile}`)

  // Background mode: spawn without waiting
  if (background) {
    if (!prompt) {
      log.error("Background mode requires --prompt to be specified")
      process.exit(1)
    }

    // Escape shell special characters
    const escapedPrompt = prompt
      .replace(/\\/g, '\\\\')
      .replace(/"/g, '\\"')
      .replace(/\$/g, '\\$')
      .replace(/`/g, '\\`')

    // Use stream-json output piped through tee for logging
    const claudeCmd = `claude --dangerously-skip-permissions --verbose ${modelFlag} -p "${escapedPrompt}" --output-format stream-json 2>&1 | tee ${logFile}`

    // Spawn without TTY for background mode
    const proc = (globalThis as any).Bun.spawn([
      "docker", "exec", "-u", "sandbox", containerName,
      "sh", "-c", claudeCmd
    ], {
      stdout: "pipe",
      stderr: "pipe",
      stdin: "ignore",
    })

    // Track this instance
    await writeInstanceTracker(containerName, {
      containerName,
      workingDir: workingDir || process.cwd(),
      pid: proc.pid,
      startedAt: new Date().toISOString(),
      prompt,
      ports,
      background: true,
    })

    log.success(`Claude spawned in background`)
    log.info(`Container: ${containerName}`)
    log.info(`PID: ${proc.pid}`)
    log.info(`Monitor logs: docker logs -f ${containerName}`)
    log.info(`Monitor session: docker exec ${containerName} tail -f ${logFile}`)
    log.info(`Check status: bun docker-sandbox.ts --status`)

    // Return immediately without waiting
    return
  }

  // Foreground mode: existing behavior
  console.log("") // Blank line before Claude output

  try {
    if (prompt) {
      // Escape shell special characters
      const escapedPrompt = prompt
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/\$/g, '\\$')
        .replace(/`/g, '\\`')

      const ttyFlags = hasTTY ? "-it" : "-i"

      // Use stream-json output piped through tee for logging
      const claudeCmd = `claude --dangerously-skip-permissions --verbose ${modelFlag} -p "${escapedPrompt}" --output-format stream-json 2>&1 | tee ${logFile}`

      const proc = (globalThis as any).Bun.spawn([
        "docker", "exec", "-u", "sandbox", ttyFlags, containerName,
        "sh", "-c", claudeCmd
      ], {
        stdout: "inherit",
        stderr: "inherit",
        stdin: "inherit",
      })

      const exitCode = await proc.exited
      if (exitCode !== 0 && exitCode !== 130) {
        throw { exitCode }
      }

      const output = await $`docker exec ${containerName} cat ${logFile}`.text()
      log.success(`Claude Code session completed`)
      log.info(`Log saved: ${logFile}`)

      return output.trim()
    } else {
      // Interactive mode - stream-json doesn't work well interactively, use text
      if (!hasTTY) {
        log.error("Interactive mode requires a TTY. Use --prompt to run non-interactively.")
        process.exit(1)
      }

      // For interactive, use text output (more readable) with tee
      const claudeCmd = `claude --dangerously-skip-permissions ${modelFlag} 2>&1 | tee ${logFile}`

      const proc = (globalThis as any).Bun.spawn([
        "docker", "exec", "-u", "sandbox", "-it", containerName,
        "sh", "-c", claudeCmd
      ], {
        stdout: "inherit",
        stderr: "inherit",
        stdin: "inherit",
      })

      const exitCode = await proc.exited
      if (exitCode !== 0 && exitCode !== 130) {
        throw { exitCode }
      }
      log.success("Claude Code session completed")
      log.info(`Log saved: ${logFile}`)
    }
  } catch (error: unknown) {
    const exitCode = (error as {exitCode?: number})?.exitCode
    if (exitCode !== 130 && exitCode !== 0) {
      log.error(`Claude exited with error`, {exitCode, error: String(error)})
      log.info(`Log preserved: ${logFile}`)
      process.exit(1)
    }
    log.info("Claude session ended by user (Ctrl+C)")
    log.info(`Log saved: ${logFile}`)
  }
}

async function main() {
  const args = parseArguments()

  if (args.help) {
    console.log(HELP_TEXT)
    process.exit(0)
  }

  // Handle --status early (doesn't require working directory)
  if (args.status) {
    await showInstanceStatus()
    process.exit(0)
  }

  // Validate working directory is provided (except for help and status)
  if (!args.workingDir && !args.help && !args.status) {
    log.error("Working directory is required")
    console.log(HELP_TEXT)
    process.exit(1)
  }

  const workingDir = args.workingDir!.startsWith("/")
    ? args.workingDir!
    : join(process.cwd(), args.workingDir!)

  const imageName = generateImageName(args.dind)

  // Determine if we need a unique container for parallel execution
  const isParallelMode = args.background || args.instanceId
  const instanceId = isParallelMode ? (args.instanceId || generateInstanceId()) : undefined
  const containerName = generateContainerName(args.dind, workingDir, args.name, instanceId)

  // Handle auto port allocation
  let effectivePorts = args.ports
  if (args.autoPort && !effectivePorts) {
    log.info("Auto-allocating ports...")
    effectivePorts = await autoAllocatePorts(1)
    log.info(`Allocated port: ${effectivePorts}`)
  } else if (effectivePorts) {
    // Validate and reallocate ports if needed
    effectivePorts = await allocatePorts(effectivePorts)
  }

  // Determine operation mode for header
  const operationMode = args.teardown ? "teardown" :
    args.remove ? "remove" :
      args.stop ? "stop" :
        args.logs ? "logs" :
          args.shell ? "shell" :
            args.background ? "background" :
              args.claude ? "claude" :
                args.rebuild ? "rebuild" : "setup"

  log.phase("AI Coding Agents Docker Sandbox")
  log.info("Configuration loaded", {
    operation: operationMode,
    image: imageName,
    container: containerName,
    workingDir: workingDir,
    dind: args.dind ? "enabled" : "disabled",
    network: args.noNetwork ? "isolated" : "enabled",
    ports: effectivePorts || "none",
    claude: args.claude ? "enabled" : "disabled",
    background: args.background ? "yes" : "no",
    instanceId: instanceId || "none",
  })

  // Handle special operations
  if (args.teardown) {
    await teardown(imageName, containerName)
    log.summary()
    process.exit(0)
  }

  if (args.remove) {
    if (!(await containerExists(containerName))) {
      log.error(`Container does not exist: ${containerName}`)
      process.exit(1)
    }
    await removeContainer(containerName)
    log.summary()
    process.exit(0)
  }

  if (args.stop) {
    if (!(await containerExists(containerName))) {
      log.error(`Container does not exist: ${containerName}`)
      process.exit(1)
    }
    await stopContainer(containerName)
    log.summary()
    process.exit(0)
  }

  if (args.logs) {
    if (!(await containerExists(containerName))) {
      log.error(`Container does not exist: ${containerName}`)
      process.exit(1)
    }
    await showLogs(containerName)
    process.exit(0)
  }

  // Build image if it doesn't exist or rebuild is requested
  log.phase("Image Preparation")
  if (args.rebuild) {
    if (await imageExists(imageName)) {
      log.info(`Rebuilding image: ${imageName}`)
      await removeImage(imageName)
    }
    await buildImage(imageName, args.dind, true)
  } else if (!(await imageExists(imageName))) {
    log.info(`Image not found, building: ${imageName}`)
    await buildImage(imageName, args.dind)
  } else {
    log.success(`Image already exists: ${imageName}`)
  }

  // Create or start container
  log.phase("Container Setup")

  // Check available memory before spawning
  await checkMemoryBeforeSpawn(args.memory || "4g")

  const containerAlreadyExists = await containerExists(containerName)

  // For parallel mode (background or instanceId), always create new container
  if (isParallelMode && containerAlreadyExists) {
    log.warn(`Container ${containerName} already exists for this instance ID`)
    log.info("Use a different --instance-id or omit for auto-generated ID")
    process.exit(1)
  }

  if (!containerAlreadyExists) {
    await createContainer({
      imageName,
      containerName,
      workingDir,
      dind: args.dind,
      noNetwork: args.noNetwork,
      ports: effectivePorts,
      memory: args.memory,
    })
  } else {
    log.info(`Container already exists: ${containerName}`)

    const running = await containerRunning(containerName)
    if (!running) {
      await startContainer(containerName)
    } else {
      log.success(`Container already running: ${containerName}`)
    }
  }

  // Run Claude if requested (foreground or background)
  if (args.claude || args.background) {
    const output = await runClaude({
      containerName,
      prompt: args.prompt,
      background: args.background,
      workingDir,
      ports: effectivePorts,
      model: args.model,
    })
    if (output && !args.background) {
      console.log("\n--- Claude Output ---")
      console.log(output)
      console.log("--- End Output ---\n")
    }
    log.summary()
  } else if (args.shell) {
    // Start shell if requested
    await startShell(containerName)
  } else {
    log.phase("Ready")
    log.success("Sandbox is ready for use")
    log.info("Available commands:")
    console.log(`     Run Claude:         bun docker-sandbox.ts --claude ${args.workingDir}`)
    console.log(`     Claude + prompt:    bun docker-sandbox.ts --claude --prompt "fix bugs" ${args.workingDir}`)
    console.log(`     Start a shell:      bun docker-sandbox.ts --shell ${args.workingDir}`)
    console.log(`     View logs:          bun docker-sandbox.ts --logs ${args.workingDir}`)
    console.log(`     Stop container:     bun docker-sandbox.ts --stop ${args.workingDir}`)
    console.log(`     Remove container:   bun docker-sandbox.ts --remove ${args.workingDir}`)
    console.log(`     Full teardown:      bun docker-sandbox.ts --teardown ${args.workingDir}`)
    console.log(`     Rebuild image:      bun docker-sandbox.ts --rebuild ${args.workingDir}`)
    log.summary()
  }
}

main().catch((error) => {
  console.error(`\n❌ Unexpected error: ${error.message}`)
  process.exit(1)
})
