package docker

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
	"github.com/google/uuid"
	"go.uber.org/zap"

	"github.com/RyanCopley/skybridge/faas/internal/domain"
	"github.com/RyanCopley/skybridge/faas/internal/runtime"
)

type DockerRuntime struct {
	client *client.Client
	logger *zap.Logger
	config *Config
}

type Config struct {
	DockerHost     string            `json:"docker_host"`
	NetworkMode    string            `json:"network_mode"`
	SecurityOpts   []string          `json:"security_opts"`
	DefaultLabels  map[string]string `json:"default_labels"`
	MaxCPUs        float64           `json:"max_cpus"`
	MaxMemory      int64             `json:"max_memory"`
	TimeoutSeconds int               `json:"timeout_seconds"`
}

func NewDockerRuntime(logger *zap.Logger, cfg *Config) (*DockerRuntime, error) {
	if cfg == nil {
		cfg = &Config{
			NetworkMode:    "bridge",
			SecurityOpts:   []string{"no-new-privileges:true"},
			DefaultLabels:  map[string]string{"service": "faas"},
			MaxCPUs:        2.0,
			MaxMemory:      512 * 1024 * 1024, // 512MB
			TimeoutSeconds: 300,
		}
	}

	var cli *client.Client
	var err error

	if cfg.DockerHost != "" {
		cli, err = client.NewClientWithOpts(client.WithHost(cfg.DockerHost))
	} else {
		cli, err = client.NewClientWithOpts(client.FromEnv)
	}

	if err != nil {
		return nil, fmt.Errorf("failed to create Docker client: %w", err)
	}

	return &DockerRuntime{
		client: cli,
		logger: logger,
		config: cfg,
	}, nil
}

func (d *DockerRuntime) Execute(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage) (*domain.ExecutionResult, error) {
	executionID := uuid.New()
	startTime := time.Now()

	d.logger.Info("Starting function execution",
		zap.String("function_id", function.ID.String()),
		zap.String("execution_id", executionID.String()),
		zap.String("image", function.Image))

	// Create container configuration
	containerConfig := &container.Config{
		Image: function.Image,
		Env:   d.buildEnvironment(function, input),
		Labels: map[string]string{
			"faas.function_id":  function.ID.String(),
			"faas.execution_id": executionID.String(),
			"faas.function_name": function.Name,
		},
		WorkingDir: "/app",
		Cmd:        []string{function.Handler},
	}

	// Add default labels
	for k, v := range d.config.DefaultLabels {
		containerConfig.Labels[k] = v
	}

	// Create host configuration with resource limits
	hostConfig := &container.HostConfig{
		Resources: container.Resources{
			Memory:    int64(function.Memory) * 1024 * 1024, // Convert MB to bytes
			CPUQuota:  int64(d.config.MaxCPUs * 100000),     // CPU quota in microseconds
			CPUPeriod: 100000,                               // CPU period in microseconds
		},
		NetworkMode: container.NetworkMode(d.config.NetworkMode),
		SecurityOpt: d.config.SecurityOpts,
		AutoRemove:  true,
	}

	// Create container
	resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, "")
	if err != nil {
		return &domain.ExecutionResult{
			Error:    fmt.Sprintf("failed to create container: %v", err),
			Duration: time.Since(startTime),
		}, nil
	}

	containerID := resp.ID

	// Start container
	if err := d.client.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
		return &domain.ExecutionResult{
			Error:    fmt.Sprintf("failed to start container: %v", err),
			Duration: time.Since(startTime),
		}, nil
	}

	// Wait for container to finish with timeout
	timeoutCtx, cancel := context.WithTimeout(ctx, function.Timeout.Duration)
	defer cancel()

	statusCh, errCh := d.client.ContainerWait(timeoutCtx, containerID, container.WaitConditionNotRunning)
	
	var waitResult container.WaitResponse
	select {
	case result := <-statusCh:
		waitResult = result
	case err := <-errCh:
		d.client.ContainerKill(ctx, containerID, "SIGTERM")
		return &domain.ExecutionResult{
			Error:    fmt.Sprintf("container wait error: %v", err),
			Duration: time.Since(startTime),
		}, nil
	case <-timeoutCtx.Done():
		d.client.ContainerKill(ctx, containerID, "SIGTERM")
		return &domain.ExecutionResult{
			Error:    "execution timeout",
			Duration: time.Since(startTime),
		}, nil
	}

	// Get container logs
	logs, err := d.getContainerLogs(ctx, containerID)
	if err != nil {
		d.logger.Warn("Failed to get container logs", zap.Error(err))
	}

	// Get container stats for memory usage
	memoryUsed := d.getMemoryUsage(ctx, containerID)

	duration := time.Since(startTime)

	// Parse output from logs if successful
	var output json.RawMessage
	var execError string

	if waitResult.StatusCode == 0 {
		// Extract output from logs (assuming last line contains JSON output)
		if len(logs) > 0 {
			lastLog := logs[len(logs)-1]
			if json.Valid([]byte(lastLog)) {
				output = json.RawMessage(lastLog)
			} else {
				output = json.RawMessage(fmt.Sprintf(`{"result": "%s"}`, lastLog))
			}
		}
	} else {
		execError = fmt.Sprintf("container exited with code %d", waitResult.StatusCode)
		if len(logs) > 0 {
			execError += ": " + strings.Join(logs, "\n")
		}
	}

	d.logger.Info("Function execution completed",
		zap.String("function_id", function.ID.String()),
		zap.String("execution_id", executionID.String()),
		zap.Duration("duration", duration),
		zap.Int64("status_code", waitResult.StatusCode),
		zap.Int("memory_used", memoryUsed))

	return &domain.ExecutionResult{
		Output:     output,
		Error:      execError,
		Duration:   duration,
		MemoryUsed: memoryUsed,
		Logs:       logs,
	}, nil
}

