package services

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

	"go.uber.org/zap"

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

type executionService struct {
	executionRepo  repository.ExecutionRepository
	functionRepo   repository.FunctionRepository
	runtimeService RuntimeService
	logger         *zap.Logger
}

func NewExecutionService(
	executionRepo repository.ExecutionRepository,
	functionRepo repository.FunctionRepository,
	runtimeService RuntimeService,
	logger *zap.Logger,
) ExecutionService {
	return &executionService{
		executionRepo:  executionRepo,
		functionRepo:   functionRepo,
		runtimeService: runtimeService,
		logger:         logger,
	}
}

func (s *executionService) Execute(ctx context.Context, req *domain.ExecuteFunctionRequest, userID string) (*domain.ExecuteFunctionResponse, error) {
	// Get function definition
	function, err := s.functionRepo.GetByID(ctx, req.FunctionID)
	if err != nil {
		return nil, fmt.Errorf("function not found: %w", err)
	}

	// Create execution record
	// Initialize input with empty JSON if nil or empty
	input := req.Input
	if input == nil || len(input) == 0 {
		input = json.RawMessage(`{}`)
	}

	execution := &domain.FunctionExecution{
		ID:         uuid.New(),
		FunctionID: req.FunctionID,
		Status:     domain.StatusPending,
		Input:      input,
		Output:     json.RawMessage(`{}`), // Initialize with empty JSON object
		ExecutorID: userID,
		CreatedAt:  time.Now(),
	}

	// Store execution
	createdExecution, err := s.executionRepo.Create(ctx, execution)
	if err != nil {
		s.logger.Error("Failed to create execution record",
			zap.String("function_id", req.FunctionID.String()),
			zap.Error(err))
		return nil, fmt.Errorf("failed to create execution record: %w", err)
	}

	if req.Async {
		// Start async execution
		go s.executeAsync(context.Background(), createdExecution, function)

		return &domain.ExecuteFunctionResponse{
			ExecutionID: createdExecution.ID,
			Status:      domain.StatusPending,
		}, nil
	} else {
		// Execute synchronously
		return s.executeSync(ctx, createdExecution, function)
	}
}

func (s *executionService) executeSync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) (*domain.ExecuteFunctionResponse, error) {
	// Update status to running
	execution.Status = domain.StatusRunning
	execution.StartedAt = &[]time.Time{time.Now()}[0]

	if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
		s.logger.Warn("Failed to update execution status to running", zap.Error(err))
	}

	// Get runtime backend
	backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
	if err != nil {
		execution.Status = domain.StatusFailed
		execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err)
		s.updateExecutionComplete(ctx, execution)
		return &domain.ExecuteFunctionResponse{
			ExecutionID: execution.ID,
			Status:      domain.StatusFailed,
			Error:       execution.Error,
		}, nil
	}

	// Create timeout context for execution
	execCtx := ctx
	var cancel context.CancelFunc
	if function.Timeout.Duration > 0 {
		execCtx, cancel = context.WithTimeout(ctx, function.Timeout.Duration)
		defer cancel()
	}

	// Define log streaming callback
	logCallback := func(logs []string) error {
		s.logger.Info("Log streaming callback called",
			zap.String("execution_id", execution.ID.String()),
			zap.Int("log_count", len(logs)),
			zap.Strings("logs_preview", logs))
		
		// Update execution with current logs using background context
		// to ensure updates continue even after HTTP request completes
		// Create a copy of the execution to avoid race conditions
		execCopy := *execution
		execCopy.Logs = logs
		_, err := s.executionRepo.Update(context.Background(), execution.ID, &execCopy)
		if err == nil {
			// Only update the original if database update succeeds
			execution.Logs = logs
			s.logger.Info("Successfully updated execution with logs in database",
				zap.String("execution_id", execution.ID.String()),
				zap.Int("log_count", len(logs)))
		} else {
			s.logger.Error("Failed to update execution with logs in database",
				zap.String("execution_id", execution.ID.String()),
				zap.Error(err))
		}
		return err
	}

	// Check if backend supports log streaming
	type logStreamingBackend interface {
		ExecuteWithLogStreaming(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage, logCallback runtime.LogStreamCallback) (*domain.ExecutionResult, error)
	}

	var result *domain.ExecutionResult
	if lsBackend, ok := backend.(logStreamingBackend); ok {
		s.logger.Info("Backend supports log streaming, using ExecuteWithLogStreaming",
			zap.String("execution_id", execution.ID.String()),
			zap.String("function_id", function.ID.String()))
		// Execute function with log streaming
		result, err = lsBackend.ExecuteWithLogStreaming(execCtx, function, execution.Input, logCallback)
	} else {
		s.logger.Info("Backend does not support log streaming, using regular Execute",
			zap.String("execution_id", execution.ID.String()),
			zap.String("function_id", function.ID.String()))
		// Fallback to regular execute
		result, err = backend.Execute(execCtx, function, execution.Input)
	}

	if err != nil {
		// Check if this was a timeout error
		if execCtx.Err() == context.DeadlineExceeded {
			execution.Status = domain.StatusTimeout
			execution.Error = fmt.Sprintf("function execution timed out after %v", function.Timeout.Duration)
		} else {
			execution.Status = domain.StatusFailed
			execution.Error = fmt.Sprintf("execution failed: %v", err)
		}
		s.updateExecutionComplete(ctx, execution)
		return &domain.ExecuteFunctionResponse{
			ExecutionID: execution.ID,
			Status:      execution.Status,
			Error:       execution.Error,
		}, nil
	}

	// Update execution with results
	execution.Status = domain.StatusCompleted
	// Handle empty output
	if len(result.Output) == 0 {
		execution.Output = json.RawMessage(`{}`)
	} else {
		execution.Output = result.Output
	}
	execution.Error = result.Error
	execution.Duration = result.Duration
	execution.MemoryUsed = result.MemoryUsed
	execution.Logs = result.Logs

	// Check if the result indicates a timeout
	if result.Error != "" {
		if strings.Contains(result.Error, "timed out") {
			execution.Status = domain.StatusTimeout
		} else {
			execution.Status = domain.StatusFailed
		}
	}

	s.updateExecutionComplete(ctx, execution)

	return &domain.ExecuteFunctionResponse{
		ExecutionID: execution.ID,
		Status:      execution.Status,
		Output:      execution.Output,
		Error:       execution.Error,
		Duration:    execution.Duration,
		MemoryUsed:  execution.MemoryUsed,
	}, nil
}

