package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

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

// GCPCredentials represents the structure of a Google Cloud Platform service account key
type GCPCredentials struct {
	Type                    string `json:"type"`
	ProjectID               string `json:"project_id"`
	PrivateKeyID            string `json:"private_key_id"`
	PrivateKey              string `json:"private_key"`
	ClientEmail             string `json:"client_email"`
	ClientID                string `json:"client_id"`
	AuthURI                 string `json:"auth_uri"`
	TokenURI                string `json:"token_uri"`
	AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
	ClientX509CertURL       string `json:"client_x509_cert_url"`
}

var (
	sourceDir = "terraform-config"
)

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

const (
	FunctionApply   TerraformFunction = "apply"
	FunctionDestroy TerraformFunction = "destroy"
	publicDir                         = "/lemc/public"
	destDir                           = "/lemc/private"
	tfVarsFilename                    = "terraform.tfvars"
)

// Config holds all the configuration values needed for the application
type Config struct {
	// Directory Configuration
	SourceDir         string
	PublicDir         string
	DestDir           string
	WorkingDir        string
	AbsoluteSourceDir string
	AbsoluteDestDir   string

	// Terraform Configuration
	TerraformFunction string

	// GCP Configuration
	GCPProjectID   string
	GCPRegion      string
	GCPZone        string
	GCPCredentials []byte
	GCPKeyFilePath string

	// LEMC Configuration
	LEMCUUID        string
	LEMCScope       string
	LEMCUsername    string
	LEMCUserID      string
	LEMCMachineType string
	LEMCHTTPBaseURL string

	// Domain Configuration
	RootDomain     string
	RootZone       string
	DomainName     string
	ResourcePrefix string

	// VM Configuration
	ImageName             string
	Port                  string
	BootDiskSizeGB        string
	BackendTimeoutSeconds string

	// Existing SSL Certificate
	ExistingSSLCertificateName string
}

