package services

import (
	"archive/tar"
	"bytes"
	"context"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"git.linuxforward.com/byop/byop-engine/analyzer"
	"git.linuxforward.com/byop/byop-engine/dbstore"
	"git.linuxforward.com/byop/byop-engine/models"
	"github.com/sirupsen/logrus"
)

// PreviewService defines the interface for preview services
type PreviewService interface {
	CreatePreview(ctx context.Context, appId uint) (*models.Preview, error)
	DeletePreview(ctx context.Context, appID uint) error
	StopPreview(ctx context.Context, previewID uint) error
	Close(ctx context.Context)
}

// PreviewCommon contains shared functionality for preview services
type PreviewCommon struct {
	store        *dbstore.SQLiteStore
	entry        *logrus.Entry
	registryURL  string
	registryUser string
	registryPass string
}

// NewPreviewCommon creates a new PreviewCommon instance
func NewPreviewCommon(store *dbstore.SQLiteStore, registryURL, registryUser, registryPass string) *PreviewCommon {
	return &PreviewCommon{
		store:        store,
		entry:        logrus.WithField("service", "PreviewCommon"),
		registryURL:  registryURL,
		registryUser: registryUser,
		registryPass: registryPass,
	}
}

// Close cleans up resources
func (pc *PreviewCommon) Close() {
	// Clean up preview database state
	pc.CleanupPreviewState(context.Background())
}

// GetStore returns the database store
func (pc *PreviewCommon) GetStore() *dbstore.SQLiteStore {
	return pc.store
}

// GetLogger returns the logger
func (pc *PreviewCommon) GetLogger() *logrus.Entry {
	return pc.entry
}

