package redact

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"regexp"
	"strings"
	"sync"
	"time"
)

// TokenPrefix represents different types of secrets for token generation
type TokenPrefix string

const (
	TokenPrefixPassword   TokenPrefix = "PASSWORD"
	TokenPrefixAPIKey     TokenPrefix = "APIKEY"
	TokenPrefixDatabase   TokenPrefix = "DATABASE"
	TokenPrefixEmail      TokenPrefix = "EMAIL"
	TokenPrefixIP         TokenPrefix = "IP"
	TokenPrefixToken      TokenPrefix = "TOKEN"
	TokenPrefixSecret     TokenPrefix = "SECRET"
	TokenPrefixKey        TokenPrefix = "KEY"
	TokenPrefixCredential TokenPrefix = "CREDENTIAL"
	TokenPrefixAuth       TokenPrefix = "AUTH"
	TokenPrefixGeneric    TokenPrefix = "GENERIC"
)

// TokenizerConfig holds configuration for the tokenizer
type TokenizerConfig struct {
	// Enable tokenization (defaults to checking TROUBLESHOOT_TOKENIZATION env var)
	Enabled bool

	// Salt for deterministic token generation per bundle
	Salt []byte

	// Default token prefix when type cannot be determined
	DefaultPrefix TokenPrefix

	// Token format template (must include %s for prefix and %s for hash)
	TokenFormat string

	// Hash length in characters (default 6)
	HashLength int
}

// Tokenizer handles deterministic secret tokenization
type Tokenizer struct {
	config     TokenizerConfig
	tokenMap   map[string]string // secret value -> token
	reverseMap map[string]string // token -> secret value (for debugging/mapping)
	mutex      sync.RWMutex

	// Secret type detection patterns
	typePatterns map[TokenPrefix]*regexp.Regexp

	// Phase 2: Cross-File Correlation fields
	bundleID          string                     // unique bundle identifier
	secretRefs        map[string][]string        // token -> list of file paths
	duplicateGroups   map[string]*DuplicateGroup // secretHash -> DuplicateGroup
	correlations      []CorrelationGroup         // detected correlations
	fileStats         map[string]*FileStats      // filePath -> FileStats
	cacheStats        CacheStats                 // performance statistics
	normalizedSecrets map[string]string          // normalized secret -> original secret
	secretHashes      map[string]string          // secret value -> hash for deduplication
}

// RedactionMap represents the mapping between tokens and original values
type RedactionMap struct {
	Tokens        map[string]string   `json:"tokens"`       // token -> original value
	Stats         RedactionStats      `json:"stats"`        // redaction statistics
	Timestamp     time.Time           `json:"timestamp"`    // when redaction was performed
	Profile       string              `json:"profile"`      // profile used
	BundleID      string              `json:"bundleId"`     // unique bundle identifier
	SecretRefs    map[string][]string `json:"secretRefs"`   // token -> list of file paths where found
	Duplicates    []DuplicateGroup    `json:"duplicates"`   // groups of identical secrets
	Correlations  []CorrelationGroup  `json:"correlations"` // correlated secret patterns
	EncryptionKey []byte              `json:"-"`            // encryption key (not serialized)
	IsEncrypted   bool                `json:"isEncrypted"`  // whether the mapping is encrypted
}

// RedactionStats contains statistics about the redaction process
type RedactionStats struct {
	TotalSecrets      int                  `json:"totalSecrets"`
	UniqueSecrets     int                  `json:"uniqueSecrets"`
	TokensGenerated   int                  `json:"tokensGenerated"`
	SecretsByType     map[string]int       `json:"secretsByType"`
	ProcessingTimeMs  int64                `json:"processingTimeMs"`
	FilesCovered      int                  `json:"filesCovered"`
	DuplicateCount    int                  `json:"duplicateCount"`
	CorrelationCount  int                  `json:"correlationCount"`
	NormalizationHits int                  `json:"normalizationHits"`
	CacheHits         int                  `json:"cacheHits"`
	CacheMisses       int                  `json:"cacheMisses"`
	FileCoverage      map[string]FileStats `json:"fileCoverage"`
}