// GetConfig validates and returns the application configuration from environment variables
func GetConfig() (*Config, error) {
	config := &Config{}

	// Set up directory configuration
	config.SourceDir = "terraform-config"
	config.PublicDir = "/lemc/public"
	config.DestDir = "/lemc/private"

	// Get current working directory
	cwd, err := os.Getwd()
	if err != nil {
		return nil, fmt.Errorf("error getting current working directory: %v", err)
	}
	config.WorkingDir = cwd

	// Resolve absolute paths
	if !filepath.IsAbs(config.SourceDir) {
		config.AbsoluteSourceDir = filepath.Join(cwd, config.SourceDir)
	} else {
		config.AbsoluteSourceDir = config.SourceDir
	}

	if !filepath.IsAbs(config.DestDir) {
		config.AbsoluteDestDir = filepath.Join(cwd, config.DestDir)
	} else {
		config.AbsoluteDestDir = config.DestDir
	}

	// 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)
	}

	// GCP Configuration
	config.GCPKeyFilePath = os.Getenv("GCP_KEY_FILE")
	googleCredsEnv := os.Getenv("GOOGLE_CREDENTIALS")
	if googleCredsEnv != "" {
		config.GCPCredentials = []byte(googleCredsEnv)
		// Parse the credentials JSON to extract project_id
		var creds GCPCredentials
		if err := json.Unmarshal([]byte(googleCredsEnv), &creds); err != nil {
			return nil, fmt.Errorf("error parsing GOOGLE_CREDENTIALS JSON: %v", err)
		}
		if creds.ProjectID != "" {
			config.GCPProjectID = creds.ProjectID
			lg(fmt.Sprintf("Using project_id from GOOGLE_CREDENTIALS: %s", config.GCPProjectID))
		}
	}
	// Only use environment variable if not set from credentials
	if config.GCPProjectID == "" {
		config.GCPProjectID = os.Getenv("GCP_PROJECT_ID")
	}
	config.GCPRegion = os.Getenv("GCP_REGION")
	config.GCPZone = os.Getenv("GCP_ZONE")

	// LEMC Configuration
	config.LEMCUUID = os.Getenv("LEMC_UUID")
	if config.LEMCUUID == "" {
		config.LEMCUUID = "default-uuid"
	}
	config.LEMCScope = os.Getenv("LEMC_SCOPE")
	config.LEMCUsername = os.Getenv("LEMC_USERNAME")
	config.LEMCUserID = os.Getenv("LEMC_USER_ID")
	config.LEMCMachineType = os.Getenv("MACHINE_TYPE")
	config.LEMCHTTPBaseURL = os.Getenv("LEMC_HTTP_DOWNLOAD_BASE_URL")

	// Domain Configuration
	config.RootDomain = os.Getenv("ROOT_DOMAIN")
	config.RootZone = os.Getenv("ROOT_ZONE")

	// VM Configuration
	config.ImageName = os.Getenv("AMI")
	config.Port = os.Getenv("PORT_FROM")
	config.BootDiskSizeGB = os.Getenv("BOOT_DISK_SIZE_GB")
	config.BackendTimeoutSeconds = os.Getenv("BACKEND_TIMEOUT_SECONDS")
	if config.BackendTimeoutSeconds == "" {
		config.BackendTimeoutSeconds = "7200" // Default to 7200 seconds
	}

	// Existing SSL Certificate
	config.ExistingSSLCertificateName = os.Getenv("EXISTING_SSL_CERTIFICATE_NAME")

	// Validate required fields
	if config.LEMCHTTPBaseURL == "" {
		return nil, fmt.Errorf("LEMC_HTTP_DOWNLOAD_BASE_URL environment variable is required")
	}

	// Validate LEMCMachineType only if TERRAFORM_FUNCTION is "apply"
	terraformFunction := os.Getenv("TERRAFORM_FUNCTION")
	if (terraformFunction == string(FunctionApply) || terraformFunction == "") && config.LEMCMachineType == "" { // Default to apply if not specified
		return nil, fmt.Errorf("E_TYPE environment variable is required when TERRAFORM_FUNCTION is 'apply'")
	}

	// Construct resource prefix
	uuidPrefix := config.LEMCUUID
	if len(uuidPrefix) >= 8 {
		uuidPrefix = uuidPrefix[:8]
	}

	scopePrefix := strings.ToLower(config.LEMCScope)
	if scopePrefix == "individual" {
		scopePrefix = "ind"
	} else if scopePrefix == "shared" {
		scopePrefix = "shd"
	}

	config.ResourcePrefix = fmt.Sprintf("lemc-%s-%s-%s-%s",
		uuidPrefix,
		config.LEMCUsername,
		config.LEMCUserID,
		scopePrefix)

	// Construct domain name if possible
	if config.RootDomain != "" && config.ResourcePrefix != "" {
		config.DomainName = fmt.Sprintf("%s.%s", config.ResourcePrefix, config.RootDomain)
	} else {
		config.DomainName = "default.example.com" // Fallback
	}

	// Ensure LEMC_HTTP_DOWNLOAD_BASE_URL ends with a slash
	if !strings.HasSuffix(config.LEMCHTTPBaseURL, "/") {
		config.LEMCHTTPBaseURL += "/"
	}

	return config, nil
}

// Copies a single file from src to dst.
func copyFile(src, dst string) error {
	sourceFileStat, err := os.Stat(src)
	if err != nil {
		return err
	}

	if !sourceFileStat.Mode().IsRegular() {
		return fmt.Errorf("%s is not a regular file", src)
	}

	source, err := os.Open(src)
	if err != nil {
		return err
	}
	defer source.Close()

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

	_, err = io.Copy(destination, source)
	return err
}

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)
}

