package main

import (
	"encoding/json"
	"fmt"
	"html"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/jaredfolkins/terraform-loading-bar/progress"
)

// TerraformFunction represents the type of Terraform operation to perform
type TerraformFunction string

const (
	FunctionApply   TerraformFunction = "apply"
	FunctionDestroy TerraformFunction = "destroy"
	FunctionNuke    TerraformFunction = "nuke"
	FunctionAMI     TerraformFunction = "ami"
	// In this cookbook, "image" creates a standard GCP disk image from the instance's boot disk.
	// Keep AWS AMI creation under "ami"; map "image" to a GCP disk image implementation.
	FunctionImage            TerraformFunction = "image"
	FunctionListImages       TerraformFunction = "list-images"
	tfVarsFilename                             = "terraform.tfvars"
	minLinuxBootDiskSizeGB                     = 20
	minWindowsBootDiskSizeGB                   = 50
	defaultSSHPort                             = 22
	minCustomSSHPort                           = 10000
	maxSSHPort                                 = 65535
)

type Config struct {
	ProjectID                 string
	Region                    string
	Zone                      string
	ResourcePrefix            string
	LEMCUUID                  string
	LEMCScope                 string
	LEMCUsername              string
	LEMCUserID                string
	MachineType               string
	Image                     string
	Port                      string
	SSHPort                   string
	DiskSize                  string
	IsWindows                 bool
	IsWindowsOverride         bool
	IsWindowsDetected         bool
	GoogleCredentials         string
	LEMCHTTPBaseURL           string
	AWSRegion                 string
	LEMCInstanceType          string
	ImageName                 string
	DomainName                string
	HostedZoneID              string
	BackendTimeoutSeconds     string
	ExistingSSLCertificateARN string
	AbsoluteSourceDir         string
	AbsoluteDestDir           string
	WorkingDir                string
	PublicDir                 string
	AWSAccessKeyID            string
	AWSSecretAccessKey        string
	AMIName                   string
	SourceInstanceID          string
	AMIShareAccounts          []string
	// GCP image creation
	SourceInstanceName string
	SourceInstanceZone string
	ImageShareMembers  []string
	// Imaging options
	WindowsSysprep       bool
	ImageFamily          string
	ImageStorageLocation string
	ImageRecipeFilter    string
	ResourceLabels       map[string]string
	ResourceNetworkTags  []string
	LEMCRecipeName       string
	LEMCPageID           string
	LEMCStepID           string
	// Add other fields as needed
}

func GetConfig() (*Config, error) {
	config := &Config{}
	// Extract project_id from GOOGLE_CREDENTIALS JSON
	credsJSON := os.Getenv("GOOGLE_CREDENTIALS")
	if credsJSON != "" {
		var creds map[string]interface{}
		if err := json.Unmarshal([]byte(credsJSON), &creds); err == nil {
			if projectID, ok := creds["project_id"].(string); ok {
				config.ProjectID = projectID
			}
		}
	}
	config.Region = os.Getenv("GCP_REGION")
	config.Zone = os.Getenv("GCP_ZONE")
	config.LEMCUUID = os.Getenv("LEMC_UUID")
	config.LEMCScope = os.Getenv("LEMC_SCOPE")
	config.LEMCUsername = os.Getenv("LEMC_USERNAME")
	config.LEMCUserID = os.Getenv("LEMC_USER_ID")
	config.LEMCRecipeName = os.Getenv("LEMC_RECIPE_NAME")
	config.LEMCPageID = os.Getenv("LEMC_PAGE_ID")
	config.LEMCStepID = os.Getenv("LEMC_STEP_ID")
	resourceTagsEnv := combinedCommaEnv("LEMC_RESOURCE_TAGS", "LEMC_TAGS", "RESOURCE_TAGS", "VM_TAGS", "IMAGE_TAGS")
	config.ResourceLabels, config.ResourceNetworkTags = parseResourceTags(resourceTagsEnv, config.LEMCUsername)
	customMachineType := strings.TrimSpace(os.Getenv("CUSTOM_MACHINE_TYPE"))
	config.MachineType = firstNonEmpty(customMachineType, os.Getenv("MACHINE_TYPE"))
	if customMachineType != "" {
		lg("Using CUSTOM_MACHINE_TYPE override for MACHINE_TYPE")
	}
	config.Image = os.Getenv("IMAGE")
	// Allow CUSTOM_IMAGE to override IMAGE when provided via form
	customImage := strings.TrimSpace(os.Getenv("CUSTOM_IMAGE"))
	if strings.EqualFold(strings.TrimSpace(os.Getenv("CUSTOM_IMAGE_REQUIRED")), "true") && customImage == "" {
		return nil, fmt.Errorf("CUSTOM_IMAGE is required for this recipe")
	}
	if customImage != "" {
		ci := customImage
		config.Image = ci
		lg("Using CUSTOM_IMAGE override for IMAGE")
	}
	// Name for the image to create (GCP) or AMI name (AWS path)
	config.ImageName = os.Getenv("IMAGE_NAME")
	if config.ImageName == "" {
		// Accept AWS-style alias for parity when running cross-cloud
		if v := os.Getenv("AMI_NAME"); v != "" {
			config.ImageName = v
			lg("Using AMI_NAME as IMAGE_NAME alias for GCP image name")
		}
	}
	config.Port = os.Getenv("PORT")
	sshPort, sshPortErr := normalizeSSHPort(os.Getenv("SSH_PORT"))
	if sshPortErr != nil {
		return nil, sshPortErr
	}
	config.SSHPort = sshPort
	config.DiskSize = os.Getenv("DISK_SIZE")
	// Flexible parse for IS_WINDOWS (true/1/yes)
	if raw, ok := os.LookupEnv("IS_WINDOWS"); ok {
		config.IsWindowsOverride = true
		v := strings.ToLower(strings.TrimSpace(raw))
		if v == "true" || v == "1" || v == "yes" {
			config.IsWindows = true
		} else if v == "false" || v == "0" || v == "no" {
			config.IsWindows = false
		}
	}
	config.GoogleCredentials = os.Getenv("GOOGLE_CREDENTIALS")
	config.LEMCHTTPBaseURL = os.Getenv("LEMC_HTTP_DOWNLOAD_BASE_URL")

	var err error
	config.WorkingDir, err = os.Getwd()
	if err != nil {
		return nil, fmt.Errorf("error getting working directory: %v", err)
	}
	config.AbsoluteSourceDir = filepath.Join(config.WorkingDir, "terraform-config")
	config.AbsoluteDestDir = "/lemc/private"
	config.PublicDir = "/lemc/public"

	// Create required directories
	if err := os.MkdirAll(config.AbsoluteDestDir, os.ModePerm); err != nil {
		return nil, fmt.Errorf("error creating destination directory %s: %v", config.AbsoluteDestDir, err)
	}

	if err := os.MkdirAll(config.PublicDir, os.ModePerm); err != nil {
		return nil, fmt.Errorf("error creating public directory %s: %v", config.PublicDir, err)
	}

	// Construct resource prefix using user identifiers for deterministic naming
	config.ResourcePrefix = fmt.Sprintf("lemc-%s-%s-%s-%s", config.LEMCUUID[:8], config.LEMCUsername, config.LEMCUserID, config.LEMCScope)

	// GCP image creation specific (optional) overrides
	config.SourceInstanceName = os.Getenv("SOURCE_INSTANCE_NAME")
	config.SourceInstanceZone = os.Getenv("SOURCE_INSTANCE_ZONE")

	// Parse image share members (comma-separated), e.g., serviceAccount:sa@proj.iam.gserviceaccount.com
	membersEnv := os.Getenv("IMAGE_SHARE_MEMBERS")
	if membersEnv == "" {
		// Back-compat with previous var name
		membersEnv = os.Getenv("MACHINE_IMAGE_SHARE_MEMBERS")
	}
	if membersEnv != "" {
		parts := strings.Split(membersEnv, ",")
		for _, p := range parts {
			m := strings.TrimSpace(p)
			if m != "" {
				config.ImageShareMembers = append(config.ImageShareMembers, m)
			}
		}
	}

	// Imaging options
	// WINDOWS_SYSPREP: default true unless explicitly disabled
	config.WindowsSysprep = true
	if raw, ok := os.LookupEnv("WINDOWS_SYSPREP"); ok {
		v := strings.ToLower(strings.TrimSpace(raw))
		if v == "false" || v == "0" || v == "no" {
			config.WindowsSysprep = false
		} else {
			config.WindowsSysprep = true
		}
	}
	config.ImageFamily = os.Getenv("IMAGE_FAMILY")
	config.ImageStorageLocation = os.Getenv("IMAGE_STORAGE_LOCATION")
	config.ImageRecipeFilter = os.Getenv("IMAGE_RECIPE_FILTER")

	// Validation
	if config.ProjectID == "" || config.Zone == "" || config.GoogleCredentials == "" {
		return nil, fmt.Errorf("missing required GCP configs")
	}

	return config, nil
}

func lt(s string) {
	fmt.Printf("lemc.html.trunc; <pre>%s</pre><br>\n", s)
}

func lg(s string) {
	fmt.Printf("lemc.html.append; <pre>%s</pre><br>\n", s)
}

func lemcErr(s string) {
	fmt.Printf("lemc.err;<pre>%s</pre><br>\n", html.EscapeString(s))
}

func failLEMC(format string, args ...interface{}) {
	lemcErr(fmt.Sprintf(format, args...))
	os.Exit(1)
}

// TerraformInit initializes a Terraform working directory
func TerraformInit(workingDir string) error {
	lg("Running Terraform init...")
	initCmd := exec.Command("terraform", "init")
	initCmd.Stdout = os.Stdout
	initCmd.Stderr = os.Stderr
	initCmd.Dir = workingDir
	return initCmd.Run()
}

// TerraformPlan creates a Terraform execution plan
func TerraformPlan(workingDir string) error {
	lg("Running Terraform plan...")
	planCmd := exec.Command("terraform", "plan", "-out", "terraform.plan")
	planCmd.Stdout = os.Stdout
	planCmd.Stderr = os.Stderr
	planCmd.Dir = workingDir
	return planCmd.Run()
}

// runTerraformCommandWithProgress executes a Terraform command and handles its progress output
func runTerraformCommandWithProgress(cmd *exec.Cmd, operation string) error {
	output, err := cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("error creating stdout pipe: %v", err)
	}
	cmd.Stderr = os.Stderr

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("error starting %s command: %v", operation, err)
	}

	progressHandler := progress.NewProgressHandler(output)

	for {
		line, err := progressHandler.ReadLine()
		if err != nil {
			if err == io.EOF {
				break
			}
			lg(fmt.Sprintf("Error: Terraform %s failed. %v", operation, err))
			for _, line := range progressHandler.GetOriginalOutput() {
				lg(line)
			}
			return fmt.Errorf("error reading %s output: %v", operation, err)
		}

		if line != "" {
			formattedLine := strings.TrimSpace(line)
			lt(formattedLine)
		}
	}

	if err := cmd.Wait(); err != nil {
		lg(fmt.Sprintf("Error: Terraform %s failed. %v", operation, err))
		for _, line := range progressHandler.GetOriginalOutput() {
			lg(line)
		}
		return fmt.Errorf("error waiting for %s command: %v", operation, err)
	}

	lg(fmt.Sprintf("Terraform %s completed.", operation))
	lg(fmt.Sprintf("Complete Terraform %s JSON output:", operation))

	for _, line := range progressHandler.GetOriginalOutput() {
		log.Println(line)
	}

	return nil
}

// TerraformApply applies the Terraform plan
func TerraformApply(workingDir string) error {
	lg("Running Terraform apply...")
	applyCmd := exec.Command("terraform", "apply", "-auto-approve", "-json", "terraform.plan")
	applyCmd.Dir = workingDir
	return runTerraformCommandWithProgress(applyCmd, "apply")
}

// TerraformDestroy destroys the Terraform-managed infrastructure
func TerraformDestroy(workingDir string) error {
	lg("Running Terraform destroy...")
	destroyCmd := exec.Command("terraform", "destroy", "-auto-approve", "-json")
	destroyCmd.Dir = workingDir
	return runTerraformCommandWithProgress(destroyCmd, "destroy")
}

// GetTerraformOutputs retrieves the Terraform outputs as a map
func GetTerraformOutputs(workingDir string) (map[string]struct {
	Value     interface{} `json:"value"`
	Type      string      `json:"type"`
	Sensitive bool        `json:"sensitive"`
}, error) {
	lg("Retrieving Terraform outputs...")
	outputCmd := exec.Command("terraform", "output", "-json")
	outputCmd.Dir = workingDir
	outputJSON, err := outputCmd.Output()
	if err != nil {
		return nil, fmt.Errorf("error running terraform output -json: %v", err)
	}

	var outputs map[string]struct {
		Value     interface{} `json:"value"`
		Type      string      `json:"type"`
		Sensitive bool        `json:"sensitive"`
	}
	if err := json.Unmarshal(outputJSON, &outputs); err != nil {
		return nil, fmt.Errorf("error parsing terraform output JSON: %v", err)
	}

	return outputs, nil
}