func (d *DockerRuntime) Deploy(ctx context.Context, function *domain.FunctionDefinition) error {
	d.logger.Info("Deploying function", 
		zap.String("function_id", function.ID.String()),
		zap.String("image", function.Image))

	// Pull image
	reader, err := d.client.ImagePull(ctx, function.Image, image.PullOptions{})
	if err != nil {
		return fmt.Errorf("failed to pull image %s: %w", function.Image, err)
	}
	defer reader.Close()

	// Read the pull response to ensure it completes
	_, err = io.ReadAll(reader)
	if err != nil {
		return fmt.Errorf("failed to complete image pull: %w", err)
	}

	d.logger.Info("Function deployed successfully", 
		zap.String("function_id", function.ID.String()),
		zap.String("image", function.Image))

	return nil
}

func (d *DockerRuntime) Remove(ctx context.Context, functionID uuid.UUID) error {
	d.logger.Info("Removing function containers", zap.String("function_id", functionID.String()))

	// List containers with the function label
	filters := filters.NewArgs()
	filters.Add("label", fmt.Sprintf("faas.function_id=%s", functionID.String()))
	containers, err := d.client.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filters,
	})
	if err != nil {
		return fmt.Errorf("failed to list containers: %w", err)
	}

	// Remove containers
	for _, container := range containers {
		if err := d.client.ContainerRemove(ctx, container.ID, struct {
			Force bool
		}{Force: true}); err != nil {
			d.logger.Warn("Failed to remove container", 
				zap.String("container_id", container.ID),
				zap.Error(err))
		}
	}

	return nil
}

func (d *DockerRuntime) GetLogs(ctx context.Context, executionID uuid.UUID) ([]string, error) {
	// Find container by execution ID
	filters := filters.NewArgs()
	filters.Add("label", fmt.Sprintf("faas.execution_id=%s", executionID.String()))
	containers, err := d.client.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filters,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to list containers: %w", err)
	}

	if len(containers) == 0 {
		return nil, fmt.Errorf("no container found for execution %s", executionID.String())
	}

	return d.getContainerLogs(ctx, containers[0].ID)
}

func (d *DockerRuntime) HealthCheck(ctx context.Context) error {
	_, err := d.client.Ping(ctx)
	if err != nil {
		return fmt.Errorf("Docker daemon not accessible: %w", err)
	}
	return nil
}

func (d *DockerRuntime) GetInfo(ctx context.Context) (*runtime.RuntimeInfo, error) {
	info, err := d.client.Info(ctx)
	if err != nil {
		return &runtime.RuntimeInfo{
			Type:      "docker",
			Available: false,
		}, nil
	}

	return &runtime.RuntimeInfo{
		Type:      "docker",
		Version:   info.ServerVersion,
		Available: true,
		Endpoint:  d.client.DaemonHost(),
		Metadata: map[string]string{
			"containers": fmt.Sprintf("%d", info.Containers),
			"images":     fmt.Sprintf("%d", info.Images),
		},
	}, nil
}

