package pitr

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"dbbackup/internal/logger"
)

// RecoveryConfigGenerator generates PostgreSQL recovery configuration files
type RecoveryConfigGenerator struct {
	log logger.Logger
}

// NewRecoveryConfigGenerator creates a new recovery config generator
func NewRecoveryConfigGenerator(log logger.Logger) *RecoveryConfigGenerator {
	return &RecoveryConfigGenerator{
		log: log,
	}
}

// RecoveryConfig holds all recovery configuration parameters
type RecoveryConfig struct {
	// Core recovery settings
	Target         *RecoveryTarget
	WALArchiveDir  string
	RestoreCommand string

	// PostgreSQL version
	PostgreSQLVersion int // Major version (12, 13, 14, etc.)

	// Additional settings
	PrimaryConnInfo       string // For standby mode
	PrimarySlotName       string // Replication slot name
	RecoveryMinApplyDelay string // Min delay for replay

	// Paths
	DataDir string // PostgreSQL data directory
}

// GenerateRecoveryConfig writes recovery configuration files
// PostgreSQL 12+: postgresql.auto.conf + recovery.signal
// PostgreSQL < 12: recovery.conf
func (rcg *RecoveryConfigGenerator) GenerateRecoveryConfig(config *RecoveryConfig) error {
	rcg.log.Info("Generating recovery configuration",
		"pg_version", config.PostgreSQLVersion,
		"target_type", config.Target.Type,
		"data_dir", config.DataDir)

	if config.PostgreSQLVersion >= 12 {
		return rcg.generateModernRecoveryConfig(config)
	}
	return rcg.generateLegacyRecoveryConfig(config)
}

// generateModernRecoveryConfig generates config for PostgreSQL 12+
// Uses postgresql.auto.conf and recovery.signal
func (rcg *RecoveryConfigGenerator) generateModernRecoveryConfig(config *RecoveryConfig) error {
	// Create recovery.signal file (empty file that triggers recovery mode)
	recoverySignalPath := filepath.Join(config.DataDir, "recovery.signal")
	rcg.log.Info("Creating recovery.signal file", "path", recoverySignalPath)

	signalFile, err := os.Create(recoverySignalPath)
	if err != nil {
		return fmt.Errorf("failed to create recovery.signal: %w", err)
	}
	signalFile.Close()

	// Generate postgresql.auto.conf with recovery settings
	autoConfPath := filepath.Join(config.DataDir, "postgresql.auto.conf")
	rcg.log.Info("Generating postgresql.auto.conf", "path", autoConfPath)

	var sb strings.Builder
	sb.WriteString("# PostgreSQL recovery configuration\n")
	sb.WriteString("# Generated by dbbackup for Point-in-Time Recovery\n")
	sb.WriteString(fmt.Sprintf("# Target: %s\n", config.Target.Summary()))
	sb.WriteString("\n")

	// Restore command
	if config.RestoreCommand == "" {
		config.RestoreCommand = rcg.generateRestoreCommand(config.WALArchiveDir)
	}
	sb.WriteString(FormatConfigLine("restore_command", config.RestoreCommand))
	sb.WriteString("\n")

	// Recovery target parameters
	targetConfig := config.Target.ToPostgreSQLConfig()
	for key, value := range targetConfig {
		sb.WriteString(FormatConfigLine(key, value))
		sb.WriteString("\n")
	}

	// Optional: Primary connection info (for standby mode)
	if config.PrimaryConnInfo != "" {
		sb.WriteString("\n# Standby configuration\n")
		sb.WriteString(FormatConfigLine("primary_conninfo", config.PrimaryConnInfo))
		sb.WriteString("\n")
		if config.PrimarySlotName != "" {
			sb.WriteString(FormatConfigLine("primary_slot_name", config.PrimarySlotName))
			sb.WriteString("\n")
		}
	}

	// Optional: Recovery delay
	if config.RecoveryMinApplyDelay != "" {
		sb.WriteString(FormatConfigLine("recovery_min_apply_delay", config.RecoveryMinApplyDelay))
		sb.WriteString("\n")
	}

	// Write the configuration file
	if err := os.WriteFile(autoConfPath, []byte(sb.String()), 0600); err != nil {
		return fmt.Errorf("failed to write postgresql.auto.conf: %w", err)
	}

	rcg.log.Info("Recovery configuration generated successfully",
		"signal", recoverySignalPath,
		"config", autoConfPath)

	return nil
}