func (s *executionService) executeAsync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) {
	// Update status to running
	execution.Status = domain.StatusRunning
	execution.StartedAt = &[]time.Time{time.Now()}[0]

	if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
		s.logger.Warn("Failed to update execution status to running", zap.Error(err))
	}

	// Get runtime backend
	backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
	if err != nil {
		s.logger.Error("Failed to get runtime backend for async execution",
			zap.String("execution_id", execution.ID.String()),
			zap.Error(err))
		execution.Status = domain.StatusFailed
		execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err)
		s.updateExecutionComplete(ctx, execution)
		return
	}

	// Create timeout context for execution
	execCtx := ctx
	var cancel context.CancelFunc
	if function.Timeout.Duration > 0 {
		execCtx, cancel = context.WithTimeout(ctx, function.Timeout.Duration)
		defer cancel()
	}

	// Define log streaming callback
	logCallback := func(logs []string) error {
		s.logger.Info("Log streaming callback called",
			zap.String("execution_id", execution.ID.String()),
			zap.Int("log_count", len(logs)),
			zap.Strings("logs_preview", logs))
		
		// Update execution with current logs using background context
		// to ensure updates continue even after HTTP request completes
		// Create a copy of the execution to avoid race conditions
		execCopy := *execution
		execCopy.Logs = logs
		_, err := s.executionRepo.Update(context.Background(), execution.ID, &execCopy)
		if err == nil {
			// Only update the original if database update succeeds
			execution.Logs = logs
			s.logger.Info("Successfully updated execution with logs in database",
				zap.String("execution_id", execution.ID.String()),
				zap.Int("log_count", len(logs)))
		} else {
			s.logger.Error("Failed to update execution with logs in database",
				zap.String("execution_id", execution.ID.String()),
				zap.Error(err))
		}
		return err
	}

	// Check if backend supports log streaming
	type logStreamingBackend interface {
		ExecuteWithLogStreaming(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage, logCallback runtime.LogStreamCallback) (*domain.ExecutionResult, error)
	}

	var result *domain.ExecutionResult
	if lsBackend, ok := backend.(logStreamingBackend); ok {
		s.logger.Info("Backend supports log streaming, using ExecuteWithLogStreaming",
			zap.String("execution_id", execution.ID.String()),
			zap.String("function_id", function.ID.String()))
		// Execute function with log streaming
		result, err = lsBackend.ExecuteWithLogStreaming(execCtx, function, execution.Input, logCallback)
	} else {
		s.logger.Info("Backend does not support log streaming, using regular Execute",
			zap.String("execution_id", execution.ID.String()),
			zap.String("function_id", function.ID.String()))
		// Fallback to regular execute
		result, err = backend.Execute(execCtx, function, execution.Input)
	}

	if err != nil {
		// Check if this was a timeout error
		if execCtx.Err() == context.DeadlineExceeded {
			execution.Status = domain.StatusTimeout
			execution.Error = fmt.Sprintf("function execution timed out after %v", function.Timeout.Duration)
		} else {
			execution.Status = domain.StatusFailed
			execution.Error = fmt.Sprintf("execution failed: %v", err)
		}
		s.updateExecutionComplete(ctx, execution)
		return
	}

	// Update execution with results
	execution.Status = domain.StatusCompleted
	// Handle empty output
	if len(result.Output) == 0 {
		execution.Output = json.RawMessage(`{}`)
	} else {
		execution.Output = result.Output
	}
	execution.Error = result.Error
	execution.Duration = result.Duration
	execution.MemoryUsed = result.MemoryUsed
	execution.Logs = result.Logs

	// Check if the result indicates a timeout
	if result.Error != "" {
		if strings.Contains(result.Error, "timed out") {
			execution.Status = domain.StatusTimeout
		} else {
			execution.Status = domain.StatusFailed
		}
	}

	s.updateExecutionComplete(ctx, execution)

	s.logger.Info("Async function execution completed",
		zap.String("execution_id", execution.ID.String()),
		zap.String("status", string(execution.Status)),
		zap.Duration("duration", execution.Duration))
}