// 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_PROJECT_ID": "gcp_project_id",
		"GCP_REGION":     "gcp_region",
		"GCP_ZONE":       "gcp_zone",
		"LEMC_SCOPE":     "lemc_scope",
		"LEMC_USERNAME":  "lemc_username",
		"LEMC_USER_ID":   "lemc_user_id",
		"PORT_FROM":      "port",
	}

	// Add mapped variables
	for envKey, tfKey := range varMap {
		var value string
		// Prioritize project_id from key file
		if envKey == "GCP_PROJECT_ID" && c.GCPProjectID != "" {
			value = c.GCPProjectID
			lg(fmt.Sprintf("Using GCP_PROJECT_ID from key file for tfvar %s: %s", tfKey, value))
		} else {
			value = os.Getenv(envKey)
		}

		if value != "" {
			content.WriteString(fmt.Sprintf("%s = \"%s\"\n", tfKey, value))
		} else {
			// Log warning only if not GCP_PROJECT_ID (which might be intentionally overridden by key)
			// or if it's GCP_PROJECT_ID but was not found in key either.
			if envKey != "GCP_PROJECT_ID" || (envKey == "GCP_PROJECT_ID" && c.GCPProjectID == "") {
				lg(fmt.Sprintf("Warning: Environment variable %s not found, and not overridden by key file.", 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))

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

	// Add boot disk size if specified
	if c.BootDiskSizeGB != "" {
		content.WriteString(fmt.Sprintf("boot_disk_size_gb = %s\n", c.BootDiskSizeGB))
	} else {
		lg("Warning: BOOT_DISK_SIZE_GB not found in environment. Terraform will use its default.")
	}

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

	// Add domain name
	content.WriteString(fmt.Sprintf("domain_name = \"%s\"\n", c.DomainName))

	// Add DNS zone name
	if c.RootZone != "" {
		content.WriteString(fmt.Sprintf("dns_zone_name = \"%s\"\n", c.RootZone))
	} else {
		lg("Warning: ROOT_ZONE not found in environment, required for dns_zone_name.")
		content.WriteString(fmt.Sprintf("dns_zone_name = \"%s\"\n", "default-zone-name")) // Placeholder
	}

	// Add backend timeout
	content.WriteString(fmt.Sprintf("backend_timeout_seconds = %s\n", c.BackendTimeoutSeconds))

	// Add existing SSL certificate name if provided
	if c.ExistingSSLCertificateName != "" {
		content.WriteString(fmt.Sprintf("existing_ssl_certificate_name = \"%s\"\n", c.ExistingSSLCertificateName))
	} else {
		// If not provided, Terraform will expect it to be an empty string or not set, which is fine as it's optional there.
		// We could also choose to not write the variable at all if empty.
		lg("Warning: EXISTING_SSL_CERTIFICATE_NAME not provided. If you intend to use an existing certificate, ensure this variable is set in Terraform or the environment.")
	}

	return content.String()
}

// 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)
	}

	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(fmt.Sprintf("IMPORT_FROMANT: Secure this key. It provides root access to the created VM. 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."))

	// Print SSH command
	publicIPOutput, ipOk := outputs["public_ip"]
	if ipOk && publicIPOutput.Value != nil && config.LEMCUsername != "" {
		publicIP, castOk := publicIPOutput.Value.(string)
		if castOk && publicIP != "" {
			sshCommand := fmt.Sprintf("ssh -i %s %s@%s", sshKeyFilename, config.LEMCUsername, publicIP)
			lg(fmt.Sprintf("To SSH into the VM (once key is downloaded and in current directory): <code>%s</code>", sshCommand))
		} else {
			lg("Warning: Could not retrieve or cast public_ip from Terraform outputs to string.")
		}
	} else {
		if !ipOk || publicIPOutput.Value == nil {
			lg("Warning: public_ip not found in Terraform outputs, cannot print SSH command.")
		}
		if config.LEMCUsername == "" {
			lg("Warning: LEMC_USERNAME not set, cannot print SSH command.")
		}
	}

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

	s := `
	SSH_USERNAME=%s
	VM_IP=%s
	DOMAIN_NAME=https://%s
	`

	envContent := fmt.Sprintf(s, config.LEMCUsername, publicIPOutput.Value.(string), config.DomainName)
	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
}

// runDestroy executes the Terraform destroy operation
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
}

// 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)
	default:
		return fmt.Errorf("unsupported Terraform function: %s", function)
	}
}

func main() {
	lt("Starting Terraform setup...")

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

	lg(fmt.Sprintf("Running in directory: %s", config.WorkingDir))
	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 {
			log.Fatalf("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 {
		log.Fatalf("Error writing %s: %v", tfVarsPath, err)
	}

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

	// Get Terraform function from environment
	tfFunction := TerraformFunction(os.Getenv("TERRAFORM_FUNCTION"))
	if tfFunction == "" {
		tfFunction = FunctionApply // Default to apply if not specified
	}

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