// generateLegacyRecoveryConfig generates config for PostgreSQL < 12
// Uses recovery.conf file
func (rcg *RecoveryConfigGenerator) generateLegacyRecoveryConfig(config *RecoveryConfig) error {
	recoveryConfPath := filepath.Join(config.DataDir, "recovery.conf")
	rcg.log.Info("Generating recovery.conf (legacy)", "path", recoveryConfPath)

	var sb strings.Builder
	sb.WriteString("# PostgreSQL recovery configuration\n")
	sb.WriteString("# Generated by dbbackup for Point-in-Time Recovery\n")
	sb.WriteString(fmt.Sprintf("# Target: %s\n", config.Target.Summary()))
	sb.WriteString("\n")

	// Restore command
	if config.RestoreCommand == "" {
		config.RestoreCommand = rcg.generateRestoreCommand(config.WALArchiveDir)
	}
	sb.WriteString(FormatConfigLine("restore_command", config.RestoreCommand))
	sb.WriteString("\n")

	// Recovery target parameters
	targetConfig := config.Target.ToPostgreSQLConfig()
	for key, value := range targetConfig {
		sb.WriteString(FormatConfigLine(key, value))
		sb.WriteString("\n")
	}

	// Optional: Primary connection info (for standby mode)
	if config.PrimaryConnInfo != "" {
		sb.WriteString("\n# Standby configuration\n")
		sb.WriteString(FormatConfigLine("standby_mode", "on"))
		sb.WriteString("\n")
		sb.WriteString(FormatConfigLine("primary_conninfo", config.PrimaryConnInfo))
		sb.WriteString("\n")
		if config.PrimarySlotName != "" {
			sb.WriteString(FormatConfigLine("primary_slot_name", config.PrimarySlotName))
			sb.WriteString("\n")
		}
	}

	// Optional: Recovery delay
	if config.RecoveryMinApplyDelay != "" {
		sb.WriteString(FormatConfigLine("recovery_min_apply_delay", config.RecoveryMinApplyDelay))
		sb.WriteString("\n")
	}

	// Write the configuration file
	if err := os.WriteFile(recoveryConfPath, []byte(sb.String()), 0600); err != nil {
		return fmt.Errorf("failed to write recovery.conf: %w", err)
	}

	rcg.log.Info("Recovery configuration generated successfully", "file", recoveryConfPath)

	return nil
}

// generateRestoreCommand creates a restore_command for fetching WAL files
func (rcg *RecoveryConfigGenerator) generateRestoreCommand(walArchiveDir string) string {
	// The restore_command is executed by PostgreSQL to fetch WAL files
	// %f = WAL filename, %p = full path to copy WAL file to

	// Try multiple extensions (.gz.enc, .enc, .gz, plain)
	// This handles compressed and/or encrypted WAL files
	return fmt.Sprintf(`bash -c 'for ext in .gz.enc .enc .gz ""; do [ -f "%s/%%f$ext" ] && { [ -z "$ext" ] && cp "%s/%%f$ext" "%%p" || case "$ext" in *.gz.enc) gpg -d "%s/%%f$ext" | gunzip > "%%p" ;; *.enc) gpg -d "%s/%%f$ext" > "%%p" ;; *.gz) gunzip -c "%s/%%f$ext" > "%%p" ;; esac; exit 0; }; done; exit 1'`,
		walArchiveDir, walArchiveDir, walArchiveDir, walArchiveDir, walArchiveDir)
}

// ValidateDataDirectory validates that the target directory is suitable for recovery
func (rcg *RecoveryConfigGenerator) ValidateDataDirectory(dataDir string) error {
	rcg.log.Info("Validating data directory", "path", dataDir)

	// Check if directory exists
	stat, err := os.Stat(dataDir)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("data directory does not exist: %s", dataDir)
		}
		return fmt.Errorf("failed to access data directory: %w", err)
	}

	if !stat.IsDir() {
		return fmt.Errorf("data directory is not a directory: %s", dataDir)
	}

	// Check for PG_VERSION file (indicates PostgreSQL data directory)
	pgVersionPath := filepath.Join(dataDir, "PG_VERSION")
	if _, err := os.Stat(pgVersionPath); err != nil {
		if os.IsNotExist(err) {
			rcg.log.Warn("PG_VERSION file not found - may not be a PostgreSQL data directory", "path", dataDir)
		}
	}

	// Check if PostgreSQL is running (postmaster.pid exists)
	postmasterPid := filepath.Join(dataDir, "postmaster.pid")
	if _, err := os.Stat(postmasterPid); err == nil {
		return fmt.Errorf("PostgreSQL is currently running in data directory %s (postmaster.pid exists). Stop PostgreSQL before running recovery", dataDir)
	}

	// Check write permissions
	testFile := filepath.Join(dataDir, ".dbbackup_test_write")
	if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
		return fmt.Errorf("data directory is not writable: %w", err)
	}
	os.Remove(testFile)

	rcg.log.Info("Data directory validation passed", "path", dataDir)
	return nil
}

