package services

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

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

// vpsID and ipAddress are used for local previews
const vpsID = "byop.local"
const ipAddress = "127.0.0.1"

// LocalPreviewService handles local preview deployments using Docker Compose
//
// IMPORTANT: This service is intended for development and testing purposes only.
// For production environments, use the RemotePreviewService which deploys to VPS instances
// for proper isolation, security, and scalability.
//
// Local previews use:
// - Docker Compose for container orchestration
// - Local Traefik instance for routing
// - Host Docker daemon (shared with development environment)
// - localhost/127.0.0.1 networking
type LocalPreviewService struct {
	common *PreviewCommon
	entry  *logrus.Entry
	config *config.Config
}

// NewLocalPreviewService creates a new LocalPreviewService
func NewLocalPreviewService(store *dbstore.SQLiteStore, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *LocalPreviewService {
	entry := logrus.WithField("service", "LocalPreviewService")
	entry.Warn("LocalPreviewService initialized - this is for development/testing only, not for production use")

	return &LocalPreviewService{
		common: NewPreviewCommon(store, registryURL, registryUser, registryPass),
		entry:  entry,
		config: cfg,
	}
}

// Close cleans up resources
func (lps *LocalPreviewService) Close(ctx context.Context) {
	lps.entry.Info("Cleaning up local preview service...")
	lps.common.CleanupAllPreviewContainers(ctx)
	lps.common.Close()
}

// CreatePreview creates a local preview environment
func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId uint) (*models.Preview, error) {
	// Get app details
	app, err := lps.common.GetStore().GetAppByID(ctx, appId)
	if err != nil {
		if models.IsErrNotFound(err) {
			return nil, models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for preview creation", appId), err)
		}
		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get app by ID %d", appId), err)
	}
	if app == nil {
		return nil, models.NewErrNotFound(fmt.Sprintf("app with ID %d not found (unexpected nil)", appId), nil)
	}

	// Create preview record
	preview := models.Preview{
		AppID:     app.ID,
		Status:    models.PreviewStatusBuilding,
		ExpiresAt: time.Now().Add(24 * time.Hour),
	}

	err = lps.common.GetStore().CreatePreview(ctx, &preview)
	if err != nil {
		if _, ok := err.(models.CustomError); !ok {
			return nil, models.NewErrInternalServer("failed to create preview record in db", err)
		}
		return nil, err
	}

	// Start async build and deploy locally
	go lps.buildAndDeployPreview(context.Background(), preview, app)

	return &preview, nil
}

func (lps *LocalPreviewService) buildAndDeployPreview(ctx context.Context, preview models.Preview, app *models.App) {
	lps.entry.WithField("preview_id", preview.ID).Info("Starting local preview build and deployment")

	// Get all components for the app
	lps.entry.WithField("preview_id", preview.ID).Info("Getting app components")
	components, err := lps.common.GetAppComponents(ctx, app)
	if err != nil {
		lps.entry.WithField("preview_id", preview.ID).Errorf("Failed to get app components: %v", err)
		lps.common.UpdatePreviewStatus(ctx, preview.ID, models.PreviewStatusFailed, fmt.Sprintf("Failed to get app components: %v", err))
		lps.common.GetStore().UpdateAppStatus(ctx, app.ID, models.AppStatusFailed, fmt.Sprintf("Preview creation failed: %v", err))
		return
	}
	lps.entry.WithField("preview_id", preview.ID).WithField("component_count", len(components)).Info("Successfully retrieved app components")

	// Step 1: Build Docker images locally
	lps.entry.WithField("preview_id", preview.ID).Info("Starting Docker image build phase")
	imageNames, buildLogs, err := lps.common.BuildComponentImages(ctx, components)
	if err != nil {
		lps.entry.WithField("preview_id", preview.ID).Errorf("Failed to build component images: %v", err)
		lps.common.UpdatePreviewStatus(ctx, preview.ID, models.PreviewStatusFailed, fmt.Sprintf("Failed to build images: %v", err))
		lps.common.UpdatePreviewBuildLogs(ctx, preview.ID, buildLogs)
		lps.common.GetStore().UpdateAppStatus(ctx, app.ID, models.AppStatusFailed, fmt.Sprintf("Preview build failed: %v", err))
		return
	}
	lps.entry.WithField("preview_id", preview.ID).WithField("image_count", len(imageNames)).Info("Docker image build phase completed successfully")

	lps.common.UpdatePreviewBuildLogs(ctx, preview.ID, buildLogs)

	// Step 2: Local deployment setup
	lps.entry.WithField("preview_id", preview.ID).Info("Starting local deployment phase")
	lps.common.UpdatePreviewStatus(ctx, preview.ID, models.PreviewStatusDeploying, "")

	// Generate unique preview ID and URL
	previewIDStr := lps.common.GeneratePreviewID()
	previewURL := fmt.Sprintf("https://%s.%s", previewIDStr, lps.config.PreviewTLD)
	lps.entry.WithField("preview_id", preview.ID).WithField("preview_url", previewURL).WithField("uuid", previewIDStr).Info("Generated local preview URL")

	// Update preview with local info
	if err := lps.common.GetStore().UpdatePreviewVPS(ctx, preview.ID, vpsID, ipAddress, previewURL); err != nil {
		lps.entry.WithField("preview_id", preview.ID).Errorf("Failed to update preview info: %v", err)
	}

	// Step 3: Deploy locally
	lps.entry.WithField("preview_id", preview.ID).Info("Starting local container deployment")
	deployLogs, err := lps.deployLocally(ctx, imageNames, app, previewIDStr)
	if err != nil {
		lps.entry.WithField("preview_id", preview.ID).Errorf("Failed to deploy locally: %v", err)
		lps.common.UpdatePreviewStatus(ctx, preview.ID, models.PreviewStatusFailed, fmt.Sprintf("Failed to deploy locally: %v", err))
		lps.common.UpdatePreviewDeployLogs(ctx, preview.ID, deployLogs)
		lps.common.GetStore().UpdateAppStatus(ctx, app.ID, models.AppStatusFailed, fmt.Sprintf("Local deployment failed: %v", err))
		return
	}
	lps.entry.WithField("preview_id", preview.ID).Info("Local deployment completed successfully")

	lps.common.UpdatePreviewDeployLogs(ctx, preview.ID, deployLogs)
	lps.common.UpdatePreviewStatus(ctx, preview.ID, models.PreviewStatusRunning, "")

	// Update app status to ready with preview info
	lps.common.GetStore().UpdateAppPreview(ctx, app.ID, preview.ID, previewURL)

	lps.entry.WithField("preview_id", preview.ID).WithField("preview_url", previewURL).Info("Local preview deployment completed successfully")
}