func (d *DockerRuntime) ListContainers(ctx context.Context) ([]runtime.ContainerInfo, error) {
	filters := filters.NewArgs()
	filters.Add("label", "service=faas")
	containers, err := d.client.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filters,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to list containers: %w", err)
	}

	var result []runtime.ContainerInfo
	for _, container := range containers {
		functionIDStr, exists := container.Labels["faas.function_id"]
		if !exists {
			continue
		}

		functionID, err := uuid.Parse(functionIDStr)
		if err != nil {
			continue
		}

		result = append(result, runtime.ContainerInfo{
			ID:         container.ID,
			FunctionID: functionID,
			Status:     container.Status,
			Image:      container.Image,
			CreatedAt:  time.Unix(container.Created, 0).Format(time.RFC3339),
			Labels:     container.Labels,
		})
	}

	return result, nil
}

func (d *DockerRuntime) StopExecution(ctx context.Context, executionID uuid.UUID) error {
	// Find container by execution ID
	filters := filters.NewArgs()
	filters.Add("label", fmt.Sprintf("faas.execution_id=%s", executionID.String()))
	containers, err := d.client.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filters,
	})
	if err != nil {
		return fmt.Errorf("failed to list containers: %w", err)
	}

	if len(containers) == 0 {
		return fmt.Errorf("no container found for execution %s", executionID.String())
	}

	// Stop container
	timeout := 10
	return d.client.ContainerStop(ctx, containers[0].ID, container.StopOptions{Timeout: &timeout})
}

func (d *DockerRuntime) buildEnvironment(function *domain.FunctionDefinition, input json.RawMessage) []string {
	env := []string{
		fmt.Sprintf("FAAS_FUNCTION_ID=%s", function.ID.String()),
		fmt.Sprintf("FAAS_FUNCTION_NAME=%s", function.Name),
		fmt.Sprintf("FAAS_RUNTIME=%s", function.Runtime),
		fmt.Sprintf("FAAS_HANDLER=%s", function.Handler),
		fmt.Sprintf("FAAS_MEMORY=%d", function.Memory),
		fmt.Sprintf("FAAS_TIMEOUT=%s", function.Timeout.String()),
	}

	// Add function-specific environment variables
	for key, value := range function.Environment {
		env = append(env, fmt.Sprintf("%s=%s", key, value))
	}

	// Add input as environment variable if provided
	if input != nil {
		env = append(env, fmt.Sprintf("FAAS_INPUT=%s", string(input)))
	}

	return env
}

func (d *DockerRuntime) getContainerLogs(ctx context.Context, containerID string) ([]string, error) {
	options := container.LogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		Timestamps: false,
	}

	reader, err := d.client.ContainerLogs(ctx, containerID, options)
	if err != nil {
		return nil, fmt.Errorf("failed to get container logs: %w", err)
	}
	defer reader.Close()

	logs, err := io.ReadAll(reader)
	if err != nil {
		return nil, fmt.Errorf("failed to read container logs: %w", err)
	}

	// Split logs into lines and remove empty lines
	lines := strings.Split(string(logs), "\n")
	var result []string
	for _, line := range lines {
		if strings.TrimSpace(line) != "" {
			result = append(result, line)
		}
	}

	return result, nil
}

func (d *DockerRuntime) getMemoryUsage(ctx context.Context, containerID string) int {
	stats, err := d.client.ContainerStats(ctx, containerID, false)
	if err != nil {
		d.logger.Warn("Failed to get container stats", zap.Error(err))
		return 0
	}
	defer stats.Body.Close()

	var containerStats struct {
		MemoryStats struct {
			Usage uint64 `json:"usage"`
		} `json:"memory_stats"`
	}
	if err := json.NewDecoder(stats.Body).Decode(&containerStats); err != nil {
		d.logger.Warn("Failed to decode container stats", zap.Error(err))
		return 0
	}

	// Return memory usage in MB
	return int(containerStats.MemoryStats.Usage / 1024 / 1024)
}