// GenerateTerraformVars generates the content for terraform.tfvars
func (c *Config) GenerateTerraformVars() string {
	var content strings.Builder

	// Map of environment variables to Terraform variables
	varMap := map[string]string{
		"GCP_REGION":             "region",
		"GCP_ZONE":               "zone",
		"LEMC_SCOPE":             "lemc_scope",
		"LEMC_USERNAME":          "lemc_username",
		"LEMC_USER_ID":           "lemc_user_id",
		"LEMC_RECIPE_NAME":       "lemc_recipe_name",
		"LEMC_PAGE_ID":           "lemc_page_id",
		"PORT":                   "port",
		"WINDOWS_ADMIN_USERNAME": "windows_admin_username",
	}

	// Add project_id from extracted credentials
	if c.ProjectID != "" {
		content.WriteString(fmt.Sprintf("project_id = \"%s\"\n", c.ProjectID))
		lg(fmt.Sprintf("Using project_id from GOOGLE_CREDENTIALS: %s", c.ProjectID))
	} else {
		lg("Warning: project_id not found in GOOGLE_CREDENTIALS")
	}

	// Add mapped variables
	for envKey, tfKey := range varMap {
		value := os.Getenv(envKey)
		if value != "" {
			content.WriteString(fmt.Sprintf("%s = \"%s\"\n", tfKey, value))
		} else {
			lg(fmt.Sprintf("Warning: Environment variable %s not found.", envKey))
		}
	}

	// Add lemc_uuid
	content.WriteString(fmt.Sprintf("lemc_uuid = \"%s\"\n", c.LEMCUUID))

	// Add resource prefix
	content.WriteString(fmt.Sprintf("resource_prefix = \"%s\"\n", c.ResourcePrefix))
	content.WriteString(fmt.Sprintf("ssh_port = %s\n", c.SSHPort))
	if c.SSHPort == strconv.Itoa(defaultSSHPort) {
		lg("Using default SSH port: 22")
	} else {
		lg(fmt.Sprintf("Using custom SSH port: %s", c.SSHPort))
	}

	if len(c.ResourceLabels) > 0 {
		content.WriteString(fmt.Sprintf("lemc_resource_labels = %s\n", terraformStringMap(c.ResourceLabels)))
		lg(fmt.Sprintf("Applying resource labels: %s", formatLabelMap(c.ResourceLabels)))
	}

	if len(c.ResourceNetworkTags) > 0 {
		content.WriteString(fmt.Sprintf("lemc_resource_tags = %s\n", terraformStringList(c.ResourceNetworkTags)))
		lg(fmt.Sprintf("Applying resource network tags: %s", strings.Join(c.ResourceNetworkTags, ", ")))
	}

	// Add machine type if specified
	if c.MachineType != "" {
		content.WriteString(fmt.Sprintf("machine_type = \"%s\"\n", c.MachineType))
	} else {
		lg("Warning: MACHINE_TYPE not found in environment. Terraform will use its default.")
	}

	nestedVirtualizationEnabled := true
	if raw, ok := os.LookupEnv("ENABLE_NESTED_VIRTUALIZATION"); ok && strings.TrimSpace(raw) != "" {
		if enabled, valid := parseBoolString(raw); valid {
			nestedVirtualizationEnabled = enabled
			content.WriteString(fmt.Sprintf("enable_nested_virtualization = %t\n", enabled))
		} else {
			lg(fmt.Sprintf("Warning: invalid ENABLE_NESTED_VIRTUALIZATION value %q. Terraform will use its default.", raw))
		}
	}

	if minCPUPlatform := strings.TrimSpace(os.Getenv("MIN_CPU_PLATFORM")); minCPUPlatform != "" {
		content.WriteString(fmt.Sprintf("min_cpu_platform = \"%s\"\n", minCPUPlatform))
	} else if nestedVirtualizationEnabled {
		if minCPUPlatform := inferMinCPUPlatform(c.MachineType); minCPUPlatform != "" {
			content.WriteString(fmt.Sprintf("min_cpu_platform = \"%s\"\n", minCPUPlatform))
			lg(fmt.Sprintf("Using min CPU platform for nested virtualization: %s", minCPUPlatform))
		}
	}

	// Add image if specified
	if c.Image != "" {
		content.WriteString(fmt.Sprintf("image = \"%s\"\n", c.Image))
		lg(fmt.Sprintf("Using image: %s", c.Image))
	} else {
		lg("Warning: IMAGE not found in environment. Terraform will use its default.")
	}

	// Add Windows flag
	// Default: detect from image string
	isWindowsImage := strings.Contains(strings.ToLower(c.Image), "windows")
	// Override if IS_WINDOWS env explicitly set
	if raw, ok := os.LookupEnv("IS_WINDOWS"); ok {
		v := strings.ToLower(strings.TrimSpace(raw))
		if v == "true" || v == "1" || v == "yes" {
			isWindowsImage = true
			lg("IS_WINDOWS override: true")
		} else if v == "false" || v == "0" || v == "no" {
			isWindowsImage = false
			lg("IS_WINDOWS override: false")
		}
	} else {
		// If not explicitly set and a CUSTOM_IMAGE was provided, probe the image via gcloud for Windows license
		if c.Image != "" && (strings.HasPrefix(c.Image, "projects/") || strings.Contains(c.Image, "/")) {
			if detected, err := detectGCPImageIsWindows(c.Image); err == nil {
				if detected != isWindowsImage {
					isWindowsImage = detected
					if detected {
						lg("Detected Windows image via gcloud license probe")
					} else {
						lg("Detected Linux image via gcloud license probe")
					}
				}
			} else {
				lg(fmt.Sprintf("Warning: unable to detect image OS via gcloud: %v", err))
			}
		}
	}
	content.WriteString(fmt.Sprintf("is_windows = %t\n", isWindowsImage))
	if isWindowsImage {
		lg("Configuring for Windows instance (RDP access)")
	} else {
		lg("Configuring for Linux instance (SSH access)")
	}

	// Add disk size
	diskSize := normalizeBootDiskSize(c.DiskSize, isWindowsImage)
	c.DiskSize = strconv.Itoa(diskSize)
	content.WriteString(fmt.Sprintf("disk_size = %d\n", diskSize))
	lg(fmt.Sprintf("Using disk size: %d GB", diskSize))

	return content.String()
}

// detectGCPImageIsWindows inspects an image reference to determine if it's Windows by checking licenses
// Supported refs:
// - projects/<project>/global/images/<name>
// - projects/<project>/global/images/family/<family>
// - <project>/<family> (e.g., windows-cloud/windows-2022)
func detectGCPImageIsWindows(imageRef string) (bool, error) {
	var cmd *exec.Cmd
	ref := imageRef
	lower := strings.ToLower(ref)
	// projects/<proj>/global/images/family/<family>
	if strings.Contains(lower, "/global/images/family/") {
		parts := strings.Split(ref, "/")
		// [projects, <proj>, global, images, family, <family>]
		if len(parts) >= 6 {
			project := parts[1]
			family := parts[5]
			cmd = exec.Command("gcloud", "compute", "images", "describe-from-family", family, "--project", project, "--format", "json")
		}
	} else if strings.Contains(lower, "/global/images/") && strings.Contains(lower, "projects/") {
		// projects/<proj>/global/images/<name>
		parts := strings.Split(ref, "/")
		if len(parts) >= 6 {
			project := parts[1]
			name := parts[5]
			cmd = exec.Command("gcloud", "compute", "images", "describe", name, "--project", project, "--format", "json")
		}
	} else if strings.Count(ref, "/") == 1 {
		// <project>/<family>
		segs := strings.Split(ref, "/")
		project := segs[0]
		family := segs[1]
		cmd = exec.Command("gcloud", "compute", "images", "describe-from-family", family, "--project", project, "--format", "json")
	}
	if cmd == nil {
		// Not a recognized format; best-effort string check
		return strings.Contains(lower, "windows"), nil
	}
	out, err := cmd.Output()
	if err != nil {
		return false, err
	}
	var img struct {
		Licenses        []string `json:"licenses"`
		GuestOsFeatures []struct {
			Type string `json:"type"`
		} `json:"guestOsFeatures"`
	}
	if err := json.Unmarshal(out, &img); err != nil {
		return false, err
	}
	for _, lic := range img.Licenses {
		if strings.Contains(strings.ToLower(lic), "windows") {
			return true, nil
		}
	}
	for _, f := range img.GuestOsFeatures {
		if strings.EqualFold(f.Type, "WINDOWS") {
			return true, nil
		}
	}
	return false, nil
}

// runApply executes the Terraform apply operation and handles all related tasks
func runApply(config *Config) error {
	// Initialize Terraform
	if err := TerraformInit(config.AbsoluteDestDir); err != nil {
		return fmt.Errorf("error initializing Terraform: %v", err)
	}

	// Create and apply Terraform plan
	if err := TerraformPlan(config.AbsoluteDestDir); err != nil {
		return fmt.Errorf("error creating Terraform plan: %v", err)
	}

	if err := TerraformApply(config.AbsoluteDestDir); err != nil {
		return fmt.Errorf("error applying Terraform plan: %v", err)
	}

	// Get Terraform outputs
	outputs, err := GetTerraformOutputs(config.AbsoluteDestDir)
	if err != nil {
		return fmt.Errorf("error getting Terraform outputs: %v", err)
	}

	publicIPOutput, ipOk := outputs["public_ip"]
	if !ipOk || publicIPOutput.Value == nil {
		return fmt.Errorf("public_ip not found in Terraform outputs")
	}

	publicIP, castOk := publicIPOutput.Value.(string)
	if !castOk || publicIP == "" {
		return fmt.Errorf("could not retrieve or cast public_ip from Terraform outputs to string")
	}

	// Detect if it's a Windows instance
	// Priority: explicit IS_WINDOWS env -> gcloud license probe -> image string heuristic
	isWindowsInstance := config.IsWindows
	if !isWindowsInstance {
		// If not explicitly set, try probing the image reference
		if config.Image != "" {
			low := strings.ToLower(config.Image)
			// Heuristic first
			if strings.Contains(low, "windows") {
				isWindowsInstance = true
			} else if strings.HasPrefix(low, "projects/") || strings.Count(config.Image, "/") == 1 || strings.Contains(low, "/global/images/") || strings.Contains(low, "/global/images/family/") {
				if detected, err := detectGCPImageIsWindows(config.Image); err == nil {
					isWindowsInstance = detected
					if detected {
						lg("Detected Windows instance via gcloud license probe (post-apply)")
					}
				} else {
					lg(fmt.Sprintf("Warning: unable to detect Windows via gcloud probe: %v", err))
				}
			}
		}
	}

	if isWindowsInstance {
		lg("Detected Windows instance; preparing RDP credentials and connection file...")
		// Handle Windows authentication with RDP
		return handleWindowsAuthentication(config, outputs, publicIP)
	} else {
		lg("Detected Linux instance; preparing SSH key and connection instructions...")
		// Handle Linux authentication with SSH
		return handleLinuxAuthentication(config, outputs, publicIP)
	}
}

// handleLinuxAuthentication handles SSH key generation and instructions for Linux instances
func handleLinuxAuthentication(config *Config, outputs map[string]struct {
	Value     interface{} `json:"value"`
	Type      string      `json:"type"`
	Sensitive bool        `json:"sensitive"`
}, publicIP string) error {
	privateKeyOutput, ok := outputs["private_ssh_key"]
	if !ok || privateKeyOutput.Value == nil {
		return fmt.Errorf("private_ssh_key not found in Terraform outputs")
	}

	privateKey, ok := privateKeyOutput.Value.(string)
	if !ok {
		return fmt.Errorf("private_ssh_key in Terraform outputs is not a string")
	}
	sshKeyFilename := "private-ssh-key"
	sshKeyPath := filepath.Join(config.PublicDir, sshKeyFilename)

	lg(fmt.Sprintf("Saving private SSH key to %s...", sshKeyPath))

	if err := os.WriteFile(sshKeyPath, []byte(privateKey), 0600); err != nil {
		return fmt.Errorf("error writing private SSH key to file: %v", err)
	}

	lg(fmt.Sprintf("Private SSH key saved to %s", sshKeyPath))

	downloadURL := config.LEMCHTTPBaseURL + sshKeyFilename
	lg(fmt.Sprintf(`SSH Private Key available for download: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">%s</a>`, downloadURL, sshKeyFilename))
	lg("IMPORTANT: Secure this key. It provides access to the created GCP instance. Download it and then ensure it is removed from the public folder if this is a shared environment or if the link is accessible by others.")

	// Use 'user' as the default user for GCP instances
	sshPort := config.SSHPort
	if portOutput, ok := outputs["ssh_port"]; ok && portOutput.Value != nil {
		switch v := portOutput.Value.(type) {
		case float64:
			sshPort = strconv.Itoa(int(v))
		case string:
			if strings.TrimSpace(v) != "" {
				sshPort = strings.TrimSpace(v)
			}
		}
	}
	if sshPort == "" {
		sshPort = strconv.Itoa(defaultSSHPort)
	}
	sshPortFlag := ""
	if sshPort != strconv.Itoa(defaultSSHPort) {
		sshPortFlag = fmt.Sprintf(" -p %s", sshPort)
	}
	sshCommand := fmt.Sprintf("ssh -i %s%s user@%s", sshKeyFilename, sshPortFlag, publicIP)
	lg(fmt.Sprintf("To SSH into the GCP instance (once key is downloaded and in current directory): <code>%s</code>", sshCommand))

	// Create .env file
	lg("Creating .env file...")

	s := `
SSH_USERNAME=user
SSH_PORT=%s
VM_IP=%s
GCP_INSTANCE_IP=%s
GCP_INSTANCE_SSH_PORT=%s
`

	envContent := fmt.Sprintf(s, sshPort, publicIP, publicIP, sshPort)
	envFilePath := filepath.Join(config.PublicDir, "dotenv")

	if err := os.WriteFile(envFilePath, []byte(envContent), 0644); err != nil {
		lg(fmt.Sprintf("Error writing dotenv file: %v", err))
	} else {
		lg(fmt.Sprintf(".env file saved to %s", envFilePath))
		envFileDownloadURL := config.LEMCHTTPBaseURL + "dotenv"
		lg(fmt.Sprintf(`.env file available for download: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">dotenv</a>`, envFileDownloadURL))
	}

	return nil
}

