package stacks

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"math/big"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/system"
	"gopkg.in/yaml.v3"
)

// AppConfig holds the per-app deployment configuration.
// Saved as app.yaml in each stack directory after first deployment.
type AppConfig struct {
	Deployed     bool              `yaml:"deployed" json:"deployed"`
	DeployedAt   string            `yaml:"deployed_at" json:"deployed_at"`
	Env          map[string]string `yaml:"env" json:"env"`
	LockedFields []string          `yaml:"locked_fields" json:"locked_fields"`
}

// DeployRequest contains the user-provided values from the deploy form.
type DeployRequest struct {
	StackName string            `json:"stack_name"`
	Values    map[string]string `json:"values"` // env_var -> user-provided value
}

// DeployStack handles first-time deployment of an app.
// Returns a warning message (empty if none) and an error if deployment is blocked.
// 1. Check available memory against app requirements
// 2. Load metadata (.felhom.yml) to know what fields exist
// 3. Auto-generate secrets for secret fields (hidden from user)
// 4. Auto-fill domain from controller config
// 5. Validate all user-provided values (password, path, required fields)
// 6. Save app.yaml
// 7. Run docker compose up -d with env vars
// 8. Update in-memory stack state
func (m *Manager) DeployStack(req DeployRequest) (string, error) {
	stack, ok := m.GetStack(req.StackName)
	if !ok {
		return "", fmt.Errorf("stack %q not found", req.StackName)
	}

	stackDir := filepath.Dir(stack.ComposePath)
	meta := LoadMetadata(stackDir)

	// Check if already deployed
	existing := LoadAppConfig(stackDir)
	if existing != nil && existing.Deployed {
		return "", fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
	}

	// --- Memory validation ---
	var deployWarning string
	reservedMB := m.cfg.System.ReservedMemoryMB
	totalMB, memErr := system.GetTotalMemoryMB()
	if memErr != nil {
		m.logger.Printf("[WARN] Cannot read system memory: %v — skipping memory check", memErr)
	} else {
		usableMB := totalMB - reservedMB
		currentReqMB, currentLimitMB := m.CommittedMemory()
		newReqMB := ParseMemoryMB(meta.Resources.MemRequest)
		newLimitMB := ParseMemoryMB(meta.Resources.MemLimit)

		m.logger.Printf("[INFO] Memory check: total=%dMB, reserved=%dMB, usable=%dMB, committed_req=%dMB, new_req=%dMB, remaining=%dMB",
			totalMB, reservedMB, usableMB, currentReqMB, newReqMB, usableMB-currentReqMB-newReqMB)

		// Hard block: requests exceed usable memory
		if newReqMB > 0 && currentReqMB+newReqMB > usableMB {
			return "", fmt.Errorf(
				"Nincs elég memória az alkalmazás telepítéséhez. "+
					"Szükséges: %d MB, Elérhető: %d MB "+
					"(összesen: %d MB, ebből %d MB már foglalt, %d MB rendszer számára fenntartva)",
				newReqMB,
				usableMB-currentReqMB,
				totalMB,
				currentReqMB,
				reservedMB,
			)
		}

		// Soft warning: limits exceed total (overcommit)
		if newLimitMB > 0 && currentLimitMB+newLimitMB > totalMB {
			deployWarning = "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. " +
				"Normál használat mellett ez nem okoz problémát."
		}
	}

	// Debug: log received values (redact passwords/secrets)
	m.logger.Printf("[DEBUG] Deploy %s: received %d user values", req.StackName, len(req.Values))
	for k, v := range req.Values {
		if strings.Contains(strings.ToLower(k), "password") || strings.Contains(strings.ToLower(k), "secret") {
			m.logger.Printf("[DEBUG]   %s = [REDACTED, len=%d]", k, len(v))
		} else {
			m.logger.Printf("[DEBUG]   %s = %q", k, v)
		}
	}

	// Build the full env map
	env := make(map[string]string)
	var lockedFields []string

	for _, field := range meta.DeployFields {
		var value string

		switch field.Type {
		case "domain":
			// Auto-fill from controller config
			value = m.cfg.Customer.Domain

		case "secret":
			// Always auto-generate, user never sees these
			generated, err := generateValue(field.Generate)
			if err != nil {
				return "", fmt.Errorf("generating %s: %w", field.EnvVar, err)
			}
			value = generated

		case "password":
			// Password fields MUST be filled by the user (via typing or Generálás button).
			// We never silently auto-generate — the user needs to know their password.
			if userVal, ok := req.Values[field.EnvVar]; ok && userVal != "" {
				value = userVal
			} else {
				return "", fmt.Errorf("a(z) %q mező kitöltése kötelező — használja a Generálás gombot vagy írjon be egy jelszót", field.Label)
			}

		default:
			// text, path, select, boolean — use user value or default
			if userVal, ok := req.Values[field.EnvVar]; ok {
				value = userVal
			} else if field.Default != "" {
				value = field.Default
			}
		}

		// Validate required fields
		if field.Required && value == "" {
			return "", fmt.Errorf("a(z) %q (%s) mező kitöltése kötelező", field.Label, field.EnvVar)
		}

		// Validate path fields exist on disk (inside the container's filesystem)
		if field.Type == "path" && value != "" {
			if _, err := os.Stat(value); os.IsNotExist(err) {
				return "", fmt.Errorf("path %q does not exist for field %q", value, field.Label)
			}
		}

		if value != "" {
			env[field.EnvVar] = value
		}

		if field.LockedAfterDeploy {
			lockedFields = append(lockedFields, field.EnvVar)
		}
	}

	// Save app.yaml
	appCfg := &AppConfig{
		Deployed:     true,
		DeployedAt:   time.Now().UTC().Format(time.RFC3339),
		Env:          env,
		LockedFields: lockedFields,
	}

	if err := SaveAppConfig(stackDir, appCfg); err != nil {
		return "", fmt.Errorf("saving app config: %w", err)
	}

	// Debug: log final env var keys (not values)
	envKeys := make([]string, 0, len(env))
	for k := range env {
		envKeys = append(envKeys, k)
	}
	m.logger.Printf("[INFO] Deploying stack %s with %d env vars: [%s]", req.StackName, len(env), strings.Join(envKeys, ", "))

	// Check which images are available locally before pulling
	if m.isDebug() {
		m.checkLocalImages(req.StackName, stackDir)
	}

	// Update in-memory stack state BEFORE compose up so the UI reflects
	// "deployed" immediately (compose up can take 30-60s for image pulls).
	// If compose up fails, we revert both disk and in-memory state below.
	m.mu.Lock()
	if s, ok := m.stacks[req.StackName]; ok {
		s.Deployed = true
		s.AppConfig = appCfg
	}
	m.mu.Unlock()

	// Run docker compose up -d
	start := time.Now()
	_, composeErr := m.composeExecWithEnv(stackDir, env, "up", "-d")
	if composeErr != nil {
		m.logger.Printf("[ERROR] Stack %s deploy failed after %.1fs: %v", req.StackName, time.Since(start).Seconds(), composeErr)
		// Revert in-memory state
		m.mu.Lock()
		if s, ok := m.stacks[req.StackName]; ok {
			s.Deployed = false
			s.AppConfig = nil
		}
		m.mu.Unlock()
		// Revert disk state — keep app.yaml for debugging but mark as not deployed
		appCfg.Deployed = false
		_ = SaveAppConfig(stackDir, appCfg)
		return "", fmt.Errorf("docker compose up failed: %w", composeErr)
	}

	m.logger.Printf("[INFO] Stack %s deployed successfully (took %.1fs)", req.StackName, time.Since(start).Seconds())

	// Post-deploy container state check (async, non-blocking)
	deployEnv := m.stackEnv(stackDir)
	m.logPostStartStatus(req.StackName, stackDir, deployEnv)

	return deployWarning, m.RefreshStatus()
}