// GeneratePreviewID generates an 8-character random hex UUID for preview URLs
func (pc *PreviewCommon) GeneratePreviewID() string {
	bytes := make([]byte, 4) // 4 bytes = 8 hex chars
	if _, err := rand.Read(bytes); err != nil {
		// Fallback to timestamp-based ID if crypto/rand fails
		return fmt.Sprintf("%08x", time.Now().Unix()%0xFFFFFFFF)
	}
	// Convert each byte directly to hex to ensure we get truly random looking IDs
	return fmt.Sprintf("%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3])
}

// CloneRepository clones a git repository to a target directory
func (pc *PreviewCommon) CloneRepository(ctx context.Context, repoURL, branch, targetDir string) error {
	if err := os.MkdirAll(targetDir, 0755); err != nil {
		return models.NewErrInternalServer(fmt.Sprintf("failed to create target directory %s", targetDir), err)
	}

	if branch == "" {
		branch = "main"
	}

	cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", branch, repoURL, targetDir)
	if err := cmd.Run(); err != nil {
		// Try with master branch if main fails
		if branch == "main" {
			cmd = exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", "master", repoURL, targetDir)
			if err := cmd.Run(); err != nil {
				return models.NewErrInternalServer(fmt.Sprintf("failed to clone repository (tried main and master branches): %s", repoURL), err)
			}
		} else {
			return models.NewErrInternalServer(fmt.Sprintf("failed to clone repository %s on branch %s", repoURL, branch), err)
		}
	}

	return nil
}

// CreateBuildContext creates a tar archive of the build context
func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir string, component *models.Component) (io.ReadCloser, error) {
	var buf bytes.Buffer
	tw := tar.NewWriter(&buf)
	defer tw.Close()

	// For docker-compose components, adjust the context directory to use the build context
	effectiveContextDir := contextDir
	relativeDockerfilePath := "Dockerfile"

	if component != nil && component.SourceType == "docker-compose" && component.BuildContext != "" {
		// Resolve the build context directory
		effectiveContextDir = filepath.Join(contextDir, component.BuildContext)

		// Set Dockerfile path relative to the build context
		if component.DockerfilePath != "" {
			relativeDockerfilePath = component.DockerfilePath
		}

		pc.entry.WithFields(logrus.Fields{
			"component_id":         component.ID,
			"build_context":        component.BuildContext,
			"dockerfile_path":      relativeDockerfilePath,
			"resolved_context_dir": effectiveContextDir,
		}).Info("Using docker-compose build context for component")
	}

	// Common ignore patterns for Git repositories
	ignorePatterns := []string{
		".git",
		".gitignore",
		"node_modules",
		".next",
		"dist",
		"build",
		"target",
		"__pycache__",
		"*.pyc",
		".DS_Store",
		"Thumbs.db",
		"*.log",
		"*.tmp",
		"*.swp",
		".env",
		".vscode",
		".idea",
		"playwright",
		"cypress",
		"coverage",
		"*.test.js",
		"*.spec.js",
		"*.test.ts",
		"*.spec.ts",
		"test",
		"tests",
		"__tests__",
		"snapshots",
		"*.png",
		"*.jpg",
		"*.jpeg",
		"*.gif",
		"*.bmp",
		"*.svg",
		"*.ico",
		"*.zip",
		"*.tar.gz",
		"*.tar",
		"*.gz",
		"README.md",
		"readme.md",
		"CHANGELOG.md",
		"LICENSE",
		"CONTRIBUTING.md",
		"*.md",
		"docs",
		"documentation",
	}

	err := filepath.Walk(effectiveContextDir, func(file string, fi os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Get relative path
		relPath, err := filepath.Rel(effectiveContextDir, file)
		if err != nil {
			return err
		}

		// Skip if matches ignore patterns
		for _, pattern := range ignorePatterns {
			if matched, _ := filepath.Match(pattern, fi.Name()); matched {
				if fi.IsDir() {
					return filepath.SkipDir
				}
				return nil
			}
			if strings.Contains(relPath, pattern) {
				if fi.IsDir() {
					return filepath.SkipDir
				}
				return nil
			}
		}

		// Skip very large files (> 100MB)
		if !fi.IsDir() && fi.Size() > 100*1024*1024 {
			pc.entry.WithField("file", relPath).WithField("size", fi.Size()).Warn("Skipping large file in build context")
			return nil
		}

		// Skip files with very long paths (> 200 chars)
		if len(relPath) > 200 {
			pc.entry.WithField("file", relPath).WithField("length", len(relPath)).Warn("Skipping file with very long path")
			return nil
		}

		// Create tar header
		header, err := tar.FileInfoHeader(fi, fi.Name())
		if err != nil {
			return err
		}

		// Update the name to be relative to the context directory
		header.Name = filepath.ToSlash(relPath)

		// Ensure header name is not too long for tar format
		if len(header.Name) > 155 {
			pc.entry.WithField("file", header.Name).WithField("length", len(header.Name)).Warn("Skipping file with tar-incompatible long name")
			return nil
		}

		// Write header
		if err := tw.WriteHeader(header); err != nil {
			return fmt.Errorf("failed to write tar header for %s: %v", relPath, err)
		}

		// If it's a file, write its content
		if !fi.IsDir() {
			data, err := os.Open(file)
			if err != nil {
				return fmt.Errorf("failed to open file %s: %v", relPath, err)
			}
			defer data.Close()

			// Use limited reader to prevent issues with very large files
			limitedReader := io.LimitReader(data, 100*1024*1024) // 100MB limit
			written, err := io.Copy(tw, limitedReader)
			if err != nil {
				pc.entry.WithField("file", relPath).WithField("written_bytes", written).Warnf("Failed to copy file to tar, skipping: %v", err)
				// Don't return error, just skip this file
				return nil
			}
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return io.NopCloser(&buf), nil
}

// fileExists checks if a file exists
func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

// BuildComponentImages builds Docker images for components using shell commands
// This simplified version uses docker build commands directly instead of Docker API
func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []models.Component) ([]string, string, error) {
	var imageNames []string
	var allLogs strings.Builder

	for _, component := range components {
		pc.entry.WithFields(logrus.Fields{
			"component_id":    component.ID,
			"status":          component.Status,
			"source_type":     component.SourceType,
			"build_context":   component.BuildContext,
			"dockerfile_path": component.DockerfilePath,
			"service_name":    component.ServiceName,
		}).Info("Processing component for preview")

		allLogs.WriteString(fmt.Sprintf("Processing component %d (%s) - SourceType: %s, BuildContext: %s, DockerfilePath: %s\n",
			component.ID, component.Name, component.SourceType, component.BuildContext, component.DockerfilePath))

		// Generate local image name for preview
		imageName := fmt.Sprintf("byop-preview-%s:%d", component.Name, component.ID)

		// Check if the local preview image already exists using shell command
		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Checking if local preview image exists")
		checkCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
		if err := checkCmd.Run(); err == nil {
			pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Local preview image already exists, skipping build")
			allLogs.WriteString(fmt.Sprintf("Component %d already has local preview image %s, skipping build\n", component.ID, imageName))
			imageNames = append(imageNames, imageName)
			continue // Skip to next component
		} else {
			pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).WithError(err).Info("Local preview image not found, will build from source")
		}

		// For docker-compose components with public images, use them directly
		if component.SourceType == "docker-compose" && component.CurrentImageURI != "" &&
			strings.Contains(component.CurrentImageURI, ":") &&
			!strings.Contains(component.CurrentImageURI, "host.docker.internal") &&
			!strings.Contains(component.CurrentImageURI, pc.registryURL) {
			// This is likely a public image (mysql:5.7, postgres:13, etc.)
			pc.entry.WithField("component_id", component.ID).WithField("image_uri", component.CurrentImageURI).Info("Using public image directly for preview")
			allLogs.WriteString(fmt.Sprintf("Component %d using public image %s directly\n", component.ID, component.CurrentImageURI))
			imageNames = append(imageNames, component.CurrentImageURI)
			continue // Skip to next component
		}

		// Build from source code
		pc.entry.WithField("component_id", component.ID).Info("Building Docker image from source")
		allLogs.WriteString(fmt.Sprintf("Building component %d from source\n", component.ID))

		// Create temp directory for this component
		tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("byop-preview-%d-%d", component.ID, time.Now().Unix()))
		defer os.RemoveAll(tempDir)

		// Clone repository
		if err := pc.CloneRepository(ctx, component.Repository, component.Branch, tempDir); err != nil {
			allLogs.WriteString(fmt.Sprintf("Failed to clone %s: %v\n", component.Repository, err))
			return nil, allLogs.String(), err
		}

		// Determine build context and dockerfile path
		buildContext := tempDir
		dockerfilePath := "Dockerfile"

		if component.SourceType == "docker-compose" && component.BuildContext != "" {
			buildContext = filepath.Join(tempDir, component.BuildContext)
			if component.DockerfilePath != "" {
				dockerfilePath = component.DockerfilePath
			}
		}

		// Check if we need to generate a Dockerfile
		fullDockerfilePath := filepath.Join(buildContext, dockerfilePath)
		if component.Status != "valid" || !fileExists(fullDockerfilePath) {
			pc.entry.WithField("component_id", component.ID).Info("Generating Dockerfile for component")
			allLogs.WriteString(fmt.Sprintf("Generating Dockerfile for component %d\n", component.ID))

			// Use analyzer to generate Dockerfile
			stack, err := analyzer.AnalyzeCode(tempDir)
			if err != nil {
				allLogs.WriteString(fmt.Sprintf("Failed to analyze code for %s: %v\n", component.Name, err))
				return nil, allLogs.String(), err
			}

			dockerfileContent, err := stack.GenerateDockerfile(tempDir)
			if err != nil {
				allLogs.WriteString(fmt.Sprintf("Failed to generate Dockerfile for %s: %v\n", component.Name, err))
				return nil, allLogs.String(), err
			}

			// Write Dockerfile (no Traefik labels here - they'll be added at runtime)
			if err := os.WriteFile(fullDockerfilePath, []byte(dockerfileContent), 0644); err != nil {
				allLogs.WriteString(fmt.Sprintf("Failed to write Dockerfile for %s: %v\n", component.Name, err))
				return nil, allLogs.String(), err
			}
		}

		// Build the image using docker build command
		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Building Docker image")
		allLogs.WriteString(fmt.Sprintf("Building Docker image for component %d: %s\n", component.ID, imageName))

		buildCmd := exec.CommandContext(ctx, "docker", "build", "-t", imageName, "-f", dockerfilePath, buildContext)
		output, err := buildCmd.CombinedOutput()
		buildOutputStr := string(output)
		allLogs.WriteString(fmt.Sprintf("Build output for %s:\n%s\n", imageName, buildOutputStr))

		if err != nil {
			allLogs.WriteString(fmt.Sprintf("Failed to build %s: %v\n", imageName, err))
			return nil, allLogs.String(), fmt.Errorf("failed to build image %s: %v", imageName, err)
		}

		// Verify the image was built successfully
		verifyCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
		if err := verifyCmd.Run(); err != nil {
			allLogs.WriteString(fmt.Sprintf("Build verification failed for %s: image not found after build - %v\n", imageName, err))
			return nil, allLogs.String(), fmt.Errorf("failed to build image %s: image not found after build", imageName)
		}

		imageNames = append(imageNames, imageName)
		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Successfully built Docker image")
	}

	return imageNames, allLogs.String(), nil
}