// handleWindowsAuthentication handles RDP file generation and instructions for Windows instances
func handleWindowsAuthentication(config *Config, outputs map[string]struct {
	Value     interface{} `json:"value"`
	Type      string      `json:"type"`
	Sensitive bool        `json:"sensitive"`
}, publicIP string) error {
	// For GCP, we use Terraform-generated random password
	lg("Setting up Windows authentication using Terraform-generated password...")

	// Get the password from Terraform outputs (generated via random_password resource)

	// Get admin_password from Terraform outputs
	adminPasswordOutput, ok := outputs["admin_password"]
	if !ok || adminPasswordOutput.Value == nil {
		return fmt.Errorf("admin_password not found in Terraform outputs - ensure Windows VM is properly configured")
	}

	windowsPassword, ok := adminPasswordOutput.Value.(string)
	if !ok {
		return fmt.Errorf("admin_password in Terraform outputs is not a string")
	}

	if windowsPassword == "" {
		lg("Warning: Windows password is empty. The instance may still be initializing. Please wait a few minutes and try recreating the instance.")
		return fmt.Errorf("Windows password is not yet available")
	}

	// Determine admin username from Terraform outputs (fallback to Administrator)
	adminUsername := "Administrator"
	if au, ok := outputs["admin_username"]; ok && au.Value != nil {
		if s, ok := au.Value.(string); ok && s != "" {
			adminUsername = s
		}
	}

	lg(fmt.Sprintf("Using Terraform-generated password for account: %s...", adminUsername))

	// Create RDP file
	rdpFilename := "windows-connection.rdp"
	rdpPath := filepath.Join(config.PublicDir, rdpFilename)

	rdpContent := fmt.Sprintf(`full address:s:%s
username:s:%s
`, publicIP, adminUsername)

	lg(fmt.Sprintf("Creating RDP file at %s...", rdpPath))

	if err := os.WriteFile(rdpPath, []byte(rdpContent), 0644); err != nil {
		return fmt.Errorf("error writing RDP file: %v", err)
	}

	lg(fmt.Sprintf("RDP file created at %s", rdpPath))

	// Create credentials file
	credsFilename := "windows-credentials.txt"
	credsPath := filepath.Join(config.PublicDir, credsFilename)

	credsContent := fmt.Sprintf(`Windows Server Connection Details
================================

Server IP: %s
Username: %s
Password: %s

Instructions:
1. Download the RDP file
2. Open the RDP file with your Remote Desktop client
3. Use the username and password above when prompted

`, publicIP, adminUsername, windowsPassword)

	if err := os.WriteFile(credsPath, []byte(credsContent), 0600); err != nil {
		return fmt.Errorf("error writing credentials file: %v", err)
	}

	// Provide download links
	rdpDownloadURL := config.LEMCHTTPBaseURL + rdpFilename
	credsDownloadURL := config.LEMCHTTPBaseURL + credsFilename

	lg(fmt.Sprintf(`RDP Connection File: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">%s</a>`, rdpDownloadURL, rdpFilename))
	lg(fmt.Sprintf(`Windows Credentials: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">%s</a>`, credsDownloadURL, credsFilename))

	lg("Windows Server Details:<br>")
	lg(fmt.Sprintf("IP Address: <code>%s</code><br>", publicIP))
	lg(fmt.Sprintf("Username: <code>%s</code><br>", adminUsername))
	lg(fmt.Sprintf("Password: <code>%s</code><br>", windowsPassword))
	lg("IMPORTANT: Secure these credentials. They provide full administrative access to the Windows server.")

	// Create .env file
	lg("Creating .env file...")

	envContent := fmt.Sprintf(`
RDP_SERVER=%s
RDP_USERNAME=%s
RDP_PASSWORD=%s
GCP_INSTANCE_IP=%s
`, publicIP, adminUsername, windowsPassword, publicIP)

	envFilePath := filepath.Join(config.PublicDir, "dotenv")

	if err := os.WriteFile(envFilePath, []byte(envContent), 0644); err != nil {
		lg(fmt.Sprintf("Error writing dotenv file: %v", err))
	} else {
		lg(fmt.Sprintf(".env file saved to %s", envFilePath))
		envFileDownloadURL := config.LEMCHTTPBaseURL + "dotenv"
		lg(fmt.Sprintf(`.env file available for download: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">dotenv</a>`, envFileDownloadURL))
	}

	return nil
}

// resetWindowsPassword resets the Windows password using GCP's standard approach
// DEPRECATED: This function is no longer used. We now use Terraform-generated passwords via random_password resource.
//
//nolint:unused // Retained as a fallback for older Windows provisioning flows.
func resetWindowsPassword(config *Config, instanceName, zone, username string) (string, error) {
	lg("Resetting Windows password using gcloud compute reset-windows-password...")

	// Use gcloud compute reset-windows-password command - this is the standard GCP approach
	cmd := exec.Command("gcloud", "compute", "reset-windows-password", instanceName,
		"--zone", zone,
		"--project", config.ProjectID,
		"--user", username,
		"--format", "get(password)",
		"--quiet") // Don't prompt for confirmation

	output, err := cmd.Output()
	if err != nil {
		lg(fmt.Sprintf("Failed to reset Windows password: %v", err))
		if exitErr, ok := err.(*exec.ExitError); ok {
			lg(fmt.Sprintf("Command stderr: %s", string(exitErr.Stderr)))
		}
		return "", fmt.Errorf("failed to reset Windows password: %v", err)
	}

	password := strings.TrimSpace(string(output))
	if password == "" {
		return "", fmt.Errorf("gcloud returned empty password")
	}

	lg("Successfully reset Windows password!")
	return password, nil
}

// runDestroy executes the Terraform destroy operation using shared state files
func runDestroy(config *Config) error {
	// Initialize Terraform
	if err := TerraformInit(config.AbsoluteDestDir); err != nil {
		return fmt.Errorf("error initializing Terraform: %v", err)
	}

	if err := TerraformDestroy(config.AbsoluteDestDir); err != nil {
		return fmt.Errorf("error destroying Terraform resources: %v", err)
	}

	lg("Terraform destroy completed successfully.")
	return nil
}

// runNuke destroys ALL LEMC resources from the GCP project by scanning for LEMC labels
func runNuke(config *Config) error {
	lg("=== NUKE MODE: Destroying ALL LEMC resources from GCP project ===")
	lg("This will destroy ALL resources with LEMC labels, regardless of who created them!")

	// Set up gcloud authentication using service account credentials
	if err := setupGCloudAuth(config); err != nil {
		return fmt.Errorf("failed to setup gcloud authentication: %v", err)
	}

	// For nuke mode, we scan for ANY LEMC resources, not just for this specific user
	// This is more aggressive and will clean up orphaned resources from all LEMC users
	if err := nukeAllLEMCResources(config); err != nil {
		return fmt.Errorf("error nuking LEMC resources: %v", err)
	}

	lg("Nuke operation completed successfully.")
	return nil
}

// nukeAllLEMCResources finds and deletes ALL GCP resources with ANY LEMC labels
func nukeAllLEMCResources(config *Config) error {
	lg("Scanning for ALL GCP resources with LEMC labels across the entire project...")

	// For nuke mode, use a broader filter to catch ANY LEMC resources
	labelFilter := "labels.lemc_uuid:*"

	// Clean up resources in dependency order: instances -> firewalls -> subnets -> networks
	if err := deleteComputeInstances(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting compute instances: %v", err))
	}

	if err := deleteFirewallRules(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting firewall rules: %v", err))
	}

	if err := deleteSubnets(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting subnets: %v", err))
	}

	if err := deleteNetworks(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting networks: %v", err))
	}

	lg("NUKE operation completed - all LEMC resources have been destroyed.")
	return nil
}

// cleanupGCPResourcesByLabels finds and deletes GCP resources with LEMC labels
//
//nolint:unused // Retained for manual cleanup flows outside the default recipe buttons.
func cleanupGCPResourcesByLabels(config *Config) error {
	lg("Scanning for GCP resources with LEMC labels...")

	// Set up gcloud authentication using service account credentials
	if err := setupGCloudAuth(config); err != nil {
		return fmt.Errorf("failed to setup gcloud authentication: %v", err)
	}

	labelFilter := fmt.Sprintf("labels.lemc_uuid=%s AND labels.lemc_username=%s AND labels.lemc_user_id=%s",
		config.LEMCUUID, config.LEMCUsername, config.LEMCUserID)

	// Clean up resources in dependency order: instances -> firewalls -> subnets -> networks
	if err := deleteComputeInstances(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting compute instances: %v", err))
	}

	if err := deleteFirewallRules(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting firewall rules: %v", err))
	}

	if err := deleteSubnets(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting subnets: %v", err))
	}

	if err := deleteNetworks(config, labelFilter); err != nil {
		lg(fmt.Sprintf("Warning: Error deleting networks: %v", err))
	}

	return nil
}

// setupGCloudAuth configures gcloud to use the service account credentials
func setupGCloudAuth(config *Config) error {
	// Write credentials to temporary file
	credsFile := "/tmp/gcp-credentials.json"
	if err := os.WriteFile(credsFile, []byte(config.GoogleCredentials), 0600); err != nil {
		return fmt.Errorf("failed to write credentials file: %v", err)
	}

	// Activate service account
	authCmd := exec.Command("gcloud", "auth", "activate-service-account",
		"--key-file", credsFile, "--project", config.ProjectID)
	if output, err := authCmd.CombinedOutput(); err != nil {
		return fmt.Errorf("failed to activate service account: %v, output: %s", err, output)
	}

	lg("Successfully authenticated with GCP")
	return nil
}

// deleteComputeInstances finds and deletes compute instances with LEMC labels
func deleteComputeInstances(config *Config, labelFilter string) error {
	lg("Looking for compute instances to delete...")

	// List instances with LEMC labels
	listCmd := exec.Command("gcloud", "compute", "instances", "list",
		"--project", config.ProjectID,
		"--filter", labelFilter,
		"--format", "value(name,zone)")

	output, err := listCmd.Output()
	if err != nil {
		return fmt.Errorf("failed to list instances: %v", err)
	}

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

		parts := strings.Fields(line)
		if len(parts) != 2 {
			continue
		}

		instanceName, zone := parts[0], parts[1]
		lg(fmt.Sprintf("Deleting compute instance: %s in zone %s", instanceName, zone))

		deleteCmd := exec.Command("gcloud", "compute", "instances", "delete", instanceName,
			"--zone", zone, "--project", config.ProjectID, "--quiet")

		if output, err := deleteCmd.CombinedOutput(); err != nil {
			lg(fmt.Sprintf("Warning: Failed to delete instance %s: %v, output: %s", instanceName, err, output))
		} else {
			lg(fmt.Sprintf("Successfully deleted instance: %s", instanceName))
		}
	}

	return nil
}

// deleteFirewallRules finds and deletes firewall rules with names containing LEMC prefix
func deleteFirewallRules(config *Config, labelFilter string) error {
	lg("Looking for firewall rules to delete...")

	// Since firewall rules don't support labels, we'll search by name pattern
	nameFilter := fmt.Sprintf("name~lemc-%s-%s-%s", config.LEMCUUID[:8], config.LEMCUsername, config.LEMCUserID)

	listCmd := exec.Command("gcloud", "compute", "firewall-rules", "list",
		"--project", config.ProjectID,
		"--filter", nameFilter,
		"--format", "value(name)")

	output, err := listCmd.Output()
	if err != nil {
		return fmt.Errorf("failed to list firewall rules: %v", err)
	}

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

		ruleName := strings.TrimSpace(line)
		lg(fmt.Sprintf("Deleting firewall rule: %s", ruleName))

		deleteCmd := exec.Command("gcloud", "compute", "firewall-rules", "delete", ruleName,
			"--project", config.ProjectID, "--quiet")

		if output, err := deleteCmd.CombinedOutput(); err != nil {
			lg(fmt.Sprintf("Warning: Failed to delete firewall rule %s: %v, output: %s", ruleName, err, output))
		} else {
			lg(fmt.Sprintf("Successfully deleted firewall rule: %s", ruleName))
		}
	}

	return nil
}

// deleteSubnets finds and deletes subnets with names containing LEMC prefix
func deleteSubnets(config *Config, labelFilter string) error {
	lg("Looking for subnets to delete...")

	nameFilter := fmt.Sprintf("name~lemc-%s-%s-%s", config.LEMCUUID[:8], config.LEMCUsername, config.LEMCUserID)

	listCmd := exec.Command("gcloud", "compute", "networks", "subnets", "list",
		"--project", config.ProjectID,
		"--filter", nameFilter,
		"--format", "value(name,region)")

	output, err := listCmd.Output()
	if err != nil {
		return fmt.Errorf("failed to list subnets: %v", err)
	}

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

		parts := strings.Fields(line)
		if len(parts) != 2 {
			continue
		}

		subnetName, region := parts[0], parts[1]
		lg(fmt.Sprintf("Deleting subnet: %s in region %s", subnetName, region))

		deleteCmd := exec.Command("gcloud", "compute", "networks", "subnets", "delete", subnetName,
			"--region", region, "--project", config.ProjectID, "--quiet")

		if output, err := deleteCmd.CombinedOutput(); err != nil {
			lg(fmt.Sprintf("Warning: Failed to delete subnet %s: %v, output: %s", subnetName, err, output))
		} else {
			lg(fmt.Sprintf("Successfully deleted subnet: %s", subnetName))
		}
	}

	return nil
}