func (lps *LocalPreviewService) deployLocally(ctx context.Context, imageNames []string, app *models.App, previewIDStr string) (string, error) {
	var logs strings.Builder

	lps.entry.WithField("app_id", app.ID).WithField("app_name", app.Name).Info("Starting local deployment")
	logs.WriteString("Starting local deployment...\n")

	// Generate docker-compose content
	composeContent, err := lps.generatePreviewDockerCompose(ctx, imageNames, app, previewIDStr)
	if err != nil {
		lps.entry.WithField("app_id", app.ID).Errorf("Failed to generate compose file: %v", err)
		if _, ok := err.(models.CustomError); !ok {
			err = models.NewErrInternalServer("failed to generate compose file", err)
		}
		return logs.String(), err
	}

	// Save docker-compose.yml locally (temp file for execution)
	composeFile := filepath.Join(os.TempDir(), fmt.Sprintf("docker-compose-preview-%s.yml", app.Name))

	// Also save to a persistent debug location
	debugDir := "/tmp/byop-debug"
	if err := os.MkdirAll(debugDir, 0755); err != nil {
		lps.entry.WithField("app_id", app.ID).Warnf("Failed to create debug directory: %v", err)
	}
	debugComposeFile := filepath.Join(debugDir, fmt.Sprintf("docker-compose-app-%d-preview-%d.yml", app.ID, time.Now().Unix()))

	// Write the temporary file
	if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
		lps.entry.WithField("app_id", app.ID).Errorf("Failed to write compose file: %v", err)
		return logs.String(), models.NewErrInternalServer(fmt.Sprintf("failed to write compose file %s", composeFile), err)
	}
	defer os.Remove(composeFile)

	// Write the debug file (persistent)
	if err := os.WriteFile(debugComposeFile, []byte(composeContent), 0644); err != nil {
		lps.entry.WithField("app_id", app.ID).Warnf("Failed to write debug compose file: %v", err)
	} else {
		lps.entry.WithField("app_id", app.ID).WithField("debug_file", debugComposeFile).Info("Wrote debug compose file for inspection")
		logs.WriteString(fmt.Sprintf("Debug compose file saved to: %s\n", debugComposeFile))
	}

	logs.WriteString(fmt.Sprintf("Generated compose file: %s\n", composeFile))
	logs.WriteString(fmt.Sprintf("Compose content:\n%s\n", composeContent))

	// Check if Traefik network exists, create if it doesn't
	lps.entry.WithField("app_id", app.ID).Info("Checking/creating Traefik network")
	cmdCtx, cancelCmd := context.WithTimeout(ctx, 15*time.Second)
	defer cancelCmd()
	cmd := exec.CommandContext(cmdCtx, "docker", "network", "create", "traefik")
	if err := cmd.Run(); err != nil {
		lps.entry.WithField("app_id", app.ID).Warnf("Failed to create traefik network (may already exist): %v", err)
		logs.WriteString(fmt.Sprintf("Network creation output: %v (this is normal if network exists)\n", err))
	} else {
		lps.entry.WithField("app_id", app.ID).Info("Created traefik network")
		logs.WriteString("Created traefik network\n")
	}

	// Start containers using docker-compose
	lps.entry.WithField("app_id", app.ID).WithField("compose_file", composeFile).Info("Starting containers with docker-compose")
	cmdCtxComposeUp, cancelComposeUp := context.WithTimeout(ctx, 2*time.Minute)
	defer cancelComposeUp()
	cmd = exec.CommandContext(cmdCtxComposeUp, "docker-compose", "-f", composeFile, "up", "-d")
	cmd.Dir = os.TempDir()

	output, err := cmd.CombinedOutput()
	logs.WriteString(fmt.Sprintf("Docker-compose output:\n%s\n", string(output)))

	if err != nil {
		lps.entry.WithField("app_id", app.ID).Errorf("Failed to start containers: %v", err)
		lps.entry.WithField("app_id", app.ID).Errorf("Docker-compose error output: %s", string(output))
		logs.WriteString(fmt.Sprintf("ERROR: Docker-compose failed with: %v\n", err))
		return logs.String(), models.NewErrInternalServer(fmt.Sprintf("docker-compose up failed for app %d", app.ID), err)
	}
	lps.entry.WithField("app_id", app.ID).Info("Successfully started containers")

	// Verify containers are running
	cmdCtxPs, cancelPs := context.WithTimeout(ctx, 30*time.Second)
	defer cancelPs()
	cmd = exec.CommandContext(cmdCtxPs, "docker-compose", "-f", composeFile, "ps")
	output, err = cmd.CombinedOutput()
	if err != nil {
		lps.entry.WithField("app_id", app.ID).Warnf("Failed to check container status: %v", err)
		logs.WriteString(fmt.Sprintf("Warning: failed to check container status: %v\n", err))
	} else {
		logs.WriteString(fmt.Sprintf("Container status:\n%s\n", string(output)))
	}

	logs.WriteString("Local deployment completed successfully\n")
	logs.WriteString(fmt.Sprintf("Debug compose file: %s\n", debugComposeFile))

	return logs.String(), nil
}

