package config

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

const (
	// integrityKeyFile is the filename for the integrity key
	integrityKeyFile = ".config_key"
	// integrityKeyLength is the length of the HMAC key in bytes
	integrityKeyLength = 32
	// hmacField is the JSON field name for the HMAC signature
	hmacField = "_integrity"
)

// ConfigWithIntegrity wraps a Config with an integrity signature
type ConfigWithIntegrity struct {
	Config
	Integrity string `json:"_integrity,omitempty"`
}

// IntegrityService provides HMAC-based integrity verification for config files
type IntegrityService struct {
	keyPath string
	key     []byte
}

// NewIntegrityService creates a new integrity service for the given app
func NewIntegrityService(appName string) (*IntegrityService, error) {
	configDir, err := os.UserConfigDir()
	if err != nil {
		return nil, fmt.Errorf("failed to get config directory: %w", err)
	}

	appConfigDir := filepath.Join(configDir, appName)
	keyPath := filepath.Join(appConfigDir, integrityKeyFile)

	svc := &IntegrityService{
		keyPath: keyPath,
	}

	// Load or generate key
	if err := svc.loadOrGenerateKey(); err != nil {
		return nil, fmt.Errorf("failed to initialize integrity key: %w", err)
	}

	return svc, nil
}

// loadOrGenerateKey loads the HMAC key from file or generates a new one
func (s *IntegrityService) loadOrGenerateKey() error {
	// Try to load existing key
	data, err := os.ReadFile(s.keyPath)
	if err == nil {
		// Key exists, decode it
		s.key, err = base64.StdEncoding.DecodeString(string(data))
		if err != nil || len(s.key) != integrityKeyLength {
			// Invalid key, regenerate
			return s.generateNewKey()
		}
		return nil
	}

	if !os.IsNotExist(err) {
		return fmt.Errorf("failed to read integrity key: %w", err)
	}

	// Key doesn't exist, generate new one
	return s.generateNewKey()
}

// generateNewKey generates a new HMAC key and saves it
func (s *IntegrityService) generateNewKey() error {
	s.key = make([]byte, integrityKeyLength)
	if _, err := rand.Read(s.key); err != nil {
		return fmt.Errorf("failed to generate key: %w", err)
	}

	// Ensure directory exists with restrictive permissions
	if err := os.MkdirAll(filepath.Dir(s.keyPath), 0700); err != nil {
		return fmt.Errorf("failed to create key directory: %w", err)
	}

	// Save key with restrictive permissions (owner read only)
	encoded := base64.StdEncoding.EncodeToString(s.key)
	if err := os.WriteFile(s.keyPath, []byte(encoded), 0400); err != nil {
		return fmt.Errorf("failed to save integrity key: %w", err)
	}

	return nil
}

// ComputeHMAC computes the HMAC signature for config data
func (s *IntegrityService) ComputeHMAC(config *Config) (string, error) {
	// Serialize config without the integrity field
	data, err := json.Marshal(config)
	if err != nil {
		return "", fmt.Errorf("failed to serialize config: %w", err)
	}

	// Compute HMAC-SHA256
	h := hmac.New(sha256.New, s.key)
	h.Write(data)
	signature := h.Sum(nil)

	return base64.StdEncoding.EncodeToString(signature), nil
}

// VerifyHMAC verifies the HMAC signature of config data
func (s *IntegrityService) VerifyHMAC(config *Config, providedHMAC string) error {
	// Compute expected HMAC
	expectedHMAC, err := s.ComputeHMAC(config)
	if err != nil {
		return fmt.Errorf("failed to compute HMAC: %w", err)
	}

	// Decode provided HMAC
	providedBytes, err := base64.StdEncoding.DecodeString(providedHMAC)
	if err != nil {
		return errors.New("invalid HMAC format")
	}

	expectedBytes, err := base64.StdEncoding.DecodeString(expectedHMAC)
	if err != nil {
		return errors.New("internal error computing HMAC")
	}

	// Constant-time comparison to prevent timing attacks
	if !hmac.Equal(providedBytes, expectedBytes) {
		return errors.New("config integrity check failed: file may have been tampered with")
	}

	return nil
}

// SignConfig adds an HMAC signature to the config
func (s *IntegrityService) SignConfig(config *Config) (*ConfigWithIntegrity, error) {
	signature, err := s.ComputeHMAC(config)
	if err != nil {
		return nil, err
	}

	return &ConfigWithIntegrity{
		Config:    *config,
		Integrity: signature,
	}, nil
}

// VerifyAndExtractConfig verifies the integrity and returns the config
func (s *IntegrityService) VerifyAndExtractConfig(configWithInt *ConfigWithIntegrity) (*Config, error) {
	if configWithInt.Integrity == "" {
		// No integrity field - config was created before integrity checking was added
		// Allow it but log a warning (caller should handle this)
		return &configWithInt.Config, nil
	}

	// Verify the HMAC
	if err := s.VerifyHMAC(&configWithInt.Config, configWithInt.Integrity); err != nil {
		return nil, err
	}

	return &configWithInt.Config, nil
}

// integrityAwareRepository wraps a repository with integrity checking
type integrityAwareRepository struct {
	inner            repository
	integritySvc     *IntegrityService
	configPath       string
	warnOnMissingMAC bool // If true, allows configs without MAC (for migration)
}

// NewIntegrityAwareRepository creates a repository wrapper with integrity checking
func NewIntegrityAwareRepository(inner repository, appName string, configPath string) (repository, error) {
	integritySvc, err := NewIntegrityService(appName)
	if err != nil {
		return nil, err
	}

	return &integrityAwareRepository{
		inner:            inner,
		integritySvc:     integritySvc,
		configPath:       configPath,
		warnOnMissingMAC: true, // Allow migration from old configs
	}, nil
}

// LoadConfig loads and verifies the config
func (r *integrityAwareRepository) LoadConfig(ctx context.Context) (*Config, error) {
	// Check if config file exists
	if _, err := os.Stat(r.configPath); os.IsNotExist(err) {
		// Load from inner (will create defaults)
		config, err := r.inner.LoadConfig(ctx)
		if err != nil {
			return nil, err
		}
		// Save with integrity
		return config, r.SaveConfig(ctx, config)
	}

	// Read raw file to check for integrity field
	data, err := os.ReadFile(r.configPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read config: %w", err)
	}

	// Try to parse with integrity field
	var configWithInt ConfigWithIntegrity
	if err := json.Unmarshal(data, &configWithInt); err != nil {
		return nil, fmt.Errorf("failed to parse config: %w", err)
	}

	// Verify integrity
	config, err := r.integritySvc.VerifyAndExtractConfig(&configWithInt)
	if err != nil {
		return nil, err
	}

	// If config had no integrity field, save it with one (migration)
	if configWithInt.Integrity == "" && r.warnOnMissingMAC {
		// Re-save with integrity
		_ = r.SaveConfig(ctx, config) // Ignore error, not critical
	}

	return config, nil
}

// SaveConfig saves the config with integrity signature
func (r *integrityAwareRepository) SaveConfig(ctx context.Context, config *Config) error {
	// Sign the config
	signedConfig, err := r.integritySvc.SignConfig(config)
	if err != nil {
		return fmt.Errorf("failed to sign config: %w", err)
	}

	// Serialize with integrity field
	data, err := json.MarshalIndent(signedConfig, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to serialize config: %w", err)
	}

	// Write with restrictive permissions
	return os.WriteFile(r.configPath, data, 0600)
}