// GetAppComponents retrieves components for an app
func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App) ([]models.Component, error) {
	var components []models.Component

	// Parse the JSON string of component IDs
	var componentIDs []uint
	if app.Components != "" {
		if err := json.Unmarshal([]byte(app.Components), &componentIDs); err != nil {
			return nil, fmt.Errorf("failed to parse component IDs from app %d: %v", app.ID, err)
		}
	}

	for _, componentID := range componentIDs {
		component, err := pc.store.GetComponentByID(ctx, componentID)
		if err != nil {
			return nil, err
		}
		if component == nil {
			return nil, models.NewErrNotFound(fmt.Sprintf("Component with ID %d not found while fetching app components", componentID), nil)
		}
		components = append(components, *component)
	}

	return components, nil
}

// CleanupPreviewImages cleans up BYOP preview Docker images using shell commands
func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
	pc.entry.Info("Cleaning up BYOP preview images...")

	// List all images and filter for byop-preview
	cmd := exec.CommandContext(ctx, "docker", "images", "--format", "{{.Repository}}:{{.Tag}}")
	output, err := cmd.Output()
	if err != nil {
		pc.entry.WithError(err).Error("Failed to list images for cleanup")
		return
	}

	removedCount := 0
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		if strings.Contains(line, "byop-preview") {
			// Remove the image
			rmCmd := exec.CommandContext(ctx, "docker", "rmi", line, "--force")
			if err := rmCmd.Run(); err != nil {
				pc.entry.WithError(err).WithField("image", line).Warn("Failed to remove preview image")
			} else {
				removedCount++
			}
		}
	}

	if removedCount > 0 {
		pc.entry.WithField("removed_images", removedCount).Info("Cleaned up BYOP preview images")
	}
}