func (lps *LocalPreviewService) generatePreviewDockerCompose(ctx context.Context, imageNames []string, app *models.App, previewIDStr string) (string, error) {
	lps.entry.WithField("app_id", app.ID).WithField("image_count", len(imageNames)).Info("Generating docker-compose content")

	// Get app components to check if this is a docker-compose imported app
	components, err := lps.common.GetAppComponents(ctx, app)
	if err != nil {
		return "", fmt.Errorf("failed to get app components: %w", err)
	}

	// Check if this is a docker-compose imported app
	isDockerComposeApp := false
	for _, component := range components {
		if component.SourceType == "docker-compose" {
			isDockerComposeApp = true
			break
		}
	}

	if isDockerComposeApp {
		return lps.generateDockerComposeFromComponents(ctx, imageNames, components, app, previewIDStr)
	} else {
		return lps.generateGenericDockerCompose(ctx, imageNames, app, previewIDStr)
	}
}

// generateDockerComposeFromComponents generates a docker-compose file based on the original component configuration
func (lps *LocalPreviewService) generateDockerComposeFromComponents(ctx context.Context, imageNames []string, components []models.Component, app *models.App, previewIDStr string) (string, error) {
	lps.entry.WithField("app_id", app.ID).Info("Generating docker-compose from component configuration")

	compose := "services:\n"

	for i, component := range components {
		if i >= len(imageNames) {
			continue // Skip if we don't have a corresponding image
		}

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

		compose += fmt.Sprintf("  %s:\n", serviceName)
		compose += fmt.Sprintf("    image: %s\n", imageName)
		compose += "    restart: unless-stopped\n"

		// Add environment variables
		compose += "    environment:\n"
		compose += "      - NODE_ENV=preview\n"
		compose += fmt.Sprintf("      - APP_NAME=%s\n", app.Name)

		// Add labels for tracking
		compose += "    labels:\n"
		compose += "      - \"byop.preview=true\"\n"
		compose += fmt.Sprintf("      - \"byop.preview.id=%s\"\n", previewIDStr)
		compose += fmt.Sprintf("      - \"byop.app.id=%d\"\n", app.ID)
		compose += fmt.Sprintf("      - \"byop.app.name=%s\"\n", app.Name)

		// Add Traefik configuration for web components
		if component.Type == "web" {
			previewDomain := fmt.Sprintf("%s.%s", previewIDStr, lps.config.PreviewTLD)
			routerName := fmt.Sprintf("local-preview-%s-%s", previewIDStr, serviceName)
			compose += "      - \"traefik.enable=true\"\n"
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.rule=Host(`%s`)\"\n", routerName, previewDomain)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.entrypoints=websecure\"\n", routerName)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls=true\"\n", routerName)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls.certresolver=tlsresolver\"\n", routerName)
			compose += "      - \"traefik.docker.network=traefik\"\n"
		}

		compose += "    networks:\n"
		compose += "      - traefik\n"
		compose += "\n"
	}

	compose += "networks:\n"
	compose += "  traefik:\n"
	compose += "    external: true\n"

	return compose, nil
}