// UpdateStackConfig updates non-locked fields for a deployed stack.
func (m *Manager) UpdateStackConfig(name string, values map[string]string) error {
	stack, ok := m.GetStack(name)
	if !ok {
		return fmt.Errorf("stack %q not found", name)
	}

	stackDir := filepath.Dir(stack.ComposePath)
	appCfg := LoadAppConfig(stackDir)
	if appCfg == nil || !appCfg.Deployed {
		return fmt.Errorf("stack %q is not deployed yet", name)
	}

	lockedSet := make(map[string]bool)
	for _, f := range appCfg.LockedFields {
		lockedSet[f] = true
	}

	for key, val := range values {
		if lockedSet[key] {
			return fmt.Errorf("field %q is locked and cannot be changed after deployment", key)
		}
		appCfg.Env[key] = val
	}

	if err := SaveAppConfig(stackDir, appCfg); err != nil {
		return fmt.Errorf("saving updated config: %w", err)
	}

	_, err := m.composeExecWithEnv(stackDir, appCfg.Env, "up", "-d")
	if err != nil {
		return fmt.Errorf("restarting with new config: %w", err)
	}

	m.logger.Printf("[INFO] Stack %s config updated and restarted", name)
	return m.RefreshStatus()
}

// composeExecWithEnv runs a compose command with custom env vars injected.
func (m *Manager) composeExecWithEnv(dir string, env map[string]string, args ...string) (string, error) {
	cmdEnv := os.Environ()
	for k, v := range env {
		cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
	}
	cmdEnv = append(cmdEnv, fmt.Sprintf("DOMAIN=%s", m.cfg.Customer.Domain))
	return m.composeExecCustomEnv(dir, cmdEnv, args...)
}

// GetDeployFields returns the deployment fields for a stack (for the deploy form).
func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) {
	stack, ok := m.GetStack(name)
	if !ok {
		return nil, nil, fmt.Errorf("stack %q not found", name)
	}

	stackDir := filepath.Dir(stack.ComposePath)
	meta := LoadMetadata(stackDir)
	appCfg := LoadAppConfig(stackDir)

	return &meta, appCfg, nil
}