// CleanupAllPreviewContainers cleans up all BYOP preview containers using shell commands
func (pc *PreviewCommon) CleanupAllPreviewContainers(ctx context.Context) {
	pc.entry.Info("Cleaning up all BYOP preview containers...")

	// List all containers and filter for byop-preview
	cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
	output, err := cmd.Output()
	if err != nil {
		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
		return
	}

	removedCount := 0
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		if strings.Contains(line, "byop-preview") || strings.Contains(line, "preview") {
			pc.entry.WithField("container_name", line).Info("Removing BYOP preview container")

			// Stop and remove container
			stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
			stopCmd.Run() // Ignore errors for stop

			rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
			if err := rmCmd.Run(); err != nil {
				pc.entry.WithError(err).WithField("container_name", line).Error("Failed to remove container")
			} else {
				removedCount++
			}
		}
	}

	if removedCount > 0 {
		pc.entry.WithField("removed_containers", removedCount).Info("Cleaned up BYOP preview containers")
	}
}

// CleanupPreviewState cleans up preview database state - mark all running previews as stopped
func (pc *PreviewCommon) CleanupPreviewState(ctx context.Context) {
	pc.entry.Info("Cleaning up preview database state...")

	// Get all active previews (building, deploying, running)
	activeStatuses := []string{"building", "deploying", "running"}

	for _, status := range activeStatuses {
		previews, err := pc.store.GetPreviewsByStatus(ctx, status)
		if err != nil {
			pc.entry.WithError(err).WithField("status", status).Error("Failed to get previews by status")
			continue
		}

		for _, preview := range previews {
			pc.entry.WithField("preview_id", preview.ID).WithField("app_id", preview.AppID).WithField("old_status", preview.Status).Info("Marking preview as stopped due to server shutdown")

			// Update preview status to stopped
			if err := pc.store.UpdatePreviewStatus(ctx, preview.ID, "stopped", "Server shutdown - containers may have been stopped"); err != nil {
				pc.entry.WithError(err).WithField("preview_id", preview.ID).Error("Failed to update preview status to stopped")
			}

			// Also update the associated app status back to "ready" if it was in a preview state
			if app, err := pc.store.GetAppByID(ctx, preview.AppID); err == nil && app != nil {
				if app.Status == "building" || app.Status == "deploying" {
					if err := pc.store.UpdateAppStatus(ctx, app.ID, "ready", ""); err != nil {
						pc.entry.WithError(err).WithField("app_id", app.ID).Error("Failed to reset app status to ready")
					} else {
						pc.entry.WithField("app_id", app.ID).Info("Reset app status to ready after preview cleanup")
					}
				}
			}
		}

		if len(previews) > 0 {
			pc.entry.WithField("count", len(previews)).WithField("status", status).Info("Updated preview statuses to stopped")
		}
	}

	pc.entry.Info("Preview database state cleanup completed")
}