// FileStats tracks statistics per file
type FileStats struct {
	FilePath     string         `json:"filePath"`
	SecretsFound int            `json:"secretsFound"`
	TokensUsed   int            `json:"tokensUsed"`
	SecretTypes  map[string]int `json:"secretTypes"`
	ProcessedAt  time.Time      `json:"processedAt"`
}

// DuplicateGroup represents a group of identical secrets found in different locations
type DuplicateGroup struct {
	SecretHash string    `json:"secretHash"` // hash of the normalized secret
	Token      string    `json:"token"`      // the token used for this secret
	SecretType string    `json:"secretType"` // classified type of the secret
	Locations  []string  `json:"locations"`  // file paths where this secret was found
	Count      int       `json:"count"`      // total occurrences
	FirstSeen  time.Time `json:"firstSeen"`  // when first detected
	LastSeen   time.Time `json:"lastSeen"`   // when last detected
}

// CorrelationGroup represents correlated secret patterns across files
type CorrelationGroup struct {
	Pattern     string    `json:"pattern"`     // correlation pattern identifier
	Description string    `json:"description"` // human-readable description
	Tokens      []string  `json:"tokens"`      // tokens involved in correlation
	Files       []string  `json:"files"`       // files where correlation was found
	Confidence  float64   `json:"confidence"`  // confidence score (0.0-1.0)
	DetectedAt  time.Time `json:"detectedAt"`  // when correlation was detected
}

// CacheStats tracks tokenizer cache performance
type CacheStats struct {
	Hits   int64 `json:"hits"`   // cache hits
	Misses int64 `json:"misses"` // cache misses
	Total  int64 `json:"total"`  // total lookups
}

var (
	// Global tokenizer instance
	globalTokenizer *Tokenizer
	tokenizerOnce   sync.Once
)

// NewTokenizer creates a new tokenizer with the given configuration
func NewTokenizer(config TokenizerConfig) *Tokenizer {
	if config.TokenFormat == "" {
		config.TokenFormat = "***TOKEN_%s_%s***"
	}
	if config.HashLength == 0 {
		config.HashLength = 6
	}
	if config.DefaultPrefix == "" {
		config.DefaultPrefix = TokenPrefixGeneric
	}

	// Generate salt if not provided
	if len(config.Salt) == 0 {
		config.Salt = make([]byte, 32)
		if _, err := rand.Read(config.Salt); err != nil {
			// Fallback to time-based salt if crypto rand fails
			timeStr := fmt.Sprintf("%d", time.Now().UnixNano())
			config.Salt = []byte(timeStr)
		}
	}

	// Generate bundle ID if not provided
	bundleID := fmt.Sprintf("bundle_%d_%s", time.Now().UnixNano(), hex.EncodeToString(config.Salt[:8]))

	tokenizer := &Tokenizer{
		config:            config,
		tokenMap:          make(map[string]string),
		reverseMap:        make(map[string]string),
		typePatterns:      make(map[TokenPrefix]*regexp.Regexp),
		bundleID:          bundleID,
		secretRefs:        make(map[string][]string),
		duplicateGroups:   make(map[string]*DuplicateGroup),
		correlations:      make([]CorrelationGroup, 0),
		fileStats:         make(map[string]*FileStats),
		cacheStats:        CacheStats{},
		normalizedSecrets: make(map[string]string),
		secretHashes:      make(map[string]string),
	}

	// Initialize secret type detection patterns
	tokenizer.initTypePatterns()

	return tokenizer
}

// GetGlobalTokenizer returns the global tokenizer instance
func GetGlobalTokenizer() *Tokenizer {
	tokenizerOnce.Do(func() {
		globalTokenizer = NewTokenizer(TokenizerConfig{
			Enabled: false, // Will be set explicitly by calling code
		})
	})

	return globalTokenizer
}

// EnableTokenization enables tokenization on the global tokenizer
func EnableTokenization() {
	globalTokenizer := GetGlobalTokenizer()
	globalTokenizer.config.Enabled = true
}

// DisableTokenization disables tokenization on the global tokenizer
func DisableTokenization() {
	globalTokenizer := GetGlobalTokenizer()
	globalTokenizer.config.Enabled = false
}