func (s *executionService) updateExecutionComplete(ctx context.Context, execution *domain.FunctionExecution) {
	execution.CompletedAt = &[]time.Time{time.Now()}[0]

	if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
		s.logger.Error("Failed to update execution completion",
			zap.String("execution_id", execution.ID.String()),
			zap.Error(err))
	}
}

func (s *executionService) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error) {
	execution, err := s.executionRepo.GetByID(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("execution not found: %w", err)
	}
	return execution, nil
}

func (s *executionService) List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
	if limit <= 0 {
		limit = 50 // Default limit
	}
	if limit > 100 {
		limit = 100 // Max limit
	}

	return s.executionRepo.List(ctx, functionID, limit, offset)
}

func (s *executionService) GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
	if limit <= 0 {
		limit = 50 // Default limit
	}
	if limit > 100 {
		limit = 100 // Max limit
	}

	return s.executionRepo.GetByFunctionID(ctx, functionID, limit, offset)
}

func (s *executionService) Cancel(ctx context.Context, id uuid.UUID, userID string) error {
	s.logger.Info("Canceling execution",
		zap.String("execution_id", id.String()),
		zap.String("user_id", userID))

	// Get execution
	execution, err := s.executionRepo.GetByID(ctx, id)
	if err != nil {
		return fmt.Errorf("execution not found: %w", err)
	}

	// Check if execution is still running
	if execution.Status != domain.StatusRunning && execution.Status != domain.StatusPending {
		return fmt.Errorf("execution is not running (status: %s)", execution.Status)
	}

	// Get function to determine runtime
	function, err := s.functionRepo.GetByID(ctx, execution.FunctionID)
	if err != nil {
		return fmt.Errorf("function not found: %w", err)
	}

	// Stop execution in runtime
	backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
	if err != nil {
		return fmt.Errorf("failed to get runtime backend: %w", err)
	}

	if err := backend.StopExecution(ctx, id); err != nil {
		s.logger.Warn("Failed to stop execution in runtime",
			zap.String("execution_id", id.String()),
			zap.Error(err))
	}

	// Update execution status
	execution.Status = domain.StatusCanceled
	execution.Error = "execution canceled by user"
	execution.CompletedAt = &[]time.Time{time.Now()}[0]

	if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
		return fmt.Errorf("failed to update execution status: %w", err)
	}

	s.logger.Info("Execution canceled successfully",
		zap.String("execution_id", id.String()))

	return nil
}

func (s *executionService) GetLogs(ctx context.Context, id uuid.UUID) ([]string, error) {
	s.logger.Debug("GetLogs called in execution service",
		zap.String("execution_id", id.String()))
		
	// Get execution with logs from database
	execution, err := s.executionRepo.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get execution from database in GetLogs",
			zap.String("execution_id", id.String()),
			zap.Error(err))
		return nil, fmt.Errorf("execution not found: %w", err)
	}

	s.logger.Info("Retrieved execution from database",
		zap.String("execution_id", id.String()),
		zap.String("status", string(execution.Status)),
		zap.Int("log_count", len(execution.Logs)),
		zap.Bool("logs_nil", execution.Logs == nil))

	// Return logs from execution record
	if execution.Logs == nil {
		s.logger.Debug("Execution has nil logs, returning empty slice",
			zap.String("execution_id", id.String()))
		return []string{}, nil
	}

	s.logger.Debug("Returning logs from execution",
		zap.String("execution_id", id.String()),
		zap.Int("log_count", len(execution.Logs)))

	return execution.Logs, nil
}

func (s *executionService) GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error) {
	return s.executionRepo.GetRunningExecutions(ctx)
}