// deleteNetworks finds and deletes VPC networks with names containing LEMC prefix
func deleteNetworks(config *Config, labelFilter string) error {
	lg("Looking for VPC networks to delete...")

	nameFilter := fmt.Sprintf("name~lemc-%s-%s-%s", config.LEMCUUID[:8], config.LEMCUsername, config.LEMCUserID)

	listCmd := exec.Command("gcloud", "compute", "networks", "list",
		"--project", config.ProjectID,
		"--filter", nameFilter,
		"--format", "value(name)")

	output, err := listCmd.Output()
	if err != nil {
		return fmt.Errorf("failed to list networks: %v", err)
	}

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

		networkName := strings.TrimSpace(line)
		lg(fmt.Sprintf("Deleting VPC network: %s", networkName))

		deleteCmd := exec.Command("gcloud", "compute", "networks", "delete", networkName,
			"--project", config.ProjectID, "--quiet")

		if output, err := deleteCmd.CombinedOutput(); err != nil {
			lg(fmt.Sprintf("Warning: Failed to delete network %s: %v, output: %s", networkName, err, output))
		} else {
			lg(fmt.Sprintf("Successfully deleted network: %s", networkName))
		}
	}

	return nil
}

// findInstanceByLEMCTags searches for EC2 instances using LEMC resource tags
func findInstanceByLEMCTags(config *Config) (string, error) {
	lg("Searching for instances with LEMC tags...")
	lg(fmt.Sprintf("Looking for instances with resource prefix: %s", config.ResourcePrefix))

	// Create AWS CLI command to find instances with matching tags
	describeCmd := exec.Command("aws", "ec2", "describe-instances",
		"--region", config.AWSRegion,
		"--filters",
		fmt.Sprintf("Name=tag:Name,Values=%s", config.ResourcePrefix),
		fmt.Sprintf("Name=tag:LEMC_UUID,Values=%s", config.LEMCUUID),
		fmt.Sprintf("Name=tag:LEMC_Username,Values=%s", config.LEMCUsername),
		"Name=instance-state-name,Values=running,stopped", // Only running or stopped instances
		"--query", "Reservations[*].Instances[*].[InstanceId,State.Name,Tags[?Key=='Name'].Value|[0]]",
		"--output", "json")

	// Set AWS credentials as environment variables for the command
	describeCmd.Env = append(os.Environ(),
		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
		fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

	output, err := describeCmd.Output()
	if err != nil {
		return "", fmt.Errorf("error querying AWS for instances: %v", err)
	}

	// Parse the JSON output
	var reservations [][][]interface{}
	if err := json.Unmarshal(output, &reservations); err != nil {
		return "", fmt.Errorf("error parsing instance query results: %v", err)
	}

	// Find matching instances
	var matchingInstances []string
	for _, reservation := range reservations {
		for _, instance := range reservation {
			if len(instance) >= 3 {
				instanceID, ok1 := instance[0].(string)
				state, ok2 := instance[1].(string)
				name, ok3 := instance[2].(string)

				if ok1 && ok2 && ok3 {
					lg(fmt.Sprintf("Found instance: %s (state: %s, name: %s)", instanceID, state, name))
					if state == "running" || state == "stopped" {
						matchingInstances = append(matchingInstances, instanceID)
					}
				}
			}
		}
	}

	if len(matchingInstances) == 0 {
		return "", fmt.Errorf("no running or stopped instances found with LEMC tags (resource_prefix=%s, lemc_uuid=%s, lemc_user=%s)",
			config.ResourcePrefix, config.LEMCUUID, config.LEMCUsername)
	}

	if len(matchingInstances) > 1 {
		lg(fmt.Sprintf("Warning: Found %d matching instances. Using the first one: %s", len(matchingInstances), matchingInstances[0]))
		lg("If you want to use a specific instance, set SOURCE_INSTANCE_ID environment variable.")
		for i, instanceID := range matchingInstances {
			lg(fmt.Sprintf("  %d: %s", i+1, instanceID))
		}
	}

	return matchingInstances[0], nil
}

// shareAMIWithAccounts shares the AMI with the specified AWS account IDs
func shareAMIWithAccounts(config *Config, amiID string) error {
	lg(fmt.Sprintf("AMI sharing configuration: AMI_SHARE_ACCOUNTS environment variable contains %d account(s)", len(config.AMIShareAccounts)))

	if len(config.AMIShareAccounts) == 0 {
		lg("No accounts specified for AMI sharing. Skipping account sharing.")
		return nil
	}

	lg(fmt.Sprintf("Sharing AMI %s with %d account(s)...", amiID, len(config.AMIShareAccounts)))
	for i, account := range config.AMIShareAccounts {
		lg(fmt.Sprintf("  %d: %s", i+1, account))
	}

	for _, accountID := range config.AMIShareAccounts {
		lg(fmt.Sprintf("Sharing AMI with account: %s", accountID))

		shareCmd := exec.Command("aws", "ec2", "modify-image-attribute",
			"--image-id", amiID,
			"--launch-permission", fmt.Sprintf("Add=[{UserId=%s}]", accountID),
			"--region", config.AWSRegion)

		// Set AWS credentials as environment variables for the command
		shareCmd.Env = append(os.Environ(),
			fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
			fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
			fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

		output, err := shareCmd.CombinedOutput()
		if err != nil {
			lg(fmt.Sprintf("Warning: Failed to share AMI with account %s: %v", accountID, err))
			lg(fmt.Sprintf("AWS CLI output: %s", string(output)))
			lg("This may be due to invalid account ID or insufficient permissions.")
		} else {
			lg(fmt.Sprintf("Successfully shared AMI with account: %s", accountID))
			if len(output) > 0 {
				lg(fmt.Sprintf("AWS CLI output: %s", string(output)))
			}
		}
	}

	lg("AMI sharing process completed.")
	return nil
}

// waitForAMIAvailable waits for an AMI to become available
func waitForAMIAvailable(config *Config, amiID string) error {
	lg(fmt.Sprintf("Waiting for AMI %s to become available...", amiID))

	maxAttempts := 60 // Maximum 60 attempts (30 minutes with 30-second intervals)
	attempt := 0

	for attempt < maxAttempts {
		attempt++
		lg(fmt.Sprintf("Checking AMI status (attempt %d/%d)...", attempt, maxAttempts))

		// Check AMI status using AWS CLI
		statusCmd := exec.Command("aws", "ec2", "describe-images",
			"--image-ids", amiID,
			"--region", config.AWSRegion,
			"--query", "Images[0].State",
			"--output", "text")

		// Set AWS credentials as environment variables for the command
		statusCmd.Env = append(os.Environ(),
			fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
			fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
			fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

		output, err := statusCmd.Output()
		if err != nil {
			lg(fmt.Sprintf("Error checking AMI status: %v", err))
			if attempt >= maxAttempts {
				return fmt.Errorf("failed to check AMI status after %d attempts: %v", maxAttempts, err)
			}
			lg("Retrying in 30 seconds...")
			time.Sleep(30 * time.Second)
			continue
		}

		status := strings.TrimSpace(string(output))
		lg(fmt.Sprintf("AMI status: %s", status))

		switch status {
		case "available":
			lg(fmt.Sprintf("AMI %s is now available and ready for use!", amiID))
			return nil
		case "pending":
			lg("AMI is still pending, waiting 30 seconds before checking again...")
			time.Sleep(30 * time.Second)
		case "failed":
			return fmt.Errorf("AMI creation failed")
		default:
			lg(fmt.Sprintf("Unknown AMI status: %s, waiting 30 seconds...", status))
			time.Sleep(30 * time.Second)
		}
	}

	return fmt.Errorf("AMI did not become available within the timeout period (%d minutes)", maxAttempts/2)
}

// runAMI executes AMI creation from the current instance
func runAMI(config *Config) error {
	lg("Starting AMI creation process...")

	// Validate AMI name is provided
	if config.AMIName == "" {
		return fmt.Errorf("AMI_NAME environment variable is required for AMI creation")
	}

	// Validate AWS credentials are available
	if config.AWSAccessKeyID == "" || config.AWSSecretAccessKey == "" {
		return fmt.Errorf("AWS credentials (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) are required for AMI creation")
	}

	// Validate AWS region is provided
	if config.AWSRegion == "" {
		return fmt.Errorf("AWS_REGION environment variable is required for AMI creation")
	}

	var instanceID string

	// If SOURCE_INSTANCE_ID is provided, use it directly
	if config.SourceInstanceID != "" {
		instanceID = config.SourceInstanceID
		lg(fmt.Sprintf("Using provided source instance ID: %s", instanceID))
	} else {
		// Try to get current instance ID from metadata service (if running on EC2)
		lg("SOURCE_INSTANCE_ID not provided. Attempting to detect current instance...")
		metadataCmd := exec.Command("curl", "-s", "--connect-timeout", "3", "http://169.254.169.254/latest/meta-data/instance-id")
		instanceIDBytes, err := metadataCmd.Output()
		if err != nil {
			lg("Could not detect current instance from metadata service.")
			lg("Attempting to find instance using LEMC resource tags...")

			// Try to find instance using AWS API with LEMC tags
			foundInstanceID, err := findInstanceByLEMCTags(config)
			if err != nil {
				lg("Could not find instance using LEMC tags.")
				lg("Please provide SOURCE_INSTANCE_ID environment variable with the instance ID you want to create an AMI from.")
				lg("Example: SOURCE_INSTANCE_ID=i-1234567890abcdef0")
				return fmt.Errorf("SOURCE_INSTANCE_ID is required when not running on EC2 instance and no matching instance found: %v", err)
			}
			instanceID = foundInstanceID
			lg(fmt.Sprintf("Found instance using LEMC tags: %s", instanceID))
		} else {
			instanceID = strings.TrimSpace(string(instanceIDBytes))
			if instanceID == "" || len(instanceID) < 10 {
				lg("Error: Retrieved instance ID appears to be invalid or empty.")
				lg("Please provide SOURCE_INSTANCE_ID environment variable.")
				return fmt.Errorf("invalid instance ID detected: %s", instanceID)
			}
			lg(fmt.Sprintf("Detected current instance ID: %s", instanceID))
		}
	}

	// Validate instance ID format
	if !strings.HasPrefix(instanceID, "i-") || len(instanceID) < 10 {
		return fmt.Errorf("invalid instance ID format: %s (should be like i-1234567890abcdef0)", instanceID)
	}

	// Verify instance exists using AWS API
	lg(fmt.Sprintf("Verifying instance %s exists and is accessible...", instanceID))
	describeCmd := exec.Command("aws", "ec2", "describe-instances",
		"--instance-ids", instanceID,
		"--region", config.AWSRegion,
		"--query", "Reservations[0].Instances[0].State.Name",
		"--output", "text")

	// Set AWS credentials as environment variables for the command
	describeCmd.Env = append(os.Environ(),
		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
		fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

	stateOutput, err := describeCmd.Output()
	if err != nil {
		lg("Error: Unable to access instance information.")
		lg("Please verify:")
		lg("1. Instance ID is correct")
		lg("2. AWS credentials have EC2 permissions")
		lg("3. Instance exists in the specified region")
		return fmt.Errorf("error verifying instance %s: %v", instanceID, err)
	}

	instanceState := strings.TrimSpace(string(stateOutput))
	lg(fmt.Sprintf("Instance %s state: %s", instanceID, instanceState))

	if instanceState == "terminated" {
		return fmt.Errorf("cannot create AMI from terminated instance %s", instanceID)
	}

	// Prepare AMI tags
	tags := map[string]string{
		"Name":                        config.AMIName,
		"LEMC_UUID":                   config.LEMCUUID,
		"LEMC_SCOPE":                  config.LEMCScope,
		"LEMC_USERNAME":               config.LEMCUsername,
		"LEMC_USER_ID":                config.LEMCUserID,
		"LEMC_RECIPE_NAME":            config.LEMCRecipeName,
		"LEMC_PAGE_ID":                config.LEMCPageID,
		"LEMC_STEP_ID":                config.LEMCStepID,
		"LEMC_HTTP_DOWNLOAD_BASE_URL": config.LEMCHTTPBaseURL,
		"CreatedBy":                   "LEMC",
		"ResourcePrefix":              config.ResourcePrefix,
		"SourceInstanceId":            instanceID,
	}

	// Build tag specifications for AWS CLI
	var tagSpecs []string
	for key, value := range tags {
		tagSpecs = append(tagSpecs, fmt.Sprintf("Key=%s,Value=%s", key, value))
	}

	// Create AMI
	lg(fmt.Sprintf("Creating AMI '%s' from instance %s...", config.AMIName, instanceID))

	createCmd := exec.Command("aws", "ec2", "create-image",
		"--instance-id", instanceID,
		"--name", config.AMIName,
		"--description", fmt.Sprintf("AMI created by LEMC for %s from instance %s", config.LEMCUsername, instanceID),
		"--no-reboot",
		"--region", config.AWSRegion)

	// Set AWS credentials as environment variables for the command
	createCmd.Env = append(os.Environ(),
		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
		fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

	createOutput, err := createCmd.Output()
	if err != nil {
		return fmt.Errorf("error creating AMI: %v", err)
	}

	// Parse AMI ID from output
	var result map[string]interface{}
	if err := json.Unmarshal(createOutput, &result); err != nil {
		return fmt.Errorf("error parsing AMI creation output: %v", err)
	}

	amiID, ok := result["ImageId"].(string)
	if !ok {
		return fmt.Errorf("AMI ID not found in creation output")
	}

	lg(fmt.Sprintf("AMI creation initiated. AMI ID: %s", amiID))

	// Wait for AMI to become available before proceeding with tagging and sharing
	if err := waitForAMIAvailable(config, amiID); err != nil {
		return fmt.Errorf("error waiting for AMI to become available: %v", err)
	}

	// Tag the AMI
	lg("Applying tags to the AMI...")
	tagCmd := exec.Command("aws", "ec2", "create-tags",
		"--resources", amiID,
		"--region", config.AWSRegion,
		"--tags")

	// Add all tag specifications
	tagCmd.Args = append(tagCmd.Args, tagSpecs...)

	tagCmd.Env = append(os.Environ(),
		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", config.AWSAccessKeyID),
		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", config.AWSSecretAccessKey),
		fmt.Sprintf("AWS_DEFAULT_REGION=%s", config.AWSRegion))

	if err := tagCmd.Run(); err != nil {
		lg(fmt.Sprintf("Warning: Error applying tags to AMI: %v", err))
	} else {
		lg("Successfully applied tags to AMI")
	}

	// Save AMI information to public directory
	amiInfo := fmt.Sprintf(`AMI Creation Summary
====================
AMI Name: %s
AMI ID: %s
Source Instance ID: %s
Instance State: %s
Region: %s
Created By: %s (%s)
UUID: %s
Scope: %s
Recipe: %s
Resource Prefix: %s

Tags Applied:
`, config.AMIName, amiID, instanceID, instanceState, config.AWSRegion, config.LEMCUsername, config.LEMCUserID, config.LEMCUUID, config.LEMCScope, config.LEMCRecipeName, config.ResourcePrefix)

	for key, value := range tags {
		amiInfo += fmt.Sprintf("  %s: %s\n", key, value)
	}

	// Add shared accounts information
	if len(config.AMIShareAccounts) > 0 {
		amiInfo += "\nShared with AWS Accounts:\n"
		for _, accountID := range config.AMIShareAccounts {
			amiInfo += fmt.Sprintf("  %s\n", accountID)
		}
	} else {
		amiInfo += "\nShared with AWS Accounts: None (AMI is private)\n"
	}

	amiInfoPath := filepath.Join(config.PublicDir, "ami-info.txt")
	if err := os.WriteFile(amiInfoPath, []byte(amiInfo), 0644); err != nil {
		lg(fmt.Sprintf("Warning: Error writing AMI info file: %v", err))
	} else {
		lg(fmt.Sprintf("AMI information saved to %s", amiInfoPath))
		downloadURL := config.LEMCHTTPBaseURL + "ami-info.txt"
		lg(fmt.Sprintf(`AMI Info available for download: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">ami-info.txt</a>`, downloadURL))
	}

	// Share AMI with specified accounts
	if err := shareAMIWithAccounts(config, amiID); err != nil {
		lg(fmt.Sprintf("Warning: Error sharing AMI with accounts: %v", err))
	}

	lg(fmt.Sprintf("AMI creation completed successfully. AMI ID: %s", amiID))
	lg("Note: AMI creation is an asynchronous process. It may take several minutes to complete.")
	lg(fmt.Sprintf("You can monitor progress with: aws ec2 describe-images --image-ids %s --region %s", amiID, config.AWSRegion))

	return nil
}

// RunTerraformFunction executes the specified Terraform function
func RunTerraformFunction(config *Config, function TerraformFunction) error {
	switch function {
	case FunctionApply:
		return runApply(config)
	case FunctionDestroy:
		return runDestroy(config)
	case FunctionNuke:
		return runNuke(config)
	case FunctionAMI:
		return runAMI(config)
	case FunctionImage:
		return runGCPMachineImage(config)
	case FunctionListImages:
		return runListGCPImages(config)
	default:
		return fmt.Errorf("unsupported Terraform function: %s", function)
	}
}

type gcpImageDescribe struct {
	Name              string `json:"name"`
	Status            string `json:"status"`
	SelfLink          string `json:"selfLink"`
	CreationTimestamp string `json:"creationTimestamp"`
	SourceDisk        string `json:"sourceDisk"`
	DiskSizeGb        string `json:"diskSizeGb"`
}

func describeGCPImage(config *Config, imageName string) ([]byte, gcpImageDescribe, error) {
	descCmd := exec.Command(
		"gcloud", "compute", "images", "describe", imageName,
		"--project", config.ProjectID,
		"--format", "json",
	)
	descOut, err := descCmd.Output()
	if err != nil {
		return nil, gcpImageDescribe{}, err
	}
	var desc gcpImageDescribe
	if err := json.Unmarshal(descOut, &desc); err != nil {
		return descOut, gcpImageDescribe{}, err
	}
	return descOut, desc, nil
}

func writeGCPImageInfoFiles(config *Config, instanceName, zone string, desc gcpImageDescribe, descOut []byte) {
	status := desc.Status
	if status == "" {
		status = "UNKNOWN"
	}
	created := desc.CreationTimestamp
	if created == "" {
		created = time.Now().Format(time.RFC3339)
	}
	diskSize := desc.DiskSizeGb
	if diskSize == "" {
		diskSize = "unknown"
	}

	title := "GCP Disk Image Created"
	info := fmt.Sprintf(`%s
%s

Result: SUCCESS
Name: %s
Project: %s
Status: %s
Source Instance: %s
Zone: %s
Disk Size GB: %s
Created: %s

Use this image name as the Custom Image value for future VM builds.
`,
		title,
		strings.Repeat("=", len(title)),
		config.ImageName,
		config.ProjectID,
		status,
		instanceName,
		zone,
		diskSize,
		created,
	)
	infoPath := filepath.Join(config.PublicDir, "image-info.txt")
	if err := os.WriteFile(infoPath, []byte(info), 0644); err != nil {
		lg(fmt.Sprintf("Warning: could not write image info file: %v", err))
	} else {
		downloadURL := config.LEMCHTTPBaseURL + "image-info.txt"
		lg(fmt.Sprintf(`Summary file: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">image-info.txt</a>`, downloadURL))
	}

	if len(descOut) == 0 {
		return
	}
	detailsPath := filepath.Join(config.PublicDir, "image-details.json")
	if err := os.WriteFile(detailsPath, descOut, 0644); err != nil {
		lg(fmt.Sprintf("Warning: could not write image details file: %v", err))
		return
	}
	detailsURL := config.LEMCHTTPBaseURL + "image-details.json"
	lg(fmt.Sprintf(`Detailed JSON: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">image-details.json</a>`, detailsURL))
}

func writeGCPImageErrorFile(config *Config, err error) {
	if config == nil || config.PublicDir == "" || err == nil {
		return
	}
	title := "GCP Disk Image Failed"
	info := fmt.Sprintf(`%s
%s

Result: ERROR
Name: %s
Project: %s
Error: %s
Time: %s

The source VM was not intentionally destroyed by this image step.
`,
		title,
		strings.Repeat("=", len(title)),
		config.ImageName,
		config.ProjectID,
		err.Error(),
		time.Now().Format(time.RFC3339),
	)
	infoPath := filepath.Join(config.PublicDir, "image-error.txt")
	if writeErr := os.WriteFile(infoPath, []byte(info), 0644); writeErr != nil {
		lg(fmt.Sprintf("Warning: could not write image error file: %v", writeErr))
		return
	}
	downloadURL := config.LEMCHTTPBaseURL + "image-error.txt"
	lg(fmt.Sprintf(`Error summary: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">image-error.txt</a>`, downloadURL))
}

func printGCPImageFailure(config *Config, err error) {
	lg("ERROR: GCP disk image was not created.")
	if config != nil && config.ImageName != "" {
		lg(fmt.Sprintf("Image name: %s", config.ImageName))
	}
	lg(fmt.Sprintf("Reason: %s", err))
	writeGCPImageErrorFile(config, err)
}

func printGCPImageSuccess(config *Config, instanceName, zone string, desc gcpImageDescribe) {
	status := desc.Status
	if status == "" {
		status = "UNKNOWN"
	}
	lg("SUCCESS: GCP disk image is ready.")
	lg(fmt.Sprintf("Image name: %s", config.ImageName))
	lg(fmt.Sprintf("Project: %s", config.ProjectID))
	lg(fmt.Sprintf("Status: %s", status))
	lg(fmt.Sprintf("Source VM: %s (%s)", instanceName, zone))
	lg("Use this image name as the Custom Image value for future VM builds.")
}

type listedGCPImage struct {
	Name              string            `json:"name"`
	Status            string            `json:"status"`
	CreationTimestamp string            `json:"creationTimestamp"`
	DiskSizeGb        string            `json:"diskSizeGb"`
	SelfLink          string            `json:"selfLink"`
	Labels            map[string]string `json:"labels"`
}

func firstNonEmpty(values ...string) string {
	for _, v := range values {
		if strings.TrimSpace(v) != "" {
			return strings.TrimSpace(v)
		}
	}
	return ""
}

func combinedCommaEnv(keys ...string) string {
	values := []string{}
	for _, key := range keys {
		if value := strings.TrimSpace(os.Getenv(key)); value != "" {
			values = append(values, value)
		}
	}
	return strings.Join(values, ",")
}

func parseResourceTags(raw, username string) (map[string]string, []string) {
	labels := map[string]string{}
	networkTags := []string{}
	addNetworkTag := func(rawTag string) {
		tag := sanitizeGCPNetworkTag(rawTag)
		if tag != "" {
			networkTags = append(networkTags, tag)
		}
	}

	for _, part := range strings.Split(raw, ",") {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}

		if key, value, ok := strings.Cut(part, "="); ok {
			key = strings.TrimSpace(key)
			value = strings.TrimSpace(value)
			labelKey := sanitizeGCPLabelKey(key)
			labelValue := sanitizeGCPLabelValue(value)
			if labelKey != "" && labelValue != "" {
				labels[labelKey] = labelValue
			}
			addNetworkTag(key + "-" + value)
			continue
		}

		labelKey := sanitizeGCPLabelKey("tag_" + sanitizeGCPLabelKey(part))
		if labelKey != "" {
			labels[labelKey] = "true"
		}
		addNetworkTag(part)
	}

	if username = strings.TrimSpace(username); username != "" {
		addNetworkTag("lemc-user-" + username)
	}

	return labels, uniqueSortedStrings(networkTags)
}

func uniqueSortedStrings(values []string) []string {
	seen := map[string]struct{}{}
	unique := []string{}
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		unique = append(unique, value)
	}
	sort.Strings(unique)
	return unique
}

func terraformStringMap(values map[string]string) string {
	if len(values) == 0 {
		return "{}"
	}

	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	var b strings.Builder
	b.WriteString("{\n")
	for _, key := range keys {
		b.WriteString(fmt.Sprintf("  %s = %s\n", strconv.Quote(key), strconv.Quote(values[key])))
	}
	b.WriteString("}")
	return b.String()
}

func terraformStringList(values []string) string {
	if len(values) == 0 {
		return "[]"
	}

	quoted := make([]string, 0, len(values))
	for _, value := range values {
		quoted = append(quoted, strconv.Quote(value))
	}
	return "[" + strings.Join(quoted, ", ") + "]"
}

func formatLabelMap(labels map[string]string) string {
	if len(labels) == 0 {
		return ""
	}

	parts := make([]string, 0, len(labels))
	for key, value := range labels {
		if key == "" || value == "" {
			continue
		}
		parts = append(parts, fmt.Sprintf("%s=%s", key, value))
	}
	sort.Strings(parts)
	return strings.Join(parts, ", ")
}

func parseBoolString(raw string) (bool, bool) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "true", "1", "yes", "y", "on":
		return true, true
	case "false", "0", "no", "n", "off":
		return false, true
	default:
		return false, false
	}
}

func normalizeSSHPort(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return strconv.Itoa(defaultSSHPort), nil
	}

	port, err := strconv.Atoi(raw)
	if err != nil {
		return "", fmt.Errorf("SSH_PORT must be numeric, got %q", raw)
	}
	if port < minCustomSSHPort || port > maxSSHPort {
		return "", fmt.Errorf("SSH_PORT must be blank for port 22, or a custom port from %d through %d", minCustomSSHPort, maxSSHPort)
	}
	return strconv.Itoa(port), nil
}