// GetPreviewImageNames reconstructs the Docker image names used for a preview
func (pc *PreviewCommon) GetPreviewImageNames(appID uint) ([]string, error) {
	// Get app details
	app, err := pc.store.GetAppByID(context.Background(), appID)
	if err != nil {
		return nil, fmt.Errorf("failed to get app by ID %d: %v", appID, err)
	}

	// Get all components for the app
	components, err := pc.GetAppComponents(context.Background(), app)
	if err != nil {
		return nil, fmt.Errorf("failed to get app components: %v", err)
	}

	// Reconstruct image names using the same format as BuildComponentImages
	var imageNames []string
	for _, component := range components {
		imageName := fmt.Sprintf("byop-preview-%s:%d", component.Name, component.ID)
		imageNames = append(imageNames, imageName)
	}

	return imageNames, nil
}

// CleanupPreviewImagesForApp cleans up Docker images for a specific app using shell commands
func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID uint, isRemote bool, ipAddress string) error {
	imageNames, err := pc.GetPreviewImageNames(appID)
	if err != nil {
		pc.entry.WithField("app_id", appID).WithError(err).Warn("Failed to get preview image names for cleanup")
		return err
	}

	if isRemote && ipAddress != "" && ipAddress != "127.0.0.1" {
		return pc.cleanupRemoteDockerImages(ctx, ipAddress, imageNames)
	} else {
		return pc.cleanupLocalDockerImages(ctx, imageNames)
	}
}

// cleanupLocalDockerImages removes specific Docker images locally using shell commands
func (pc *PreviewCommon) cleanupLocalDockerImages(ctx context.Context, imageNames []string) error {
	pc.entry.WithField("image_count", len(imageNames)).Info("Cleaning up specific Docker images locally")

	for _, imageName := range imageNames {
		// Remove the image locally using docker rmi command
		cmd := exec.CommandContext(ctx, "docker", "rmi", imageName, "--force")
		if err := cmd.Run(); err != nil {
			// Log warning but don't fail the cleanup - image might already be removed or in use
			pc.entry.WithField("image_name", imageName).WithError(err).Warn("Failed to remove Docker image locally (this may be normal)")
		} else {
			pc.entry.WithField("image_name", imageName).Info("Successfully removed Docker image locally")
		}
	}

	return nil
}