// IsEnabled returns whether tokenization is enabled
func (t *Tokenizer) IsEnabled() bool {
	return t.config.Enabled
}

// initTypePatterns initializes regex patterns for secret type detection
func (t *Tokenizer) initTypePatterns() {
	patterns := map[TokenPrefix]string{
		TokenPrefixPassword:   `(?i)password|passwd|pwd`,
		TokenPrefixAPIKey:     `(?i)api.?key|apikey|access.?key`,
		TokenPrefixDatabase:   `(?i)database|db.?(url|uri|host|pass|connection)`,
		TokenPrefixEmail:      `(?i)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`,
		TokenPrefixIP:         `(?i)\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`,
		TokenPrefixToken:      `(?i)token|bearer|jwt|oauth`,
		TokenPrefixSecret:     `(?i)secret|private.?key`,
		TokenPrefixCredential: `(?i)credential|cred|auth`,
		TokenPrefixKey:        `(?i)key|cert|certificate`,
	}

	for prefix, pattern := range patterns {
		if compiled, err := regexp.Compile(pattern); err == nil {
			t.typePatterns[prefix] = compiled
		}
	}
}

// classifySecret determines the appropriate token prefix for a secret value
func (t *Tokenizer) classifySecret(context, value string) TokenPrefix {
	contextLower := strings.ToLower(context)
	valueLower := strings.ToLower(value)

	// Check context first, with specific patterns having priority
	// Order matters here - more specific patterns should be checked first
	specificPrefixes := []TokenPrefix{
		TokenPrefixAPIKey,
		TokenPrefixPassword,
		TokenPrefixDatabase,
		TokenPrefixCredential,
		TokenPrefixSecret,
		TokenPrefixToken,
		TokenPrefixKey, // More general, check last
	}

	for _, prefix := range specificPrefixes {
		if pattern, exists := t.typePatterns[prefix]; exists {
			if pattern.MatchString(contextLower) {
				return prefix
			}
		}
	}

	// Check value patterns for specific formats (email, IP, etc.)
	if pattern, exists := t.typePatterns[TokenPrefixEmail]; exists && pattern.MatchString(value) {
		return TokenPrefixEmail
	}
	if pattern, exists := t.typePatterns[TokenPrefixIP]; exists && pattern.MatchString(value) {
		return TokenPrefixIP
	}

	// Check value content for common secret indicators (same priority order)
	for _, prefix := range specificPrefixes {
		prefixLower := strings.ToLower(string(prefix))
		if strings.Contains(valueLower, prefixLower) {
			return prefix
		}
	}

	return t.config.DefaultPrefix
}

// generateToken creates a deterministic token for a given secret value
func (t *Tokenizer) generateToken(value, context string) string {
	// Classify the secret type
	prefix := t.classifySecret(context, value)

	// Generate deterministic hash using HMAC-SHA256
	h := hmac.New(sha256.New, t.config.Salt)
	h.Write([]byte(value))
	h.Write([]byte(context)) // Include context for better uniqueness
	hash := h.Sum(nil)

	// Convert to hex and truncate to desired length
	hashStr := hex.EncodeToString(hash)
	if len(hashStr) > t.config.HashLength {
		hashStr = hashStr[:t.config.HashLength]
	}

	// Generate token with collision detection
	baseToken := fmt.Sprintf(t.config.TokenFormat, string(prefix), strings.ToUpper(hashStr))

	// Check for collisions and resolve them
	token := t.resolveCollision(baseToken, value)

	return token
}

// resolveCollision handles token collisions by appending a counter
func (t *Tokenizer) resolveCollision(baseToken, value string) string {
	// Check for collision without lock first
	existingValue, exists := t.reverseMap[baseToken]

	// No collision
	if !exists || existingValue == value {
		return baseToken
	}

	// Collision detected, try up to 100 variations
	for counter := 1; counter <= 100; counter++ {
		newToken := fmt.Sprintf("%s_%d", baseToken, counter)

		existingValue, exists = t.reverseMap[newToken]
		if !exists || existingValue == value {
			return newToken
		}
	}

	// If we still have collisions after 100 tries, use timestamp
	timestamp := time.Now().UnixNano()
	// Insert counter before the final *** to match ValidateToken regex
	if strings.HasSuffix(baseToken, "***") {
		base := strings.TrimSuffix(baseToken, "***")
		return fmt.Sprintf("%s_%d***", base, timestamp%10000)
	}
	return fmt.Sprintf("%s_%d", baseToken, timestamp%10000)
}