func normalizeBootDiskSize(raw string, isWindowsImage bool) int {
	minDiskSize := minLinuxBootDiskSizeGB
	imageKind := "selected image"
	if isWindowsImage {
		minDiskSize = minWindowsBootDiskSizeGB
		imageKind = "selected Windows image"
	}

	raw = strings.TrimSpace(raw)
	if raw == "" {
		return minDiskSize
	}
	size, err := strconv.Atoi(raw)
	if err != nil {
		lg(fmt.Sprintf("Warning: invalid DISK_SIZE value %q. Using %d GB.", raw, minDiskSize))
		return minDiskSize
	}
	if size < minDiskSize {
		lg(fmt.Sprintf("DISK_SIZE=%d GB is below the minimum for the %s. Using %d GB.", size, imageKind, minDiskSize))
		return minDiskSize
	}
	return size
}

func inferMinCPUPlatform(machineType string) string {
	machineType = strings.ToLower(strings.TrimSpace(machineType))
	switch {
	case strings.HasPrefix(machineType, "n2-"):
		return "Intel Cascade Lake"
	case strings.HasPrefix(machineType, "n1-"), strings.HasPrefix(machineType, "custom-"):
		return "Intel Haswell"
	default:
		return ""
	}
}

func gcpImageUsernameFilter(config *Config) (string, error) {
	username := sanitizeGCPLabelValue(config.LEMCUsername)
	if username == "" {
		return "", fmt.Errorf("LEMC_USERNAME is required to list images by username label")
	}
	return fmt.Sprintf("labels.lemc_username=%s", username), nil
}