// cleanupRemoteDockerImages removes Docker images from a VPS via SSH
func (pc *PreviewCommon) cleanupRemoteDockerImages(ctx context.Context, ipAddress string, imageNames []string) error {
	pc.entry.WithField("ip_address", ipAddress).WithField("image_count", len(imageNames)).Info("Cleaning up Docker images on VPS")

	for _, imageName := range imageNames {
		// Remove the image
		rmImageCmd := fmt.Sprintf("docker rmi %s --force", imageName)
		pc.entry.WithField("image_name", imageName).WithField("ip_address", ipAddress).Info("Removing Docker image")

		if err := pc.executeSSHCommand(ctx, ipAddress, rmImageCmd); err != nil {
			// Log warning but don't fail the cleanup - image might already be removed or in use
			pc.entry.WithField("image_name", imageName).WithField("ip_address", ipAddress).WithError(err).Warn("Failed to remove Docker image (this may be normal)")
		} else {
			pc.entry.WithField("image_name", imageName).WithField("ip_address", ipAddress).Info("Successfully removed Docker image")
		}

		// Also remove the tar file if it exists
		tarFileName := strings.ReplaceAll(imageName, ":", "_")
		rmTarCmd := fmt.Sprintf("rm -f /tmp/%s.tar", tarFileName)
		pc.executeSSHCommand(ctx, ipAddress, rmTarCmd) // Ignore errors for tar cleanup
	}

	// Clean up any dangling images
	pc.entry.WithField("ip_address", ipAddress).Info("Cleaning up dangling Docker images")
	danglingCmd := "docker image prune -f"
	if err := pc.executeSSHCommand(ctx, ipAddress, danglingCmd); err != nil {
		pc.entry.WithField("ip_address", ipAddress).WithError(err).Warn("Failed to clean dangling images")
	}

	return nil
}

// executeSSHCommand executes a command on a remote VPS via SSH
func (pc *PreviewCommon) executeSSHCommand(ctx context.Context, ipAddress, command string) error {
	pc.entry.WithField("ip_address", ipAddress).WithField("command", command).Debug("Executing SSH command")
	cmd := exec.CommandContext(ctx, "ssh", "-o", "StrictHostKeyChecking=no", ipAddress, command)

	output, err := cmd.CombinedOutput()
	if err != nil {
		pc.entry.WithField("ip_address", ipAddress).WithField("command", command).WithField("output", string(output)).WithError(err).Error("SSH command failed")
		return models.NewErrInternalServer(fmt.Sprintf("SSH command failed on %s: %s. Output: %s", ipAddress, command, string(output)), err)
	}

	if len(output) > 0 {
		pc.entry.WithField("ip_address", ipAddress).WithField("command", command).WithField("output", string(output)).Debug("SSH command output")
	}

	return nil
}

// Database helper methods
func (pc *PreviewCommon) UpdatePreviewStatus(ctx context.Context, previewID uint, status, errorMsg string) {
	if err := pc.store.UpdatePreviewStatus(ctx, previewID, status, errorMsg); err != nil {
		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview status: %v", err)
	}
}

func (pc *PreviewCommon) UpdatePreviewBuildLogs(ctx context.Context, previewID uint, logs string) {
	if err := pc.store.UpdatePreviewBuildLogs(ctx, previewID, logs); err != nil {
		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview build logs: %v", err)
	}
}

func (pc *PreviewCommon) UpdatePreviewDeployLogs(ctx context.Context, previewID uint, logs string) {
	if err := pc.store.UpdatePreviewDeployLogs(ctx, previewID, logs); err != nil {
		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview deploy logs: %v", err)
	}
}