// DetectPostgreSQLVersion detects the PostgreSQL version from the data directory
func (rcg *RecoveryConfigGenerator) DetectPostgreSQLVersion(dataDir string) (int, error) {
	pgVersionPath := filepath.Join(dataDir, "PG_VERSION")

	content, err := os.ReadFile(pgVersionPath)
	if err != nil {
		return 0, fmt.Errorf("failed to read PG_VERSION: %w", err)
	}

	versionStr := strings.TrimSpace(string(content))

	// Parse major version (e.g., "14" or "14.2")
	parts := strings.Split(versionStr, ".")
	if len(parts) == 0 {
		return 0, fmt.Errorf("invalid PG_VERSION format: %s", versionStr)
	}

	var majorVersion int
	if _, err := fmt.Sscanf(parts[0], "%d", &majorVersion); err != nil {
		return 0, fmt.Errorf("failed to parse PostgreSQL version from '%s': %w", versionStr, err)
	}

	rcg.log.Info("Detected PostgreSQL version", "version", majorVersion, "full", versionStr)
	return majorVersion, nil
}

// CleanupRecoveryFiles removes recovery configuration files (for cleanup after recovery)
func (rcg *RecoveryConfigGenerator) CleanupRecoveryFiles(dataDir string, pgVersion int) error {
	rcg.log.Info("Cleaning up recovery files", "data_dir", dataDir)

	if pgVersion >= 12 {
		// Remove recovery.signal
		recoverySignal := filepath.Join(dataDir, "recovery.signal")
		if err := os.Remove(recoverySignal); err != nil && !os.IsNotExist(err) {
			rcg.log.Warn("Failed to remove recovery.signal", "error", err)
		}

		// Note: postgresql.auto.conf is kept as it may contain other settings
		rcg.log.Info("Removed recovery.signal file")
	} else {
		// Remove recovery.conf
		recoveryConf := filepath.Join(dataDir, "recovery.conf")
		if err := os.Remove(recoveryConf); err != nil && !os.IsNotExist(err) {
			rcg.log.Warn("Failed to remove recovery.conf", "error", err)
		}
		rcg.log.Info("Removed recovery.conf file")
	}

	// Remove recovery.done if it exists (created by PostgreSQL after successful recovery)
	recoveryDone := filepath.Join(dataDir, "recovery.done")
	if err := os.Remove(recoveryDone); err != nil && !os.IsNotExist(err) {
		rcg.log.Warn("Failed to remove recovery.done", "error", err)
	}

	return nil
}

// BackupExistingConfig backs up existing recovery configuration (if any)
func (rcg *RecoveryConfigGenerator) BackupExistingConfig(dataDir string) error {
	timestamp := fmt.Sprintf("%d", os.Getpid())

	// Backup recovery.signal if exists (PG 12+)
	recoverySignal := filepath.Join(dataDir, "recovery.signal")
	if _, err := os.Stat(recoverySignal); err == nil {
		backup := filepath.Join(dataDir, fmt.Sprintf("recovery.signal.bak.%s", timestamp))
		if err := os.Rename(recoverySignal, backup); err != nil {
			return fmt.Errorf("failed to backup recovery.signal: %w", err)
		}
		rcg.log.Info("Backed up existing recovery.signal", "backup", backup)
	}

	// Backup recovery.conf if exists (PG < 12)
	recoveryConf := filepath.Join(dataDir, "recovery.conf")
	if _, err := os.Stat(recoveryConf); err == nil {
		backup := filepath.Join(dataDir, fmt.Sprintf("recovery.conf.bak.%s", timestamp))
		if err := os.Rename(recoveryConf, backup); err != nil {
			return fmt.Errorf("failed to backup recovery.conf: %w", err)
		}
		rcg.log.Info("Backed up existing recovery.conf", "backup", backup)
	}

	return nil
}