func writeGCPImageListFile(config *Config, text string) {
	if config == nil || config.PublicDir == "" {
		return
	}
	infoPath := filepath.Join(config.PublicDir, "image-list.txt")
	if err := os.WriteFile(infoPath, []byte(text), 0644); err != nil {
		lg(fmt.Sprintf("Warning: could not write image list file: %v", err))
		return
	}
	downloadURL := config.LEMCHTTPBaseURL + "image-list.txt"
	lg(fmt.Sprintf(`Image list: <a href="%s" target="_blank" style="color: blue; text-decoration: none;">image-list.txt</a>`, downloadURL))
}

func filterListedGCPImagesByRecipe(images []listedGCPImage, recipeFilter string) []listedGCPImage {
	recipeFilter = sanitizeGCPLabelValue(strings.ToLower(strings.TrimSpace(recipeFilter)))
	if recipeFilter == "" {
		return images
	}

	filtered := make([]listedGCPImage, 0, len(images))
	for _, img := range images {
		imageRecipe := sanitizeGCPLabelValue(strings.ToLower(strings.TrimSpace(img.Labels["image_recipe"])))
		// Keep legacy images that predate recipe provenance labels so old saved images do not disappear.
		if imageRecipe == "" || imageRecipe == recipeFilter {
			filtered = append(filtered, img)
		}
	}
	return filtered
}

func runListGCPImages(config *Config) error {
	lg("Listing GCP images for this LEMC username...")

	if err := setupGCloudAuth(config); err != nil {
		return fmt.Errorf("failed to authenticate to GCP: %v", err)
	}

	filter, err := gcpImageUsernameFilter(config)
	if err != nil {
		return err
	}
	log.Printf("Listing GCP images with filter: %s", filter)
	cmd := exec.Command(
		"gcloud", "compute", "images", "list",
		"--project", config.ProjectID,
		"--filter", filter,
		"--sort-by", "~creationTimestamp",
		"--format", "json",
	)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to list GCP images: %v (output: %s)", err, compactCommandOutput(out))
	}

	var images []listedGCPImage
	if err := json.Unmarshal(out, &images); err != nil {
		return fmt.Errorf("failed to parse GCP image list: %v", err)
	}
	recipeFilter := strings.TrimSpace(config.ImageRecipeFilter)
	images = filterListedGCPImagesByRecipe(images, recipeFilter)
	sort.SliceStable(images, func(i, j int) bool {
		return images[i].CreationTimestamp > images[j].CreationTimestamp
	})

	title := "GCP Images For This Username"
	var b strings.Builder
	b.WriteString(title + "\n")
	b.WriteString(strings.Repeat("=", len(title)) + "\n\n")
	b.WriteString(fmt.Sprintf("Project: %s\n", config.ProjectID))
	b.WriteString(fmt.Sprintf("Username Label: %s\n", sanitizeGCPLabelValue(config.LEMCUsername)))
	b.WriteString(fmt.Sprintf("Filter: %s\n\n", filter))
	if recipeFilter != "" {
		b.WriteString(fmt.Sprintf("Image Recipe Filter: %s\n\n", recipeFilter))
	}

	if len(images) == 0 {
		b.WriteString("No images found for this username label.\n")
		lg("No images found for this username label.")
		writeGCPImageListFile(config, b.String())
		return nil
	}

	lg(fmt.Sprintf("Found %d image(s) for username label %s.", len(images), sanitizeGCPLabelValue(config.LEMCUsername)))
	for i, img := range images {
		labels := img.Labels
		sourceRecipe := firstNonEmpty(labels["lemc_recipe"], labels["lemc_recipe_name"], "unknown")
		imageRecipe := firstNonEmpty(labels["image_recipe"], "unknown")
		sourceInstance := firstNonEmpty(labels["source_instance"], "unknown")
		sourceZone := firstNonEmpty(labels["source_zone"], "unknown")
		status := firstNonEmpty(img.Status, "UNKNOWN")
		diskSize := firstNonEmpty(img.DiskSizeGb, "unknown")
		created := firstNonEmpty(img.CreationTimestamp, "unknown")
		labelSummary := firstNonEmpty(formatLabelMap(labels), "none")

		lg(fmt.Sprintf("%d. %s | %s | source recipe: %s | created: %s | labels: %s", i+1, img.Name, status, sourceRecipe, created, labelSummary))
		b.WriteString(fmt.Sprintf("%d. %s\n", i+1, img.Name))
		b.WriteString(fmt.Sprintf("   Status: %s\n", status))
		b.WriteString(fmt.Sprintf("   Source Recipe: %s\n", sourceRecipe))
		b.WriteString(fmt.Sprintf("   Image Recipe: %s\n", imageRecipe))
		b.WriteString(fmt.Sprintf("   Source VM: %s\n", sourceInstance))
		b.WriteString(fmt.Sprintf("   Source Zone: %s\n", sourceZone))
		b.WriteString(fmt.Sprintf("   Disk Size GB: %s\n", diskSize))
		b.WriteString(fmt.Sprintf("   Created: %s\n", created))
		b.WriteString(fmt.Sprintf("   Labels: %s\n", labelSummary))
		if img.SelfLink != "" {
			b.WriteString(fmt.Sprintf("   Self Link: %s\n", img.SelfLink))
		}
		b.WriteString("\n")
	}
	b.WriteString("Use the image Name value with launch-from-machine-image.\n")
	writeGCPImageListFile(config, b.String())
	return nil
}

// runGCPMachineImage creates a standard GCP disk image from the provisioned instance's boot disk
func runGCPMachineImage(config *Config) (err error) {
	lg("Starting GCP disk image creation.")
	lg("This can take several minutes. If GCP requires it, the source VM will be stopped briefly and restarted automatically.")
	defer func() {
		if err != nil {
			printGCPImageFailure(config, err)
		}
	}()

	if config.ImageName == "" {
		return fmt.Errorf("IMAGE_NAME environment variable is required for image creation")
	}

	// Authenticate to GCP
	if err := setupGCloudAuth(config); err != nil {
		return fmt.Errorf("failed to authenticate to GCP: %v", err)
	}

	var instanceName, zone string

	// If explicit overrides provided, prefer those
	if config.SourceInstanceName != "" && config.SourceInstanceZone != "" {
		instanceName = config.SourceInstanceName
		zone = config.SourceInstanceZone
		lg(fmt.Sprintf("Using provided SOURCE_INSTANCE_NAME=%s and SOURCE_INSTANCE_ZONE=%s", instanceName, zone))
	} else {
		// Try Terraform outputs first
		outputs, err := GetTerraformOutputs(config.AbsoluteDestDir)
		if err == nil {
			if instOut, ok := outputs["instance_name"]; ok && instOut.Value != nil {
				if v, ok := instOut.Value.(string); ok {
					instanceName = v
				}
			}
			if zoneOut, ok := outputs["zone"]; ok && zoneOut.Value != nil {
				if v, ok := zoneOut.Value.(string); ok {
					zone = v
				}
			}
		} else {
			lg(fmt.Sprintf("Warning: could not read Terraform outputs: %v", err))
		}

		// If still missing, try label-based discovery via gcloud
		if instanceName == "" || zone == "" {
			lg("Finding source VM by LEMC labels...")
			n, z, err := findGCPInstanceByLEMCLables(config)
			if err != nil {
				return fmt.Errorf("unable to determine source instance: %v", err)
			}
			instanceName, zone = n, z
		}
	}

	if detectedWindows, reason, err := detectGCPInstanceOS(config, instanceName, zone); err != nil {
		lg(fmt.Sprintf("Warning: unable to automatically detect instance OS: %v", err))
	} else {
		config.IsWindowsDetected = true

		osLabel := "Linux"
		if detectedWindows {
			osLabel = "Windows"
		}
		if reason != "" {
			lg(fmt.Sprintf("Detected %s OS on source instance (%s).", osLabel, reason))
		} else {
			lg(fmt.Sprintf("Detected %s OS on source instance.", osLabel))
		}

		if config.IsWindowsOverride && detectedWindows != config.IsWindows {
			overrideLabel := "Windows"
			if !config.IsWindows {
				overrideLabel = "Linux"
			}
			lg(fmt.Sprintf("IS_WINDOWS override is set; proceeding with %s workflow despite detection.", overrideLabel))
		} else {
			config.IsWindows = detectedWindows
		}
	}

	lg(fmt.Sprintf("Creating image '%s' from source VM '%s' in %s...", config.ImageName, instanceName, zone))

	// Build description and derive labels from instance, mirroring AWS tag flow
	description := fmt.Sprintf("LEMC image: uuid=%s scope=%s user=%s user_id=%s", strings.ToLower(config.LEMCUUID), strings.ToLower(config.LEMCScope), strings.ToLower(config.LEMCUsername), strings.ToLower(config.LEMCUserID))

	// Fetch labels from the source instance and merge with LEMC context
	instLabels, err := getGCPInstanceLabels(config, instanceName, zone)
	if err != nil {
		lg(fmt.Sprintf("Warning: could not fetch instance labels; proceeding with LEMC defaults: %v", err))
		instLabels = map[string]string{}
	}
	for key, value := range config.ResourceLabels {
		if key != "" && value != "" {
			instLabels[key] = value
		}
	}
	// Ensure core LEMC labels exist (in case instance labels are missing)
	instLabels["lemc_uuid"] = strings.ToLower(config.LEMCUUID)
	instLabels["lemc_scope"] = strings.ToLower(config.LEMCScope)
	instLabels["lemc_username"] = strings.ToLower(config.LEMCUsername)
	instLabels["lemc_user_id"] = strings.ToLower(config.LEMCUserID)
	if config.LEMCRecipeName != "" && instLabels["lemc_recipe"] == "" {
		instLabels["lemc_recipe"] = strings.ToLower(config.LEMCRecipeName)
	}
	if config.LEMCPageID != "" && instLabels["lemc_page_id"] == "" {
		instLabels["lemc_page_id"] = strings.ToLower(config.LEMCPageID)
	}
	// Add helpful provenance labels
	instLabels["source_instance"] = strings.ToLower(instanceName)
	instLabels["source_zone"] = strings.ToLower(zone)
	instLabels["created_by"] = "lemc"
	if config.LEMCRecipeName != "" {
		instLabels["image_recipe"] = strings.ToLower(config.LEMCRecipeName)
	}
	instLabels["resource_prefix"] = strings.ToLower(config.ResourcePrefix)

	labels := buildGCPLabelFlag(instLabels)
	if labels != "" {
		log.Printf("Derived labels to apply to image %s: %s", config.ImageName, labels)
	}

	// Create the disk image from the instance's boot disk
	if err := createGCPDiskImageFromInstance(config, config.ImageName, instanceName, zone, description, labels); err != nil {
		return err
	}

	// Describe the created image for details
	descOut, imageDesc, err := describeGCPImage(config, config.ImageName)
	if err != nil {
		lg(fmt.Sprintf("Warning: failed to describe disk image '%s': %v", config.ImageName, err))
	}

	// Optional: share the image with specified members
	if err := shareGCPImageWithMembers(config, config.ImageName); err != nil {
		lg(fmt.Sprintf("Warning: error sharing image: %v", err))
	}

	writeGCPImageInfoFiles(config, instanceName, zone, imageDesc, descOut)
	printGCPImageSuccess(config, instanceName, zone, imageDesc)
	return nil
}

// getGCPInstanceLabels obtains the labels from a GCE instance
func getGCPInstanceLabels(config *Config, instanceName, zone string) (map[string]string, error) {
	cmd := exec.Command(
		"gcloud", "compute", "instances", "describe", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--format", "json",
	)
	out, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to describe instance for labels: %v", err)
	}
	var desc struct {
		Labels map[string]string `json:"labels"`
	}
	if err := json.Unmarshal(out, &desc); err != nil {
		return nil, fmt.Errorf("failed to parse instance labels: %v", err)
	}
	if desc.Labels == nil {
		return map[string]string{}, nil
	}
	// Normalize to lowercase keys/values and strip invalid characters
	norm := map[string]string{}
	for k, v := range desc.Labels {
		nk := sanitizeGCPLabelKey(strings.ToLower(k))
		nv := sanitizeGCPLabelValue(strings.ToLower(v))
		if nk != "" && nv != "" {
			norm[nk] = nv
		}
	}
	return norm, nil
}

// detectGCPInstanceOS inspects an instance and attempts to determine if it is Windows-based.
// Returns true when a Windows signal is found, along with a short reason string.
func detectGCPInstanceOS(config *Config, instanceName, zone string) (bool, string, error) {
	cmd := exec.Command(
		"gcloud", "compute", "instances", "describe", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--format", "json",
	)
	out, err := cmd.Output()
	if err != nil {
		return false, "", fmt.Errorf("failed to describe instance for OS detection: %v", err)
	}

	var desc struct {
		Disks []struct {
			Licenses        []string `json:"licenses"`
			GuestOsFeatures []struct {
				Type string `json:"type"`
			} `json:"guestOsFeatures"`
		} `json:"disks"`
		Metadata struct {
			Items []struct {
				Key   string `json:"key"`
				Value string `json:"value"`
			} `json:"items"`
		} `json:"metadata"`
	}

	if err := json.Unmarshal(out, &desc); err != nil {
		return false, "", fmt.Errorf("failed to parse instance description for OS detection: %v", err)
	}

	for _, disk := range desc.Disks {
		for _, license := range disk.Licenses {
			if strings.Contains(strings.ToLower(license), "windows") {
				return true, fmt.Sprintf("license %s", license), nil
			}
		}
		for _, feature := range disk.GuestOsFeatures {
			if strings.EqualFold(feature.Type, "WINDOWS") {
				return true, "guestOsFeatures WINDOWS", nil
			}
		}
	}

	for _, item := range desc.Metadata.Items {
		key := strings.ToLower(item.Key)
		if strings.Contains(key, "windows") || strings.Contains(key, "sysprep") {
			return true, fmt.Sprintf("metadata key %s", item.Key), nil
		}
	}

	return false, "", nil
}