// CleanupByAppID cleans up all BYOP preview containers and images for a specific app ID using shell commands
func (pc *PreviewCommon) CleanupByAppID(ctx context.Context, appID uint) {
	pc.entry.WithField("app_id", appID).Info("Cleaning up BYOP preview containers...")

	// List all containers and filter for byop-preview containers with the specific app ID
	// We'll use the app ID in the container naming pattern
	appPattern := fmt.Sprintf("byop-preview-app-%d", appID)

	cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
	output, err := cmd.Output()
	if err != nil {
		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
		return
	}

	removedCount := 0
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// Check if this container is for the specific app
		if strings.Contains(line, appPattern) || (strings.Contains(line, "byop-preview") && strings.Contains(line, fmt.Sprintf("-%d-", appID))) {
			pc.entry.WithField("container_name", line).WithField("app_id", appID).Info("Removing BYOP preview container for app")

			// Stop and remove container
			stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
			stopCmd.Run() // Ignore errors for stop

			rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
			if err := rmCmd.Run(); err != nil {
				pc.entry.WithError(err).WithField("container_name", line).WithField("app_id", appID).Error("Failed to remove container")
			} else {
				removedCount++
			}
		}
	}

	if removedCount > 0 {
		pc.entry.WithField("removed_containers", removedCount).WithField("app_id", appID).Info("Cleaned up BYOP preview containers for app")
	}
}

// AddTraefikLabelsToDockerfile adds Traefik routing labels to a Dockerfile
func (pc *PreviewCommon) AddTraefikLabelsToDockerfile(dockerfileContent, appName string, appID uint, port string) string {
	if port == "" {
		port = "3000" // Default port
	}

	// Generate the preview subdomain
	previewID := pc.GeneratePreviewID()
	subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)

	// Traefik labels to add
	traefikLabels := []string{
		`LABEL traefik.enable="true"`,
		fmt.Sprintf(`LABEL traefik.http.routers.%s.rule="Host(%s.preview.byop.dev)"`, subdomain, "`"+subdomain+"`"),
		fmt.Sprintf(`LABEL traefik.http.routers.%s.entrypoints="websecure"`, subdomain),
		fmt.Sprintf(`LABEL traefik.http.routers.%s.tls.certresolver="letsencrypt"`, subdomain),
		fmt.Sprintf(`LABEL traefik.http.services.%s.loadbalancer.server.port="%s"`, subdomain, port),
		`LABEL traefik.docker.network="traefik"`,
		`LABEL byop.preview="true"`,
		fmt.Sprintf(`LABEL byop.app.id="%d"`, appID),
		fmt.Sprintf(`LABEL byop.app.name="%s"`, appName),
		fmt.Sprintf(`LABEL byop.preview.id="%s"`, previewID),
	}

	// Parse the Dockerfile content
	lines := strings.Split(dockerfileContent, "\n")
	var modifiedLines []string

	// Find the last non-empty, non-comment line to insert labels before any final CMD/ENTRYPOINT
	lastInstructionIndex := -1
	for i := len(lines) - 1; i >= 0; i-- {
		line := strings.TrimSpace(lines[i])
		if line != "" && !strings.HasPrefix(line, "#") {
			if strings.HasPrefix(strings.ToUpper(line), "CMD") ||
				strings.HasPrefix(strings.ToUpper(line), "ENTRYPOINT") {
				lastInstructionIndex = i
				break
			}
		}
	}

	// If we found a CMD/ENTRYPOINT, insert labels before it
	if lastInstructionIndex != -1 {
		modifiedLines = append(modifiedLines, lines[:lastInstructionIndex]...)
		modifiedLines = append(modifiedLines, "")
		modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
		modifiedLines = append(modifiedLines, traefikLabels...)
		modifiedLines = append(modifiedLines, "")
		modifiedLines = append(modifiedLines, lines[lastInstructionIndex:]...)
	} else {
		// No CMD/ENTRYPOINT found, just append labels at the end
		modifiedLines = append(modifiedLines, lines...)
		modifiedLines = append(modifiedLines, "")
		modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
		modifiedLines = append(modifiedLines, traefikLabels...)
	}

	return strings.Join(modifiedLines, "\n")
}