// UpdateOptionalConfig updates optional env vars in app.yaml and restarts the stack if deployed.
// Only updates env vars that are listed in the metadata's optional_config sections.
func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]string) error {
	stack, ok := m.GetStack(stackName)
	if !ok {
		return fmt.Errorf("stack %q not found", stackName)
	}

	// Build a set of allowed env vars from optional_config
	allowed := make(map[string]bool)
	for _, group := range stack.Meta.OptionalConfig {
		for _, field := range group.Fields {
			allowed[field.EnvVar] = true
		}
	}
	if len(allowed) == 0 {
		return fmt.Errorf("no optional config fields defined for %s", stackName)
	}

	// Load existing app.yaml (or create empty one)
	stackDir := filepath.Dir(stack.ComposePath)
	appCfg := LoadAppConfig(stackDir)
	if appCfg == nil {
		appCfg = &AppConfig{
			Env: make(map[string]string),
		}
	}
	if appCfg.Env == nil {
		appCfg.Env = make(map[string]string)
	}

	// Update only allowed env vars
	changed := false
	for key, val := range values {
		if !allowed[key] {
			m.logger.Printf("[WARN] Ignoring non-optional env var: %s", key)
			continue
		}
		if appCfg.Env[key] != val {
			appCfg.Env[key] = val
			changed = true
			m.logger.Printf("[INFO] Updated optional config %s for %s", key, stackName)
		}
	}

	if !changed {
		return nil
	}

	// Save app.yaml
	if err := SaveAppConfig(stackDir, appCfg); err != nil {
		return fmt.Errorf("saving app config: %w", err)
	}
	m.logger.Printf("[INFO] Saved updated app.yaml for %s", stackName)

	// If deployed, recreate containers to pick up new env vars
	// (docker compose restart does NOT pick up new env vars — must use up -d)
	if stack.Deployed {
		m.logger.Printf("[INFO] Restarting %s to apply new optional config", stackName)
		env := m.stackEnv(stackDir)
		if _, err := m.composeExecCustomEnv(stackDir, env, "up", "-d"); err != nil {
			return fmt.Errorf("restart after config update: %w", err)
		}
		m.logPostStartStatus(stackName, stackDir, env)
	}

	return m.RefreshStatus()
}

// LoadAppConfigByName reads app.yaml for a named stack. Returns nil if not found.
func (m *Manager) LoadAppConfigByName(stackName string) *AppConfig {
	stack, ok := m.GetStack(stackName)
	if !ok {
		return nil
	}
	stackDir := filepath.Dir(stack.ComposePath)
	return LoadAppConfig(stackDir)
}

// --- App config persistence ---

func LoadAppConfig(stackDir string) *AppConfig {
	path := filepath.Join(stackDir, "app.yaml")
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	cfg := &AppConfig{}
	if err := yaml.Unmarshal(data, cfg); err != nil {
		return nil
	}
	return cfg
}

func SaveAppConfig(stackDir string, cfg *AppConfig) error {
	data, err := yaml.Marshal(cfg)
	if err != nil {
		return fmt.Errorf("marshaling app config: %w", err)
	}
	path := filepath.Join(stackDir, "app.yaml")
	header := "# Auto-generated by felhom-controller — do not edit locked fields manually\n"
	content := header + string(data)
	if err := os.WriteFile(path, []byte(content), 0600); err != nil {
		return fmt.Errorf("writing %s: %w", path, err)
	}
	return nil
}

// --- Secret generation ---

const alphanumChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func generateValue(spec string) (string, error) {
	if spec == "" {
		return "", fmt.Errorf("empty generator spec")
	}
	parts := strings.SplitN(spec, ":", 2)
	if len(parts) != 2 {
		return "", fmt.Errorf("invalid generator spec: %q (expected type:param)", spec)
	}

	switch parts[0] {
	case "password":
		length := 0
		if _, err := fmt.Sscanf(parts[1], "%d", &length); err != nil || length <= 0 {
			return "", fmt.Errorf("invalid password length: %q", parts[1])
		}
		return randomAlphanumeric(length)
	case "hex":
		byteLen := 0
		if _, err := fmt.Sscanf(parts[1], "%d", &byteLen); err != nil || byteLen <= 0 {
			return "", fmt.Errorf("invalid hex length: %q", parts[1])
		}
		b := make([]byte, byteLen)
		if _, err := rand.Read(b); err != nil {
			return "", fmt.Errorf("reading random bytes: %w", err)
		}
		return hex.EncodeToString(b), nil
	case "static":
		return parts[1], nil
	default:
		return "", fmt.Errorf("unknown generator type: %q", parts[0])
	}
}

func randomAlphanumeric(length int) (string, error) {
	result := make([]byte, length)
	for i := range result {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumChars))))
		if err != nil {
			return "", err
		}
		result[i] = alphanumChars[n.Int64()]
	}
	return string(result), nil
}