// TokenizeValue generates or retrieves a token for a secret value
func (t *Tokenizer) TokenizeValue(value, context string) string {
	return t.TokenizeValueWithPath(value, context, "")
}

// TokenizeValueWithPath generates or retrieves a token for a secret value with file path tracking
func (t *Tokenizer) TokenizeValueWithPath(value, context, filePath string) string {
	if !t.config.Enabled || value == "" {
		return MASK_TEXT // Fallback to original behavior
	}

	t.mutex.Lock()
	defer t.mutex.Unlock()

	// Normalize the secret value for better correlation
	normalizedValue := t.normalizeSecret(value)

	// Update cache statistics
	t.cacheStats.Total++

	// Check if we already have a token for this normalized value
	if existing, exists := t.tokenMap[normalizedValue]; exists {
		t.cacheStats.Hits++

		// Track this usage even if token already exists
		if filePath != "" {
			t.addSecretReference(existing, filePath)

			// Get secret type for tracking
			secretType := string(t.classifySecret(context, value))
			t.updateFileStats(filePath, secretType)

			// Update duplicate tracking
			secretHash := t.generateSecretHash(normalizedValue)
			t.trackDuplicateSecret(secretHash, existing, secretType, filePath, normalizedValue)
		}

		return existing
	}

	t.cacheStats.Misses++

	// Generate new token
	token := t.generateToken(normalizedValue, context)

	// Store in both directions (use normalized value as key)
	t.tokenMap[normalizedValue] = token
	t.reverseMap[token] = value // Store original value for mapping

	// Track secret hash for deduplication
	secretHash := t.generateSecretHash(normalizedValue)
	t.secretHashes[normalizedValue] = secretHash

	// Track file reference and stats if path provided
	if filePath != "" {
		t.addSecretReference(token, filePath)

		// Get secret type for tracking
		secretType := string(t.classifySecret(context, value))
		t.updateFileStats(filePath, secretType)

		// Track as duplicate (even first occurrence)
		t.trackDuplicateSecret(secretHash, token, secretType, filePath, normalizedValue)
	}

	return token
}

// GetRedactionMap returns the current redaction map
func (t *Tokenizer) GetRedactionMap(profile string) RedactionMap {
	t.mutex.Lock()
	defer t.mutex.Unlock()

	// Analyze correlations before generating the map
	t.analyzeCorrelations()

	// Create stats
	secretsByType := make(map[string]int)
	for token := range t.reverseMap {
		// Extract type from token format
		if parts := strings.Split(token, "_"); len(parts) >= 2 {
			// Expected format: ***TOKEN_TYPE_HASH***
			if len(parts) >= 3 && strings.HasPrefix(token, "***TOKEN_") {
				tokenType := parts[2] // Extract TYPE part
				secretsByType[tokenType]++
			}
		}
	}

	// Count duplicates and correlations
	duplicateCount := 0
	for _, group := range t.duplicateGroups {
		if group.Count > 1 {
			duplicateCount++
		}
	}

	// Copy file coverage
	fileCoverage := make(map[string]FileStats)
	for path, stats := range t.fileStats {
		if stats != nil {
			fileCoverage[path] = *stats
		}
	}

	// Convert duplicate groups to slice
	duplicates := make([]DuplicateGroup, 0, len(t.duplicateGroups))
	for _, group := range t.duplicateGroups {
		if group != nil {
			duplicates = append(duplicates, *group)
		}
	}

	stats := RedactionStats{
		TotalSecrets:      len(t.tokenMap),
		UniqueSecrets:     len(t.tokenMap),
		TokensGenerated:   len(t.reverseMap),
		SecretsByType:     secretsByType,
		ProcessingTimeMs:  0, // Would be populated by caller
		FilesCovered:      len(t.fileStats),
		DuplicateCount:    duplicateCount,
		CorrelationCount:  len(t.correlations),
		NormalizationHits: len(t.normalizedSecrets),
		CacheHits:         int(t.cacheStats.Hits),
		CacheMisses:       int(t.cacheStats.Misses),
		FileCoverage:      fileCoverage,
	}

	return RedactionMap{
		Tokens:       t.reverseMap,
		Stats:        stats,
		Timestamp:    time.Now(),
		Profile:      profile,
		BundleID:     t.bundleID,
		SecretRefs:   t.secretRefs,
		Duplicates:   duplicates,
		Correlations: t.correlations,
		IsEncrypted:  false, // Will be set when encryption is applied
	}
}