// DetectPortFromDockerfile attempts to detect the exposed port from a Dockerfile
func (pc *PreviewCommon) DetectPortFromDockerfile(dockerfileContent string) string {
	lines := strings.Split(dockerfileContent, "\n")

	for _, line := range lines {
		line = strings.TrimSpace(strings.ToUpper(line))
		if strings.HasPrefix(line, "EXPOSE ") {
			// Extract port number
			parts := strings.Fields(line)
			if len(parts) >= 2 {
				port := strings.Split(parts[1], "/")[0] // Handle cases like "3000/tcp"
				return port
			}
		}
	}

	// Common default ports based on technology detection
	content := strings.ToLower(dockerfileContent)
	if strings.Contains(content, "node") || strings.Contains(content, "npm") {
		return "3000"
	} else if strings.Contains(content, "python") || strings.Contains(content, "flask") {
		return "5000"
	} else if strings.Contains(content, "django") {
		return "8000"
	} else if strings.Contains(content, "nginx") {
		return "80"
	}

	return "3000" // Default fallback
}

// GenerateTraefikComposeOverride creates a docker-compose override for Traefik routing
// This is used during preview deployment to add routing labels to pre-built images
func (pc *PreviewCommon) GenerateTraefikComposeOverride(serviceName, appName string, appID uint, port string, previewID string) string {
	if port == "" {
		port = "3000"
	}

	if previewID == "" {
		previewID = pc.GeneratePreviewID()
	}

	subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)

	override := fmt.Sprintf(`version: '3.8'
services:
  %s:
    labels:
      - traefik.enable=true
      - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
      - traefik.http.routers.%s.entrypoints=websecure
      - traefik.http.routers.%s.tls.certresolver=letsencrypt
      - traefik.http.services.%s.loadbalancer.server.port=%s
      - traefik.docker.network=traefik
      - byop.preview=true
      - byop.app.id=%d
      - byop.app.name=%s
      - byop.preview.id=%s
    networks:
      - traefik
      - default

networks:
  traefik:
    external: true
`, serviceName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, appID, appName, previewID)

	return override
}

// GeneratePreviewComposeFile creates a complete docker-compose file for preview deployment
// This uses pre-built component images and adds Traefik routing
func (pc *PreviewCommon) GeneratePreviewComposeFile(app *models.App, components []models.Component, imageNames []string, previewID string) (string, error) {
	if len(components) != len(imageNames) {
		return "", fmt.Errorf("component count (%d) doesn't match image count (%d)", len(components), len(imageNames))
	}

	var services []string

	for i, component := range components {
		imageName := imageNames[i]
		serviceName := component.ServiceName
		if serviceName == "" {
			serviceName = component.Name
		}

		// Detect port from component or use default
		port := "3000" // Default port

		// Try to detect port from the built image if available
		// For now, we'll use a default since we don't store port info in Component model

		// Generate subdomain for this component
		subdomain := fmt.Sprintf("%s-%d-%s", app.Name, app.ID, previewID)
		if len(components) > 1 {
			subdomain = fmt.Sprintf("%s-%s-%d-%s", app.Name, component.Name, app.ID, previewID)
		}

		serviceConfig := fmt.Sprintf(`  %s:
    image: %s
    labels:
      - traefik.enable=true
      - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
      - traefik.http.routers.%s.entrypoints=websecure
      - traefik.http.routers.%s.tls.certresolver=letsencrypt
      - traefik.http.services.%s.loadbalancer.server.port=%s
      - traefik.docker.network=traefik
      - byop.preview=true
      - byop.app.id=%d
      - byop.app.name=%s
      - byop.preview.id=%s
      - byop.component.id=%d
      - byop.component.name=%s
    networks:
      - traefik
      - default
    restart: unless-stopped`,
			serviceName, imageName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, app.ID, app.Name, previewID, component.ID, component.Name)

		services = append(services, serviceConfig)
	}

	composeFile := fmt.Sprintf(`version: '3.8'
services:
%s

networks:
  traefik:
    external: true
`, strings.Join(services, "\n\n"))

	return composeFile, nil
}