// buildGCPLabelFlag converts a label map to gcloud --labels string "k1=v1,k2=v2"
func buildGCPLabelFlag(labels map[string]string) string {
	parts := make([]string, 0, len(labels))
	for k, v := range labels {
		key := sanitizeGCPLabelKey(k)
		value := sanitizeGCPLabelValue(v)
		if key == "" || value == "" {
			continue
		}
		parts = append(parts, fmt.Sprintf("%s=%s", key, value))
	}
	sort.Strings(parts)
	return strings.Join(parts, ",")
}

// sanitizeGCPLabelKey ensures only allowed characters for label keys
func sanitizeGCPLabelKey(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	var b strings.Builder
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { // conservative set
			b.WriteRune(r)
		} else {
			b.WriteRune('-')
		}
	}
	out := strings.Trim(b.String(), "-")
	if out == "" {
		return ""
	}
	// Must start with a letter: prepend 'a' if needed
	if out[0] < 'a' || out[0] > 'z' {
		out = "a" + out
	}
	if len(out) > 63 {
		out = out[:63]
	}
	return out
}

// sanitizeGCPLabelValue ensures only allowed characters for label values
func sanitizeGCPLabelValue(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	var b strings.Builder
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { // conservative set
			b.WriteRune(r)
		} else {
			b.WriteRune('-')
		}
	}
	out := strings.Trim(b.String(), "-")
	if len(out) > 63 {
		out = out[:63]
	}
	return out
}

func sanitizeGCPNetworkTag(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	var b strings.Builder
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
			b.WriteRune(r)
		} else if r == '_' || r == ' ' || r == '=' {
			b.WriteRune('-')
		} else {
			b.WriteRune('-')
		}
	}

	out := strings.Trim(b.String(), "-")
	if out == "" {
		return ""
	}
	if out[0] < 'a' || out[0] > 'z' {
		out = "t" + out
	}
	if len(out) > 63 {
		out = strings.TrimRight(out[:63], "-")
	}
	out = strings.TrimRight(out, "-")
	return out
}

// isDiskInUseError returns true when gcloud reports that a disk is still attached to an instance
func isDiskInUseError(output string) bool {
	lower := strings.ToLower(output)
	phrases := []string{
		"is already being used by",
		"is currently in use by",
		"resourceinusebyanotherresource",
		"attached to instance",
		"must be stopped before creating",
		"please stop the instance",
	}
	for _, phrase := range phrases {
		if strings.Contains(lower, phrase) {
			return true
		}
	}
	return false
}

type gcloudAttempt struct {
	Name   string
	Err    error
	Output string
}

func compactCommandOutput(out []byte) string {
	msg := strings.TrimSpace(string(out))
	if msg == "" {
		return "(no command output)"
	}
	if len(msg) > 600 {
		msg = msg[:600] + "...(truncated)"
	}
	return msg
}

func recordGCloudAttempt(attempts *[]gcloudAttempt, name string, out []byte, err error) {
	attempt := gcloudAttempt{Name: name, Err: err, Output: compactCommandOutput(out)}
	*attempts = append(*attempts, attempt)
	log.Printf("GCP image creation attempt failed: name=%q err=%v output=%s", attempt.Name, attempt.Err, attempt.Output)
}

func lastGCloudAttemptSummary(attempts []gcloudAttempt) string {
	if len(attempts) == 0 {
		return "no gcloud output was captured"
	}
	last := attempts[len(attempts)-1]
	return fmt.Sprintf("%s failed: %v; output: %s", last.Name, last.Err, last.Output)
}

// ensureInstanceStopped stops the instance and waits for it to reach TERMINATED
func ensureInstanceStopped(config *Config, instanceName, zone string) error {
	lg("Stopping source VM before image creation...")
	stop := exec.Command(
		"gcloud", "compute", "instances", "stop", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--quiet",
	)
	if out, err := stop.CombinedOutput(); err != nil {
		return fmt.Errorf("failed to stop instance: %v (output: %s)", err, strings.TrimSpace(string(out)))
	}
	if err := waitForInstanceStatus(config, instanceName, zone, "TERMINATED", 60, 30*time.Second); err != nil {
		return err
	}
	return nil
}

// ensureInstanceStarted starts the instance and waits for it to reach RUNNING
func ensureInstanceStarted(config *Config, instanceName, zone string) error {
	lg("Restarting source VM...")
	start := exec.Command(
		"gcloud", "compute", "instances", "start", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--quiet",
	)
	if out, err := start.CombinedOutput(); err != nil {
		return fmt.Errorf("failed to start instance: %v (output: %s)", err, strings.TrimSpace(string(out)))
	}
	if err := waitForInstanceStatus(config, instanceName, zone, "RUNNING", 60, 30*time.Second); err != nil {
		return err
	}
	lg(fmt.Sprintf("Instance %s is back to RUNNING.", instanceName))
	return nil
}

// createGCPBetaMachineImage tries using the beta gcloud surface
//
//nolint:unused // Retained for future machine-image fallback support.
func createGCPBetaMachineImage(config *Config, name, sourceInstancePath, description string) error {
	lg("Trying beta machine image creation...")
	cmd := exec.Command(
		"gcloud", "beta", "compute", "machine-images", "create", name,
		fmt.Sprintf("--source-instance=%s", sourceInstancePath),
		fmt.Sprintf("--project=%s", config.ProjectID),
		fmt.Sprintf("--description=%s", description),
	)
	if out, err := cmd.CombinedOutput(); err != nil {
		lg(fmt.Sprintf("gcloud beta output: %s", string(out)))
		return err
	}
	return nil
}

// createGCPDiskImageFromInstance creates a standard disk image from the instance's boot disk
func createGCPDiskImageFromInstance(config *Config, name, instanceName, zone, description string, labels string) (err error) {
	lg("Creating image from the source VM boot disk...")
	desc := exec.Command(
		"gcloud", "compute", "instances", "describe", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--format", "value(disks[0].source)",
	)
	diskURLBytes, err := desc.Output()
	if err != nil {
		return fmt.Errorf("failed to describe instance to find boot disk: %v", err)
	}
	diskURL := strings.TrimSpace(string(diskURLBytes))
	if diskURL == "" {
		return fmt.Errorf("could not determine boot disk from instance description")
	}
	parts := strings.Split(diskURL, "/")
	diskName := parts[len(parts)-1]
	log.Printf("Detected boot disk for image %s: name=%s self_link=%s", name, diskName, diskURL)

	instanceStopped := false
	attempts := []gcloudAttempt{}
	defer func() {
		if !instanceStopped {
			return
		}
		if err == nil {
			lg("Image creation completed; restarting the source VM.")
		} else {
			lg("Image creation did not finish cleanly; attempting to restart the source VM before exiting.")
		}
		if errStart := ensureInstanceStarted(config, instanceName, zone); errStart != nil {
			lg(fmt.Sprintf("Warning: failed to restart instance automatically: %v", errStart))
		}
	}()

	isWindows := config.IsWindows
	if !isWindows && !config.IsWindowsOverride && !config.IsWindowsDetected {
		if strings.Contains(strings.ToLower(config.Image), "windows") {
			isWindows = true
		}
	}
	if isWindows {
		if config.WindowsSysprep {
			if err := runWindowsSysprep(config, instanceName, zone); err != nil {
				return fmt.Errorf("failed to sysprep Windows instance prior to imaging: %v", err)
			}
			instanceStopped = true
		} else {
			lg("Windows source detected and WINDOWS_SYSPREP disabled; stopping the source VM before imaging...")
			if err := ensureInstanceStopped(config, instanceName, zone); err != nil {
				lg(fmt.Sprintf("Warning: error stopping instance: %v", err))
			} else {
				instanceStopped = true
			}
		}
	}

	addCommonImageFlags := func(args []string) []string {
		if config.ImageFamily != "" {
			args = append(args, fmt.Sprintf("--family=%s", config.ImageFamily))
		}
		if config.ImageStorageLocation != "" {
			args = append(args, fmt.Sprintf("--storage-location=%s", config.ImageStorageLocation))
		}
		return args
	}

	tryDirect := func() (bool, error) {
		diskInUse := false

		args1 := []string{"compute", "images", "create", name,
			fmt.Sprintf("--source-disk=%s", diskURL),
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--description=%s", description),
		}
		if labels != "" {
			args1 = append(args1, fmt.Sprintf("--labels=%s", labels))
		}
		args1 = addCommonImageFlags(args1)
		cmd1 := exec.Command("gcloud", args1...)
		if out1, err1 := cmd1.CombinedOutput(); err1 != nil {
			msg1 := string(out1)
			recordGCloudAttempt(&attempts, "direct create with disk self link and labels", out1, err1)
			if isDiskInUseError(msg1) {
				diskInUse = true
			}
		} else {
			return false, nil
		}

		args2 := []string{"compute", "images", "create", name,
			fmt.Sprintf("--source-disk=%s", diskURL),
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--description=%s", description),
		}
		args2 = addCommonImageFlags(args2)
		cmd2 := exec.Command("gcloud", args2...)
		if out2, err2 := cmd2.CombinedOutput(); err2 != nil {
			msg2 := string(out2)
			recordGCloudAttempt(&attempts, "direct create with disk self link", out2, err2)
			if isDiskInUseError(msg2) {
				diskInUse = true
			}
		} else {
			add := exec.Command(
				"gcloud", "compute", "images", "add-labels", name,
				fmt.Sprintf("--project=%s", config.ProjectID),
				fmt.Sprintf("--labels=%s", labels),
			)
			if out5, err5 := add.CombinedOutput(); err5 != nil {
				lg(fmt.Sprintf("Warning: failed to add labels to image: %v", err5))
				lg(fmt.Sprintf("gcloud images add-labels output: %s", string(out5)))
			}
			return false, nil
		}

		args3 := []string{"compute", "images", "create", name,
			fmt.Sprintf("--source-disk=%s", diskName),
			fmt.Sprintf("--source-disk-zone=%s", zone),
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--description=%s", description),
		}
		if labels != "" {
			args3 = append(args3, fmt.Sprintf("--labels=%s", labels))
		}
		args3 = addCommonImageFlags(args3)
		cmd3 := exec.Command("gcloud", args3...)
		if out3, err3 := cmd3.CombinedOutput(); err3 != nil {
			msg3 := string(out3)
			recordGCloudAttempt(&attempts, "direct create with disk name, zone, and labels", out3, err3)
			if isDiskInUseError(msg3) {
				diskInUse = true
			}
		} else {
			return false, nil
		}

		args4 := []string{"compute", "images", "create", name,
			fmt.Sprintf("--source-disk=%s", diskName),
			fmt.Sprintf("--source-disk-zone=%s", zone),
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--description=%s", description),
		}
		args4 = addCommonImageFlags(args4)
		cmd4 := exec.Command("gcloud", args4...)
		if out4, err4 := cmd4.CombinedOutput(); err4 != nil {
			msg4 := string(out4)
			recordGCloudAttempt(&attempts, "direct create with disk name and zone", out4, err4)
			if isDiskInUseError(msg4) {
				diskInUse = true
			}
			return diskInUse, fmt.Errorf("gcloud compute images create failed: %s", lastGCloudAttemptSummary(attempts))
		}

		add := exec.Command(
			"gcloud", "compute", "images", "add-labels", name,
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--labels=%s", labels),
		)
		if out5, err5 := add.CombinedOutput(); err5 != nil {
			lg(fmt.Sprintf("Warning: failed to add labels to image: %v", err5))
			lg(fmt.Sprintf("gcloud images add-labels output: %s", string(out5)))
		}
		return false, nil
	}

	for {
		diskInUse, err := tryDirect()
		if err == nil {
			return nil
		}
		if diskInUse && !instanceStopped {
			lg("GCP requires the source VM to be stopped before imaging. Stopping it now; it will be restarted automatically.")
			lg("Retrying after the VM stops. This can take several minutes.")
			if errStop := ensureInstanceStopped(config, instanceName, zone); errStop != nil {
				lg(fmt.Sprintf("Warning: could not stop instance prior to retry: %v", errStop))
			} else {
				instanceStopped = true
				continue
			}
		}
		lg("Direct image creation did not complete; trying snapshot-based image creation.")
		break
	}

	snapName := fmt.Sprintf("%s-snap-%s", name, time.Now().Format("20060102-150405"))
	lg(fmt.Sprintf("Creating snapshot %s from disk %s in zone %s. This can take several minutes — please wait…", snapName, diskName, zone))
	snapArgs := []string{"compute", "disks", "snapshot", diskName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		fmt.Sprintf("--snapshot-names=%s", snapName),
	}
	if config.IsWindows || strings.Contains(strings.ToLower(config.Image), "windows") {
		snapArgs = append(snapArgs, "--guest-flush")
	}
	snapCmd := exec.Command("gcloud", snapArgs...)
	if outS, errS := snapCmd.CombinedOutput(); errS != nil {
		lg(fmt.Sprintf("gcloud disks snapshot output: %s", string(outS)))
		return fmt.Errorf("failed to create snapshot for image: %v", errS)
	}

	if err := waitForGCPSnapshotReady(config, snapName); err != nil {
		return fmt.Errorf("snapshot did not become READY: %v", err)
	}

	lg(fmt.Sprintf("Creating image %s from snapshot %s. This may also take several minutes — please wait…", name, snapName))
	argsImg := []string{"compute", "images", "create", name,
		fmt.Sprintf("--source-snapshot=%s", snapName),
		fmt.Sprintf("--project=%s", config.ProjectID),
		fmt.Sprintf("--description=%s", description),
	}
	if labels != "" {
		argsImg = append(argsImg, fmt.Sprintf("--labels=%s", labels))
	}
	argsImg = addCommonImageFlags(argsImg)
	imgFromSnap := exec.Command("gcloud", argsImg...)
	if outI, errI := imgFromSnap.CombinedOutput(); errI != nil {
		recordGCloudAttempt(&attempts, "snapshot create with labels", outI, errI)
		argsImg2 := []string{"compute", "images", "create", name,
			fmt.Sprintf("--source-snapshot=%s", snapName),
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--description=%s", description),
		}
		argsImg2 = addCommonImageFlags(argsImg2)
		imgFromSnap2 := exec.Command("gcloud", argsImg2...)
		if outI2, errI2 := imgFromSnap2.CombinedOutput(); errI2 != nil {
			recordGCloudAttempt(&attempts, "snapshot create without labels", outI2, errI2)
			return fmt.Errorf("failed to create image from snapshot: %s", lastGCloudAttemptSummary(attempts))
		}
		add := exec.Command(
			"gcloud", "compute", "images", "add-labels", name,
			fmt.Sprintf("--project=%s", config.ProjectID),
			fmt.Sprintf("--labels=%s", labels),
		)
		if outL, errL := add.CombinedOutput(); errL != nil {
			lg(fmt.Sprintf("Warning: failed to add labels to image: %v", errL))
			lg(fmt.Sprintf("gcloud images add-labels output: %s", string(outL)))
		}
	}
	return nil
}