// ValidateToken checks if a token matches the expected format
func (t *Tokenizer) ValidateToken(token string) bool {
	// Basic format validation - should match ***TOKEN_PREFIX_HASH***
	pattern := `^\*\*\*TOKEN_[A-Z]+_[A-F0-9]+(\*\*\*|_\d+\*\*\*)$`
	matched, err := regexp.MatchString(pattern, token)
	return err == nil && matched
}

// Reset clears all tokens and mappings (useful for testing)
func (t *Tokenizer) Reset() {
	t.mutex.Lock()
	defer t.mutex.Unlock()

	t.tokenMap = make(map[string]string)
	t.reverseMap = make(map[string]string)
	t.secretRefs = make(map[string][]string)
	t.duplicateGroups = make(map[string]*DuplicateGroup)
	t.correlations = make([]CorrelationGroup, 0)
	t.fileStats = make(map[string]*FileStats)
	t.cacheStats = CacheStats{}
	t.normalizedSecrets = make(map[string]string)
	t.secretHashes = make(map[string]string)
}

// GetTokenCount returns the number of tokens generated
func (t *Tokenizer) GetTokenCount() int {
	t.mutex.RLock()
	defer t.mutex.RUnlock()

	return len(t.tokenMap)
}

// ResetGlobalTokenizer resets the global tokenizer instance (useful for testing)
func ResetGlobalTokenizer() {
	globalTokenizer = nil
	tokenizerOnce = sync.Once{}
}

// analyzeCorrelations detects patterns and correlations across secrets
func (t *Tokenizer) analyzeCorrelations() {
	// Detect common correlation patterns
	correlations := make([]CorrelationGroup, 0)

	// Pattern 1: Database connection components (host, user, password, database)
	dbTokens := make([]string, 0)
	dbFiles := make([]string, 0)

	for token, files := range t.secretRefs {
		// Check if token looks like database-related
		if strings.Contains(token, "DATABASE") || strings.Contains(token, "PASSWORD") {
			dbTokens = append(dbTokens, token)
			for _, file := range files {
				// Add file if not already present
				found := false
				for _, existing := range dbFiles {
					if existing == file {
						found = true
						break
					}
				}
				if !found {
					dbFiles = append(dbFiles, file)
				}
			}
		}
	}

	if len(dbTokens) >= 2 && len(dbFiles) >= 1 {
		correlations = append(correlations, CorrelationGroup{
			Pattern:     "database_credentials",
			Description: "Database connection credentials found together",
			Tokens:      dbTokens,
			Files:       dbFiles,
			Confidence:  0.8,
			DetectedAt:  time.Now(),
		})
	}

	// Pattern 2: AWS credential pairs (Access Key + Secret)
	awsTokens := make([]string, 0)
	awsFiles := make([]string, 0)

	for token, files := range t.secretRefs {
		// Look for any APIKEY or SECRET tokens - AWS detection can be broader
		if strings.Contains(token, "APIKEY") || strings.Contains(token, "SECRET") {
			awsTokens = append(awsTokens, token)
			for _, file := range files {
				found := false
				for _, existing := range awsFiles {
					if existing == file {
						found = true
						break
					}
				}
				if !found {
					awsFiles = append(awsFiles, file)
				}
			}
		}
	}

	if len(awsTokens) >= 2 && len(awsFiles) >= 1 {
		correlations = append(correlations, CorrelationGroup{
			Pattern:     "aws_credentials",
			Description: "AWS credential pair (access key + secret) found together",
			Tokens:      awsTokens,
			Files:       awsFiles,
			Confidence:  0.9,
			DetectedAt:  time.Now(),
		})
	}

	// Pattern 3: API authentication (API key + token)
	apiTokens := make([]string, 0)
	apiFiles := make([]string, 0)

	for token, files := range t.secretRefs {
		if strings.Contains(token, "APIKEY") || strings.Contains(token, "TOKEN") {
			apiTokens = append(apiTokens, token)
			for _, file := range files {
				found := false
				for _, existing := range apiFiles {
					if existing == file {
						found = true
						break
					}
				}
				if !found {
					apiFiles = append(apiFiles, file)
				}
			}
		}
	}

	if len(apiTokens) >= 2 && len(apiFiles) >= 1 {
		correlations = append(correlations, CorrelationGroup{
			Pattern:     "api_authentication",
			Description: "API authentication tokens found together",
			Tokens:      apiTokens,
			Files:       apiFiles,
			Confidence:  0.7,
			DetectedAt:  time.Now(),
		})
	}

	t.correlations = correlations
}

