package host

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
	"strings"
	"time"
)

// DockerBackend executes commands inside a Docker container,
// providing a full Linux OS environment with container-level isolation.
type DockerBackend struct {
	container string
	image     string
	timeout   time.Duration
}

// NewDockerBackend creates a Docker backend that runs commands in the
// specified container. If the container does not exist, it will be
// created from the given image.
func NewDockerBackend(container, image string, defaultTimeout time.Duration) *DockerBackend {
	if defaultTimeout <= 0 {
		defaultTimeout = 30 * time.Second
	}
	return &DockerBackend{
		container: container,
		image:     image,
		timeout:   defaultTimeout,
	}
}

func (b *DockerBackend) Name() string { return "docker" }

// ensureContainer checks that the container exists and is running.
// If it doesn't exist, it creates it from the configured image.
func (b *DockerBackend) ensureContainer() error {
	// Check if container exists and is running
	check := exec.Command("docker", "inspect", "-f", "{{.State.Running}}", b.container)
	out, err := check.Output()
	if err == nil && strings.TrimSpace(string(out)) == "true" {
		return nil
	}

	// Check if container exists but is stopped
	if err == nil && strings.TrimSpace(string(out)) == "false" {
		start := exec.Command("docker", "start", b.container)
		if out, err := start.CombinedOutput(); err != nil {
			return fmt.Errorf("cannot start container %s: %s — %w", b.container, string(out), err)
		}
		return nil
	}

	// Create and start a new container
	create := exec.Command("docker", "run", "-d", "--name", b.container,
		"--restart", "unless-stopped",
		b.image, "sleep", "infinity")
	if out, err := create.CombinedOutput(); err != nil {
		return fmt.Errorf("cannot create container %s from image %s: %s — %w",
			b.container, b.image, string(out), err)
	}
	return nil
}

// Exec runs a command inside the Docker container.
func (b *DockerBackend) Exec(ctx context.Context, command, workDir string, timeout time.Duration) (*ExecResult, error) {
	if command == "" {
		return nil, fmt.Errorf("empty command")
	}

	if err := b.ensureContainer(); err != nil {
		return nil, err
	}

	if timeout <= 0 {
		timeout = b.timeout
	}

	execCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	// Build the shell command to run inside the container
	script := command
	if workDir != "" {
		script = fmt.Sprintf("cd %s && %s", shellEscapeDocker(workDir), command)
	}

	cmd := exec.CommandContext(execCtx, "docker", "exec", b.container, "sh", "-c", script)

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	start := time.Now()
	err := cmd.Run()
	elapsed := time.Since(start)

	result := &ExecResult{
		Duration: elapsed.Round(time.Millisecond).String(),
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
	}

	if execCtx.Err() == context.DeadlineExceeded {
		result.TimedOut = true
		result.ExitCode = -1
		return result, fmt.Errorf("command timed out after %s", timeout)
	}

	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			result.ExitCode = exitErr.ExitCode()
		} else {
			result.ExitCode = -1
		}
	} else {
		result.ExitCode = 0
	}

	return result, err
}

// ReadFile reads a file from inside the container using cat.
func (b *DockerBackend) ReadFile(path string, maxBytes int) (string, error) {
	if maxBytes <= 0 {
		maxBytes = 1024 * 1024
	}
	if err := b.ensureContainer(); err != nil {
		return "", err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "docker", "exec", b.container, "cat", path)
	out, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("cannot read file %s: %w", path, err)
	}
	if len(out) > maxBytes {
		out = out[:maxBytes]
	}
	return string(out), nil
}

// WriteFile writes content to a file inside the container.
func (b *DockerBackend) WriteFile(path, content string, maxBytes int) error {
	if maxBytes <= 0 {
		maxBytes = 1024 * 1024
	}
	if len(content) > maxBytes {
		return fmt.Errorf("content too large: %d bytes (max %d)", len(content), maxBytes)
	}
	if err := b.ensureContainer(); err != nil {
		return err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Create parent directory and write file
	cmd := exec.CommandContext(ctx, "docker", "exec", "-i", b.container, "sh", "-c",
		fmt.Sprintf("mkdir -p $(dirname %s) && cat > %s", shellEscapeDocker(path), shellEscapeDocker(path)))
	cmd.Stdin = strings.NewReader(content)
	_, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("cannot write file %s: %w", path, err)
	}
	return nil
}

// ListDir lists a directory inside the container.
func (b *DockerBackend) ListDir(path string) ([]DirEntry, error) {
	if err := b.ensureContainer(); err != nil {
		return nil, err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "docker", "exec", b.container, "sh", "-c",
		fmt.Sprintf("ls -la %s 2>/dev/null | tail -n +2 || echo ''", shellEscapeDocker(path)))
	out, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("cannot list dir %s: %w", path, err)
	}

	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	result := make([]DirEntry, 0, len(lines))
	for _, line := range lines {
		if line == "" || strings.HasPrefix(line, "total ") {
			continue
		}
		// Parse ls -la output: drwxr-xr-x 2 root root 4096 Jan 1 12:00 name
		fields := strings.Fields(line)
		if len(fields) < 9 {
			continue
		}
		isDir := strings.HasPrefix(fields[0], "d")
		name := fields[len(fields)-1]
		if name == "." || name == ".." {
			continue
		}
		var size int64
		fmt.Sscanf(fields[4], "%d", &size)
		result = append(result, DirEntry{
			Name:  name,
			IsDir: isDir,
			Size:  size,
		})
	}
	return result, nil
}

// SystemInfo returns system information from inside the container.
func (b *DockerBackend) SystemInfo() map[string]interface{} {
	info := map[string]interface{}{
		"backend":   "docker",
		"container": b.container,
		"image":     b.image,
	}

	if err := b.ensureContainer(); err != nil {
		info["error"] = err.Error()
		return info
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if out, err := exec.CommandContext(ctx, "docker", "exec", b.container, "uname", "-a").Output(); err == nil {
		info["uname"] = strings.TrimSpace(string(out))
	}
	if out, err := exec.CommandContext(ctx, "docker", "exec", b.container, "hostname").Output(); err == nil {
		info["hostname"] = strings.TrimSpace(string(out))
	}
	if out, err := exec.CommandContext(ctx, "docker", "exec", b.container, "free", "-h").Output(); err == nil {
		info["memory"] = strings.TrimSpace(string(out))
	}
	if out, err := exec.CommandContext(ctx, "docker", "exec", b.container, "df", "-h", "/").Output(); err == nil {
		lines := strings.Split(strings.TrimSpace(string(out)), "\n")
		if len(lines) > 1 {
			info["disk"] = strings.TrimSpace(lines[1])
		}
	}
	return info
}

// DiskUsage returns disk usage for a path inside the container.
func (b *DockerBackend) DiskUsage(path string) (map[string]interface{}, error) {
	if err := b.ensureContainer(); err != nil {
		return nil, err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "docker", "exec", b.container, "stat", path)
	out, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("cannot stat path %s: %w", path, err)
	}
	return map[string]interface{}{
		"path":   path,
		"stat":   strings.TrimSpace(string(out)),
	}, nil
}

// shellEscapeDocker escapes a string for safe use in a shell command.
func shellEscapeDocker(s string) string {
	escaped := strings.ReplaceAll(s, "'", "'\\''")
	return "'" + escaped + "'"
}