// generateGenericDockerCompose generates a generic docker-compose file for non-docker-compose apps
func (lps *LocalPreviewService) generateGenericDockerCompose(ctx context.Context, imageNames []string, app *models.App, previewIDStr string) (string, error) {
	compose := "services:\n"

	for i, imageName := range imageNames {
		serviceName := fmt.Sprintf("service-%d", i)
		compose += fmt.Sprintf("  %s:\n", serviceName)
		compose += fmt.Sprintf("    image: %s\n", imageName)
		compose += "    restart: unless-stopped\n"
		compose += "    environment:\n"
		compose += "      - NODE_ENV=preview\n"
		compose += fmt.Sprintf("      - APP_NAME=%s\n", app.Name)

		compose += "    labels:\n"
		compose += "      - \"byop.preview=true\"\n"
		compose += fmt.Sprintf("      - \"byop.preview.id=%s\"\n", previewIDStr)
		compose += fmt.Sprintf("      - \"byop.app.id=%d\"\n", app.ID)
		compose += fmt.Sprintf("      - \"byop.app.name=%s\"\n", app.Name)

		if i == 0 {
			previewDomain := fmt.Sprintf("%s.%s", previewIDStr, lps.config.PreviewTLD)
			routerName := fmt.Sprintf("local-preview-%s", previewIDStr)
			compose += "      - \"traefik.enable=true\"\n"
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.rule=Host(`%s`)\"\n", routerName, previewDomain)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.entrypoints=websecure\"\n", routerName)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls=true\"\n", routerName)
			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls.certresolver=tlsresolver\"\n", routerName)
			compose += "      - \"traefik.docker.network=traefik\"\n"
		}

		compose += "    networks:\n"
		compose += "      - traefik\n"
		compose += "\n"
	}

	compose += "networks:\n"
	compose += "  traefik:\n"
	compose += "    external: true\n"

	return compose, nil
}

// DeletePreview deletes a local preview
func (lps *LocalPreviewService) DeletePreview(ctx context.Context, appID uint) error {
	preview, err := lps.common.GetStore().GetPreviewByAppID(ctx, appID)
	if err != nil {
		if models.IsErrNotFound(err) {
			return models.NewErrNotFound(fmt.Sprintf("preview for app ID %d not found for deletion", appID), err)
		}
		return models.NewErrInternalServer(fmt.Sprintf("failed to get preview by app ID %d for deletion", appID), err)
	}

	if preview == nil {
		return models.NewErrNotFound(fmt.Sprintf("preview with app ID %d not found for deletion (unexpected nil)", appID), nil)
	}

	lps.entry.WithField("preview_id", preview.ID).Info("Deleting local preview")

	lps.common.CleanupByAppID(ctx, appID)

	if err := lps.common.GetStore().DeletePreview(ctx, preview.ID); err != nil {
		if models.IsErrNotFound(err) {
			return models.NewErrNotFound(fmt.Sprintf("preview %d not found for deletion from DB", preview.ID), err)
		}
		return models.NewErrInternalServer(fmt.Sprintf("failed to delete preview %d from database", preview.ID), err)
	}

	lps.entry.WithField("preview_id", preview.ID).Info("Successfully deleted local preview")
	return nil
}

// StopPreview stops a local preview
func (lps *LocalPreviewService) StopPreview(ctx context.Context, previewID uint) error {
	preview, err := lps.common.GetStore().GetPreviewByID(ctx, previewID)
	if err != nil {
		if models.IsErrNotFound(err) {
			return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for stopping", previewID), err)
		}
		return models.NewErrInternalServer(fmt.Sprintf("failed to get preview by ID %d for stopping", previewID), err)
	}

	if preview == nil {
		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for stopping (unexpected nil)", previewID), nil)
	}

	lps.common.CleanupByAppID(ctx, preview.AppID)

	err = lps.common.GetStore().UpdatePreviewStatus(ctx, previewID, models.PreviewStatusStopped, "")
	if err != nil {
		if models.IsErrNotFound(err) {
			return models.NewErrNotFound(fmt.Sprintf("preview %d not found for status update to stopped", previewID), err)
		}
		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview %d status to stopped", previewID), err)
	}
	return nil
}