// GetBundleID returns the unique bundle identifier
func (t *Tokenizer) GetBundleID() string {
	t.mutex.RLock()
	defer t.mutex.RUnlock()
	return t.bundleID
}

// GetDuplicateGroups returns all duplicate secret groups
func (t *Tokenizer) GetDuplicateGroups() []DuplicateGroup {
	t.mutex.RLock()
	defer t.mutex.RUnlock()

	duplicates := make([]DuplicateGroup, 0, len(t.duplicateGroups))
	for _, group := range t.duplicateGroups {
		if group != nil && group.Count > 1 {
			duplicates = append(duplicates, *group)
		}
	}
	return duplicates
}

// GetFileStats returns statistics for a specific file
func (t *Tokenizer) GetFileStats(filePath string) (FileStats, bool) {
	t.mutex.RLock()
	defer t.mutex.RUnlock()

	if stats, exists := t.fileStats[filePath]; exists && stats != nil {
		return *stats, true
	}
	return FileStats{}, false
}

// GetCacheStats returns cache performance statistics
func (t *Tokenizer) GetCacheStats() CacheStats {
	t.mutex.RLock()
	defer t.mutex.RUnlock()
	return t.cacheStats
}

// normalizeSecret performs various normalizations on secret values for better correlation
func (t *Tokenizer) normalizeSecret(value string) string {
	// Track original value for statistics
	originalValue := value

	// 1. Trim whitespace
	value = strings.TrimSpace(value)

	// 2. Handle common case variations (but preserve case for actual secrets)
	// Only normalize if it looks like a common pattern, not actual credentials
	if len(value) < 8 { // Short values might be user names, etc.
		// Check if it's all letters (might be username)
		if matched, _ := regexp.MatchString(`^[a-zA-Z]+$`, value); matched {
			value = strings.ToLower(value)
		}
	}

	// 3. Remove common prefixes/suffixes that don't change secret meaning
	prefixes := []string{"Bearer ", "Basic ", "Token ", "API_KEY=", "PASSWORD=", "SECRET="}
	for _, prefix := range prefixes {
		if strings.HasPrefix(value, prefix) {
			value = strings.TrimPrefix(value, prefix)
			break
		}
	}

	// 4. Handle quotes (both single and double)
	if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
		(strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
		value = value[1 : len(value)-1]
	}

	// 5. Normalize common connection string patterns
	// Example: "user:pass@host" vs "user: pass @ host"
	value = regexp.MustCompile(`\s*:\s*`).ReplaceAllString(value, ":")
	value = regexp.MustCompile(`\s*@\s*`).ReplaceAllString(value, "@")

	// Track normalization statistics
	if value != originalValue {
		t.cacheStats.Total++
		// This is a bit of a hack to track normalization hits
		t.normalizedSecrets[value] = originalValue
	}

	return value
}

// generateSecretHash creates a consistent hash for secret deduplication
func (t *Tokenizer) generateSecretHash(normalizedValue string) string {
	h := hmac.New(sha256.New, []byte("secret-hash-salt"))
	h.Write([]byte(normalizedValue))
	hash := h.Sum(nil)
	return hex.EncodeToString(hash[:16]) // Use first 16 bytes for shorter hash
}