// waitForGCPMachineImageReady polls gcloud until machine image status is READY or times out
//
//nolint:unused // Retained for future machine-image fallback support.
func waitForGCPMachineImageReady(config *Config, imageName string) error {
	lg(fmt.Sprintf("Waiting for machine image %s to become READY...", imageName))
	maxAttempts := 60
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		cmd := exec.Command(
			"gcloud", "compute", "machine-images", "describe", imageName,
			"--project", config.ProjectID,
			"--format", "value(status)",
		)
		out, err := cmd.Output()
		if err != nil {
			lg(fmt.Sprintf("Attempt %d: error describing machine image: %v", attempt, err))
		} else {
			status := strings.TrimSpace(string(out))
			if status != "" {
				lg(fmt.Sprintf("Machine image status: %s", status))
			}
			if strings.EqualFold(status, "READY") {
				lg("Machine image is READY.")
				return nil
			}
			if strings.EqualFold(status, "FAILED") {
				return fmt.Errorf("machine image status FAILED")
			}
		}
		if attempt < maxAttempts {
			time.Sleep(30 * time.Second)
		}
	}
	return fmt.Errorf("machine image did not become READY within the timeout period")
}

// waitForGCPSnapshotReady polls until the snapshot is READY
func waitForGCPSnapshotReady(config *Config, snapshotName string) error {
	lg(fmt.Sprintf("Waiting for snapshot %s to become READY...", snapshotName))
	maxAttempts := 60
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		cmd := exec.Command(
			"gcloud", "compute", "snapshots", "describe", snapshotName,
			fmt.Sprintf("--project=%s", config.ProjectID),
			"--format", "value(status)",
		)
		out, err := cmd.Output()
		if err != nil {
			lg(fmt.Sprintf("Attempt %d: error describing snapshot: %v", attempt, err))
		} else {
			status := strings.TrimSpace(string(out))
			if status != "" {
				lg(fmt.Sprintf("Snapshot status: %s", status))
			}
			if strings.EqualFold(status, "READY") {
				lg("Snapshot is READY.")
				return nil
			}
			if strings.EqualFold(status, "FAILED") {
				return fmt.Errorf("snapshot status FAILED")
			}
		}
		if attempt < maxAttempts {
			time.Sleep(30 * time.Second)
		}
	}
	return fmt.Errorf("snapshot did not become READY within the timeout period")
}

// waitForInstanceStatus polls until the instance reaches the desired status (e.g., RUNNING or TERMINATED)
func waitForInstanceStatus(config *Config, instanceName, zone, desired string, maxAttempts int, sleep time.Duration) error {
	lg(fmt.Sprintf("Waiting for source VM to reach %s...", desired))
	desired = strings.ToUpper(desired)
	lastStatus := ""
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		cmd := exec.Command(
			"gcloud", "compute", "instances", "describe", instanceName,
			fmt.Sprintf("--zone=%s", zone),
			fmt.Sprintf("--project=%s", config.ProjectID),
			"--format", "value(status)",
		)
		out, err := cmd.Output()
		if err != nil {
			log.Printf("Attempt %d: error describing instance %s: %v", attempt, instanceName, err)
		} else {
			status := strings.ToUpper(strings.TrimSpace(string(out)))
			if status != "" && status != lastStatus {
				lg(fmt.Sprintf("Source VM status: %s", status))
				lastStatus = status
			}
			if status == desired {
				lg(fmt.Sprintf("Source VM reached %s.", desired))
				return nil
			}
			if status == "TERMINATED" && desired == "STOPPED" {
				// Treat TERMINATED as STOPPED for GCE
				lg("Source VM is TERMINATED.")
				return nil
			}
		}
		if attempt < maxAttempts {
			time.Sleep(sleep)
		}
	}
	return fmt.Errorf("instance did not reach status %s within timeout", desired)
}

// runWindowsSysprep attempts to invoke GCESysprep on the Windows VM and waits for it to shut down
func runWindowsSysprep(config *Config, instanceName, zone string) error {
	lg("Preparing Windows instance for imaging: scheduling GCESysprep via startup script (this will shut down the VM)...")

	// Add a one-time startup script to run GCESysprep on next boot
	// Note: This replaces any existing windows-startup-script-ps1. Safe for imaging flow.
	addMeta := exec.Command(
		"gcloud", "compute", "instances", "add-metadata", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--metadata", "windows-startup-script-ps1=GCESysprep",
	)
	if out, err := addMeta.CombinedOutput(); err != nil {
		lg(fmt.Sprintf("Error adding GCESysprep startup metadata: %v", err))
		lg(fmt.Sprintf("gcloud output: %s", string(out)))
		return fmt.Errorf("failed to add GCESysprep startup metadata: %v", err)
	}

	// Reboot the instance so the startup script runs
	lg("Rebooting instance to run GCESysprep startup script...")
	reset := exec.Command(
		"gcloud", "compute", "instances", "reset", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--quiet",
	)
	if out, err := reset.CombinedOutput(); err != nil {
		lg(fmt.Sprintf("Warning: could not reset instance: %v", err))
		lg(fmt.Sprintf("gcloud output: %s", string(out)))
		// Try start if reset fails (instance might be stopped already)
		start := exec.Command(
			"gcloud", "compute", "instances", "start", instanceName,
			fmt.Sprintf("--zone=%s", zone),
			fmt.Sprintf("--project=%s", config.ProjectID),
			"--quiet",
		)
		if out2, err2 := start.CombinedOutput(); err2 != nil {
			lg(fmt.Sprintf("Error starting instance to run GCESysprep: %v", err2))
			lg(fmt.Sprintf("gcloud output: %s", string(out2)))
			return fmt.Errorf("failed to reset/start instance for GCESysprep")
		}
	}

	// Wait up to 30 minutes for TERMINATED state
	if err := waitForInstanceStatus(config, instanceName, zone, "TERMINATED", 60, 30*time.Second); err != nil {
		return fmt.Errorf("Windows sysprep did not complete: %v", err)
	}

	// Clean up metadata so instance won't sysprep again if ever started
	rmMeta := exec.Command(
		"gcloud", "compute", "instances", "remove-metadata", instanceName,
		fmt.Sprintf("--zone=%s", zone),
		fmt.Sprintf("--project=%s", config.ProjectID),
		"--keys", "windows-startup-script-ps1",
	)
	if out, err := rmMeta.CombinedOutput(); err != nil {
		lg(fmt.Sprintf("Warning: could not remove sysprep startup metadata (safe to ignore): %v", err))
		lg(fmt.Sprintf("gcloud output: %s", string(out)))
	}

	lg("Windows sysprep completed and instance is stopped.")
	return nil
}

// findGCPInstanceByLEMCLables finds a GCP instance by LEMC labels
func findGCPInstanceByLEMCLables(config *Config) (string, string, error) {
	lg("Searching for GCP instance by LEMC labels...")
	filter := fmt.Sprintf("labels.lemc_uuid=%s AND labels.lemc_username=%s AND labels.lemc_user_id=%s AND labels.lemc_scope=%s",
		config.LEMCUUID, config.LEMCUsername, config.LEMCUserID, config.LEMCScope)

	listCmd := exec.Command(
		"gcloud", "compute", "instances", "list",
		"--project", config.ProjectID,
		"--filter", filter,
		"--format", "value(name,zone)",
	)
	out, err := listCmd.Output()
	if err != nil {
		return "", "", fmt.Errorf("failed to list instances: %v", err)
	}
	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	var candidates [][2]string
	for _, line := range lines {
		if strings.TrimSpace(line) == "" {
			continue
		}
		parts := strings.Fields(line)
		if len(parts) == 2 {
			candidates = append(candidates, [2]string{parts[0], parts[1]})
		}
	}
	if len(candidates) == 0 {
		return "", "", fmt.Errorf("no instances found with LEMC labels")
	}
	if len(candidates) > 1 {
		lg(fmt.Sprintf("Warning: found %d matching instances. Using the first one: %s %s", len(candidates), candidates[0][0], candidates[0][1]))
		for i, c := range candidates {
			lg(fmt.Sprintf("  %d: %s %s", i+1, c[0], c[1]))
		}
	}
	return candidates[0][0], candidates[0][1], nil
}

// shareGCPImageWithMembers grants IAM bindings on the image to specified members
func shareGCPImageWithMembers(config *Config, imageName string) error {
	if len(config.ImageShareMembers) == 0 {
		lg("No image sharing requested; the image remains private.")
		return nil
	}
	lg(fmt.Sprintf("Sharing image %s with %d member(s)...", imageName, len(config.ImageShareMembers)))
	for _, member := range config.ImageShareMembers {
		lg(fmt.Sprintf("Granting roles/compute.imageUser to %s", member))
		cmd := exec.Command(
			"gcloud", "compute", "images", "add-iam-policy-binding", imageName,
			"--project", config.ProjectID,
			"--member", member,
			"--role", "roles/compute.imageUser",
		)
		if output, err := cmd.CombinedOutput(); err != nil {
			lg(fmt.Sprintf("Warning: failed to add IAM policy binding for %s: %v", member, err))
			lg(fmt.Sprintf("gcloud output: %s", string(output)))
		}
	}
	return nil
}

// (machine image IAM sharing helper removed; images are used instead)

func main() {
	tfFunction := TerraformFunction(os.Getenv("TERRAFORM_FUNCTION"))
	if tfFunction == "" {
		tfFunction = FunctionApply // Default to apply if not specified
	}

	skipTerraformPrep := tfFunction == FunctionImage || tfFunction == FunctionListImages

	if skipTerraformPrep {
		lt("Preparing GCP image workflow...")
	} else {
		lt("Starting Terraform setup...")
	}

	// Get configuration
	config, err := GetConfig()
	if err != nil {
		failLEMC("Error getting configuration: %v", err)
	}

	lg(fmt.Sprintf("Running in directory: %s", config.WorkingDir))

	if skipTerraformPrep {
		lg("Skipping Terraform configuration sync because this function does not update Terraform state.")
	} else {
		lg("Starting Terraform setup...")

		// Copy files from source directory
		lg(fmt.Sprintf("Copying files from %s to %s...", config.AbsoluteSourceDir, config.AbsoluteDestDir))
		filesToCopy := []string{"main.tf", "outputs.tf", "variables.tf", "configure.sh"}
		for _, filename := range filesToCopy {
			srcPath := filepath.Join(config.AbsoluteSourceDir, filename)
			dstPath := filepath.Join(config.AbsoluteDestDir, filename)
			lg(fmt.Sprintf("  Copying %s to %s", srcPath, dstPath))
			if err := copyFile(srcPath, dstPath); err != nil {
				failLEMC("Error copying file %s: %v", filename, err)
			}
		}
		lg("Finished copying base Terraform files.")

		// Generate terraform.tfvars content
		lg("Generating terraform.tfvars content...")
		tfVarsContent := config.GenerateTerraformVars()

		// Write terraform.tfvars file
		tfVarsPath := filepath.Join(config.AbsoluteDestDir, tfVarsFilename)
		lg(fmt.Sprintf("Writing %s...", tfVarsPath))
		err = os.WriteFile(tfVarsPath, []byte(tfVarsContent), 0644)
		if err != nil {
			failLEMC("Error writing %s: %v", tfVarsPath, err)
		}

		lg(fmt.Sprintf("Successfully created %s", tfVarsPath))
	}

	// Run the specified Terraform function
	if err := RunTerraformFunction(config, tfFunction); err != nil {
		failLEMC("Error running Terraform function: %v", err)
	}
}

func copyFile(src, dst string) error {
	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
		return err
	}
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	destFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destFile.Close()

	_, err = io.Copy(destFile, sourceFile)
	return err
}