// addSecretReference tracks where a token was used
func (t *Tokenizer) addSecretReference(token, filePath string) {
	if t.secretRefs == nil {
		t.secretRefs = make(map[string][]string)
	}

	// Check if file already exists for this token
	for _, existingFile := range t.secretRefs[token] {
		if existingFile == filePath {
			return // Already recorded
		}
	}

	t.secretRefs[token] = append(t.secretRefs[token], filePath)
}

// trackDuplicateSecret manages duplicate secret detection and tracking
func (t *Tokenizer) trackDuplicateSecret(secretHash, token, secretType, filePath string, normalizedValue string) {
	now := time.Now()

	if existing, exists := t.duplicateGroups[secretHash]; exists {
		// Update existing duplicate group
		existing.Count++
		existing.LastSeen = now

		// Add location if not already present
		for _, loc := range existing.Locations {
			if loc == filePath {
				return // Location already tracked
			}
		}
		existing.Locations = append(existing.Locations, filePath)
	} else {
		// Create new duplicate group
		t.duplicateGroups[secretHash] = &DuplicateGroup{
			SecretHash: secretHash,
			Token:      token,
			SecretType: secretType,
			Locations:  []string{filePath},
			Count:      1,
			FirstSeen:  now,
			LastSeen:   now,
		}
	}
}

// updateFileStats tracks statistics per file
func (t *Tokenizer) updateFileStats(filePath, secretType string) {
	if t.fileStats == nil {
		t.fileStats = make(map[string]*FileStats)
	}

	stats, exists := t.fileStats[filePath]
	if !exists {
		stats = &FileStats{
			FilePath:     filePath,
			SecretsFound: 0,
			TokensUsed:   0,
			SecretTypes:  make(map[string]int),
			ProcessedAt:  time.Now(),
		}
		t.fileStats[filePath] = stats
	}

	stats.SecretsFound++
	stats.TokensUsed++
	stats.SecretTypes[secretType]++
	stats.ProcessedAt = time.Now()
}

// Phase 2.2: Redaction Mapping System

// GenerateRedactionMapFile creates a redaction mapping file with optional encryption
func (t *Tokenizer) GenerateRedactionMapFile(profile, outputPath string, encrypt bool) error {
	// Analyze correlations before generating map
	t.analyzeCorrelations()

	// Get the redaction map
	redactionMap := t.GetRedactionMap(profile)

	// Encrypt if requested
	if encrypt {
		encryptionKey := make([]byte, 32)
		if _, err := rand.Read(encryptionKey); err != nil {
			return fmt.Errorf("failed to generate encryption key: %w", err)
		}

		encryptedMap, err := t.encryptRedactionMap(redactionMap, encryptionKey)
		if err != nil {
			return fmt.Errorf("failed to encrypt redaction map: %w", err)
		}

		redactionMap = encryptedMap
		redactionMap.IsEncrypted = true
	}

	// Marshal to JSON
	jsonData, err := json.MarshalIndent(redactionMap, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal redaction map: %w", err)
	}

	// Write to file with secure permissions
	if err := ioutil.WriteFile(outputPath, jsonData, 0600); err != nil {
		return fmt.Errorf("failed to write redaction map file: %w", err)
	}

	return nil
}

// encryptRedactionMap encrypts sensitive parts of the redaction map
func (t *Tokenizer) encryptRedactionMap(redactionMap RedactionMap, encryptionKey []byte) (RedactionMap, error) {
	// Create cipher
	block, err := aes.NewCipher(encryptionKey)
	if err != nil {
		return redactionMap, fmt.Errorf("failed to create cipher: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return redactionMap, fmt.Errorf("failed to create GCM: %w", err)
	}

	// Encrypt the tokens map
	encryptedTokens := make(map[string]string)
	for token, originalValue := range redactionMap.Tokens {
		// Generate nonce
		nonce := make([]byte, gcm.NonceSize())
		if _, err := rand.Read(nonce); err != nil {
			return redactionMap, fmt.Errorf("failed to generate nonce: %w", err)
		}

		// Encrypt the original value
		encryptedValue := gcm.Seal(nonce, nonce, []byte(originalValue), nil)
		encryptedTokens[token] = hex.EncodeToString(encryptedValue)
	}

	// Create encrypted copy
	encryptedMap := redactionMap
	encryptedMap.Tokens = encryptedTokens
	encryptedMap.EncryptionKey = encryptionKey // Store key (won't be serialized due to json:"-")
	encryptedMap.IsEncrypted = true            // Mark as encrypted

	return encryptedMap, nil
}

// decryptRedactionMap decrypts an encrypted redaction map
func (t *Tokenizer) decryptRedactionMap(encryptedMap RedactionMap, encryptionKey []byte) (RedactionMap, error) {
	if !encryptedMap.IsEncrypted {
		return encryptedMap, nil // Not encrypted
	}

	// Create cipher
	block, err := aes.NewCipher(encryptionKey)
	if err != nil {
		return encryptedMap, fmt.Errorf("failed to create cipher: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return encryptedMap, fmt.Errorf("failed to create GCM: %w", err)
	}

	// Decrypt the tokens map
	decryptedTokens := make(map[string]string)
	for token, encryptedValue := range encryptedMap.Tokens {
		// Decode hex
		encryptedBytes, err := hex.DecodeString(encryptedValue)
		if err != nil {
			continue // Skip malformed entries
		}

		if len(encryptedBytes) < gcm.NonceSize() {
			continue // Invalid data
		}

		// Extract nonce and ciphertext
		nonce := encryptedBytes[:gcm.NonceSize()]
		ciphertext := encryptedBytes[gcm.NonceSize():]

		// Decrypt
		decryptedBytes, err := gcm.Open(nil, nonce, ciphertext, nil)
		if err != nil {
			continue // Skip failed decryptions
		}

		decryptedTokens[token] = string(decryptedBytes)
	}

	// Create decrypted copy
	decryptedMap := encryptedMap
	decryptedMap.Tokens = decryptedTokens
	decryptedMap.IsEncrypted = false

	return decryptedMap, nil
}

// LoadRedactionMapFile loads and optionally decrypts a redaction mapping file
func LoadRedactionMapFile(filePath string, encryptionKey []byte) (RedactionMap, error) {
	// Read file
	jsonData, err := ioutil.ReadFile(filePath)
	if err != nil {
		return RedactionMap{}, fmt.Errorf("failed to read redaction map file: %w", err)
	}

	// Parse JSON
	var redactionMap RedactionMap
	if err := json.Unmarshal(jsonData, &redactionMap); err != nil {
		return RedactionMap{}, fmt.Errorf("failed to parse redaction map: %w", err)
	}

	// Decrypt if needed and key provided
	if redactionMap.IsEncrypted && len(encryptionKey) > 0 {
		tokenizer := &Tokenizer{} // Temporary instance for decryption
		decryptedMap, err := tokenizer.decryptRedactionMap(redactionMap, encryptionKey)
		if err != nil {
			return RedactionMap{}, fmt.Errorf("failed to decrypt redaction map: %w", err)
		}
		return decryptedMap, nil
	}

	return redactionMap, nil
}

// ValidateRedactionMapFile validates the structure and integrity of a redaction map file
func ValidateRedactionMapFile(filePath string) error {
	redactionMap, err := LoadRedactionMapFile(filePath, nil)
	if err != nil {
		return err
	}

	// Basic validation checks
	if redactionMap.BundleID == "" {
		return fmt.Errorf("invalid redaction map: missing bundle ID")
	}

	if redactionMap.Stats.TotalSecrets != len(redactionMap.Tokens) {
		return fmt.Errorf("invalid redaction map: stats mismatch (expected %d secrets, found %d)",
			redactionMap.Stats.TotalSecrets, len(redactionMap.Tokens))
	}

	// Validate token format
	tokenizer := &Tokenizer{}
	for token := range redactionMap.Tokens {
		if !tokenizer.ValidateToken(token) {
			return fmt.Errorf("invalid token format: %s", token)
		}
	}

	return nil
}
