package services

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/Wikid82/charon/backend/internal/security"
	"github.com/Wikid82/charon/backend/internal/util"
	"gorm.io/gorm"
)

// EnhancedSecurityNotificationService provides provider-based security notifications with compatibility layer.
type EnhancedSecurityNotificationService struct {
	db *gorm.DB
}

// NewEnhancedSecurityNotificationService creates a new enhanced service instance.
func NewEnhancedSecurityNotificationService(db *gorm.DB) *EnhancedSecurityNotificationService {
	return &EnhancedSecurityNotificationService{db: db}
}

// CompatibilitySettings represents the compatibility GET/PUT structure.
type CompatibilitySettings struct {
	SecurityWAFEnabled       bool   `json:"security_waf_enabled"`
	SecurityACLEnabled       bool   `json:"security_acl_enabled"`
	SecurityRateLimitEnabled bool   `json:"security_rate_limit_enabled"`
	Destination              string `json:"destination,omitempty"`
	DestinationAmbiguous     bool   `json:"destination_ambiguous,omitempty"`
	WebhookURL               string `json:"webhook_url,omitempty"`
	DiscordWebhookURL        string `json:"discord_webhook_url,omitempty"`
	SlackWebhookURL          string `json:"slack_webhook_url,omitempty"`
	GotifyURL                string `json:"gotify_url,omitempty"`
	GotifyToken              string `json:"-"` // Security: Never expose token in JSON (OWASP A02)
}

// MigrationMarker represents the migration state stored in settings table.
type MigrationMarker struct {
	Version         string `json:"version"`
	Checksum        string `json:"checksum"`
	LastCompletedAt string `json:"last_completed_at"`
	Result          string `json:"result"` // completed | completed_with_warnings
}

// GetSettings retrieves compatibility settings via provider aggregation (Spec Section 2).
func (s *EnhancedSecurityNotificationService) GetSettings() (*models.NotificationConfig, error) {
	// Check feature flag
	enabled, err := s.isFeatureEnabled()
	if err != nil {
		return nil, fmt.Errorf("check feature flag: %w", err)
	}

	if !enabled {
		// Feature disabled: return legacy config
		return s.getLegacyConfig()
	}

	// Feature enabled: aggregate from providers
	return s.getProviderAggregatedConfig()
}

// getProviderAggregatedConfig aggregates settings from active providers using OR semantics.
// Blocker 2: Returns proper compatibility contract with security_* fields.
// Blocker 3: Filters enabled=true AND supported notify-only provider types.
// Note: This is the GET aggregation path, not the dispatch path. All provider types are included
// for configuration visibility. Discord-only enforcement applies to SendViaProviders (dispatch path).
func (s *EnhancedSecurityNotificationService) getProviderAggregatedConfig() (*models.NotificationConfig, error) {
	var providers []models.NotificationProvider
	err := s.db.Where("enabled = ?", true).Find(&providers).Error
	if err != nil {
		return nil, fmt.Errorf("query providers: %w", err)
	}

	// Blocker 3: Filter for supported notify-only provider types (PR-1 scope)
	// All supported types are included in GET aggregation for configuration visibility
	supportedTypes := map[string]bool{
		"webhook": true,
		"discord": true,
		"slack":   true,
		"gotify":  true,
	}
	filteredProviders := []models.NotificationProvider{}
	for _, p := range providers {
		if supportedTypes[p.Type] {
			filteredProviders = append(filteredProviders, p)
		}
	}

	// OR aggregation: if ANY provider has true, result is true
	config := &models.NotificationConfig{
		NotifyWAFBlocks:         false,
		NotifyACLDenies:         false,
		NotifyRateLimitHits:     false,
		NotifyCrowdSecDecisions: false,
	}

	for _, p := range filteredProviders {
		if p.NotifySecurityWAFBlocks {
			config.NotifyWAFBlocks = true
		}
		if p.NotifySecurityACLDenies {
			config.NotifyACLDenies = true
		}
		if p.NotifySecurityRateLimitHits {
			config.NotifyRateLimitHits = true
		}
		if p.NotifySecurityCrowdSecDecisions {
			config.NotifyCrowdSecDecisions = true
		}
	}

	// Destination reporting: only if exactly one managed provider exists
	managedProviders := []models.NotificationProvider{}
	for _, p := range filteredProviders {
		if p.ManagedLegacySecurity {
			managedProviders = append(managedProviders, p)
		}
	}

	if len(managedProviders) == 1 {
		// Exactly one managed provider - report destination based on type
		p := managedProviders[0]
		switch p.Type {
		case "webhook":
			config.WebhookURL = p.URL
		case "discord":
			config.DiscordWebhookURL = p.URL
		case "slack":
			config.SlackWebhookURL = p.URL
		case "gotify":
			config.GotifyURL = p.URL
			// Blocker 2: Never expose gotify token in compatibility GET responses
			// Token remains in DB but is not returned to client
		}
		config.DestinationAmbiguous = false
	} else {
		// Zero or multiple managed providers = ambiguous
		config.DestinationAmbiguous = true
	}

	return config, nil
}

// getLegacyConfig retrieves settings from the legacy notification_configs table.
func (s *EnhancedSecurityNotificationService) getLegacyConfig() (*models.NotificationConfig, error) {
	var config models.NotificationConfig
	err := s.db.First(&config).Error
	if err == gorm.ErrRecordNotFound {
		return &models.NotificationConfig{
			NotifyWAFBlocks:     true,
			NotifyACLDenies:     true,
			NotifyRateLimitHits: true,
		}, nil
	}
	return &config, err
}

// UpdateSettings updates security notification settings via managed provider set (Spec Section 3).
func (s *EnhancedSecurityNotificationService) UpdateSettings(req *models.NotificationConfig) error {
	// Check feature flag
	enabled, err := s.isFeatureEnabled()
	if err != nil {
		return fmt.Errorf("check feature flag: %w", err)
	}

	if !enabled {
		// Feature disabled: update legacy config
		return s.updateLegacyConfig(req)
	}

	// Feature enabled: update via managed provider set
	return s.updateManagedProviders(req)
}

// updateManagedProviders updates the managed provider set with replace semantics.
// Blocker 4: Complete gotify validation - requires both URL and token, reject incomplete with 422.
func (s *EnhancedSecurityNotificationService) updateManagedProviders(req *models.NotificationConfig) error {
	// Validate destination mapping (Spec Section 5: fail-safe handling)
	destCount := 0
	var destType string

	if req.WebhookURL != "" {
		destCount++
		destType = "webhook"
	}
	if req.DiscordWebhookURL != "" {
		destCount++
		destType = "discord"
	}
	if req.SlackWebhookURL != "" {
		destCount++
		destType = "slack"
	}
	// Blocker 4: Validate gotify requires BOTH url and token
	if req.GotifyURL != "" || req.GotifyToken != "" {
		destCount++
		destType = "gotify"
		// Reject incomplete gotify payload with 422 and no mutation
		if req.GotifyURL == "" || req.GotifyToken == "" {
			return fmt.Errorf("incomplete gotify configuration: both gotify_url and gotify_token are required")
		}
	}

	if destCount > 1 {
		return fmt.Errorf("ambiguous destination: multiple destination types provided")
	}

	// Resolve deterministic target set (Spec Section 3: deterministic conflict behavior)
	return s.db.Transaction(func(tx *gorm.DB) error {
		var managedProviders []models.NotificationProvider
		err := tx.Where("managed_legacy_security = ?", true).Find(&managedProviders).Error
		if err != nil {
			return fmt.Errorf("query managed providers: %w", err)
		}

		// Blocker 4: Deterministic target set allows one-or-more managed providers
		// Update full managed set; only 409 on true non-resolvable identity corruption
		// Multiple managed providers ARE the valid target set (not corruption)

		if len(managedProviders) == 0 {
			// Create managed provider
			provider := &models.NotificationProvider{
				Name:                        "Migrated Security Notifications (Legacy)",
				Type:                        destType,
				Enabled:                     true,
				ManagedLegacySecurity:       true,
				NotifySecurityWAFBlocks:     req.NotifyWAFBlocks,
				NotifySecurityACLDenies:     req.NotifyACLDenies,
				NotifySecurityRateLimitHits: req.NotifyRateLimitHits,
				URL:                         s.extractDestinationURL(req),
				Token:                       s.extractDestinationToken(req),
			}
			return tx.Create(provider).Error
		}

		// Blocker 3: Enforce PUT idempotency - only save if values actually changed
		// Update all managed providers with replace semantics
		for i := range managedProviders {
			changed := false

			// Check if security event flags changed
			if managedProviders[i].NotifySecurityWAFBlocks != req.NotifyWAFBlocks {
				managedProviders[i].NotifySecurityWAFBlocks = req.NotifyWAFBlocks
				changed = true
			}
			if managedProviders[i].NotifySecurityACLDenies != req.NotifyACLDenies {
				managedProviders[i].NotifySecurityACLDenies = req.NotifyACLDenies
				changed = true
			}
			if managedProviders[i].NotifySecurityRateLimitHits != req.NotifyRateLimitHits {
				managedProviders[i].NotifySecurityRateLimitHits = req.NotifyRateLimitHits
				changed = true
			}

			// Update destination if provided
			if destURL := s.extractDestinationURL(req); destURL != "" {
				if managedProviders[i].URL != destURL {
					managedProviders[i].URL = destURL
					changed = true
				}
				if managedProviders[i].Type != destType {
					managedProviders[i].Type = destType
					changed = true
				}
				if managedProviders[i].Token != s.extractDestinationToken(req) {
					managedProviders[i].Token = s.extractDestinationToken(req)
					changed = true
				}
			}

			// Blocker 3: Only save (update timestamps) if values actually changed
			if changed {
				if err := tx.Save(&managedProviders[i]).Error; err != nil {
					return fmt.Errorf("update provider %s: %w", managedProviders[i].ID, err)
				}
			}
		}

		return nil
	})
}

// extractDestinationURL extracts the destination URL from the request.
func (s *EnhancedSecurityNotificationService) extractDestinationURL(req *models.NotificationConfig) string {
	if req.WebhookURL != "" {
		return req.WebhookURL
	}
	if req.DiscordWebhookURL != "" {
		return req.DiscordWebhookURL
	}
	if req.SlackWebhookURL != "" {
		return req.SlackWebhookURL
	}
	if req.GotifyURL != "" {
		return req.GotifyURL
	}
	return ""
}

// extractDestinationToken extracts the auth token from the request (currently only gotify).
func (s *EnhancedSecurityNotificationService) extractDestinationToken(req *models.NotificationConfig) string {
	if req.GotifyToken != "" {
		return req.GotifyToken
	}
	return ""
}

// updateLegacyConfig updates the legacy notification_configs table.
func (s *EnhancedSecurityNotificationService) updateLegacyConfig(req *models.NotificationConfig) error {
	var existing models.NotificationConfig
	err := s.db.First(&existing).Error

	if err == gorm.ErrRecordNotFound {
		return s.db.Create(req).Error
	}

	if err != nil {
		return fmt.Errorf("fetch existing config: %w", err)
	}

	req.ID = existing.ID
	return s.db.Save(req).Error
}

// MigrateFromLegacyConfig performs deterministic migration from legacy config to managed provider (Spec Section 4).
// Blocker 2: Respects feature flag - does NOT mutate providers when flag=false.
func (s *EnhancedSecurityNotificationService) MigrateFromLegacyConfig() error {
	// Check feature flag first
	enabled, err := s.isFeatureEnabled()
	if err != nil {
		return fmt.Errorf("check feature flag: %w", err)
	}

	// Read legacy config
	var legacyConfig models.NotificationConfig
	err = s.db.First(&legacyConfig).Error
	if err == gorm.ErrRecordNotFound {
		// No legacy config to migrate
		return nil
	}
	if err != nil {
		return fmt.Errorf("read legacy config: %w", err)
	}

	// Compute checksum
	checksum := computeConfigChecksum(legacyConfig)

	// Read migration marker
	var markerSetting models.Setting
	err = s.db.Where("key = ?", "notifications.security_provider_events.migration.v1").First(&markerSetting).Error

	if err == nil {
		// Marker exists - check if checksum matches
		var marker MigrationMarker
		if err := json.Unmarshal([]byte(markerSetting.Value), &marker); err != nil {
			logger.Log().WithError(err).Warn("Failed to unmarshal migration marker")
		} else if marker.Checksum == checksum {
			// Checksum matches - no-op
			return nil
		}
	}

	// If feature flag is disabled, perform dry-evaluate only (no mutation)
	if !enabled {
		logger.Log().Info("Feature flag disabled - migration runs in read-only mode (no provider mutation)")
		return nil
	}

	// Perform migration in transaction
	return s.db.Transaction(func(tx *gorm.DB) error {
		// Upsert managed provider
		var provider models.NotificationProvider
		err := tx.Where("managed_legacy_security = ?", true).First(&provider).Error

		if err == gorm.ErrRecordNotFound {
			// Create new managed provider
			provider = models.NotificationProvider{
				Name:                        "Migrated Security Notifications (Legacy)",
				Type:                        "webhook",
				Enabled:                     true,
				ManagedLegacySecurity:       true,
				NotifySecurityWAFBlocks:     legacyConfig.NotifyWAFBlocks,
				NotifySecurityACLDenies:     legacyConfig.NotifyACLDenies,
				NotifySecurityRateLimitHits: legacyConfig.NotifyRateLimitHits,
				URL:                         legacyConfig.WebhookURL,
			}
			if createErr := tx.Create(&provider).Error; createErr != nil {
				return fmt.Errorf("create managed provider: %w", createErr)
			}
		} else if err != nil {
			return fmt.Errorf("query managed provider: %w", err)
		} else {
			// Update existing managed provider
			provider.NotifySecurityWAFBlocks = legacyConfig.NotifyWAFBlocks
			provider.NotifySecurityACLDenies = legacyConfig.NotifyACLDenies
			provider.NotifySecurityRateLimitHits = legacyConfig.NotifyRateLimitHits
			provider.URL = legacyConfig.WebhookURL
			if saveErr := tx.Save(&provider).Error; saveErr != nil {
				return fmt.Errorf("update managed provider: %w", saveErr)
			}
		}

		// Write migration marker
		marker := MigrationMarker{
			Version:         "v1",
			Checksum:        checksum,
			LastCompletedAt: time.Now().UTC().Format(time.RFC3339),
			Result:          "completed",
		}
		markerJSON, err := json.Marshal(marker)
		if err != nil {
			return fmt.Errorf("marshal marker: %w", err)
		}

		newMarkerSetting := models.Setting{
			Key:      "notifications.security_provider_events.migration.v1",
			Value:    string(markerJSON),
			Type:     "json",
			Category: "notifications",
		}

		// Upsert marker
		if queryErr := tx.Where("key = ?", newMarkerSetting.Key).First(&markerSetting).Error; queryErr == gorm.ErrRecordNotFound {
			return tx.Create(&newMarkerSetting).Error
		}
		newMarkerSetting.ID = markerSetting.ID
		return tx.Save(&newMarkerSetting).Error
	})
}

// computeConfigChecksum computes a deterministic checksum from legacy config fields.
func computeConfigChecksum(config models.NotificationConfig) string {
	// Create deterministic string representation
	fields := []string{
		fmt.Sprintf("waf:%t", config.NotifyWAFBlocks),
		fmt.Sprintf("acl:%t", config.NotifyACLDenies),
		fmt.Sprintf("rate:%t", config.NotifyRateLimitHits),
		fmt.Sprintf("url:%s", config.WebhookURL),
	}
	sort.Strings(fields) // Ensure field order doesn't affect checksum

	data := ""
	for _, f := range fields {
		data += f + "|"
	}

	hash := sha256.Sum256([]byte(data))
	return hex.EncodeToString(hash[:])
}

// isFeatureEnabled checks the feature flag in settings table (Spec Section 6).
func (s *EnhancedSecurityNotificationService) isFeatureEnabled() (bool, error) {
	var setting models.Setting
	err := s.db.Where("key = ?", "feature.notifications.security_provider_events.enabled").First(&setting).Error

	if err == gorm.ErrRecordNotFound {
		// Blocker 5: Implement feature flag defaults exactly as per spec
		// Initialize based on environment detection
		defaultValue := s.getDefaultFeatureFlagValue()

		// Create the setting with appropriate default
		newSetting := models.Setting{
			Key:      "feature.notifications.security_provider_events.enabled",
			Value:    defaultValue,
			Type:     "bool",
			Category: "feature",
		}

		if createErr := s.db.Create(&newSetting).Error; createErr != nil {
			// If creation fails (e.g., race condition), re-query
			if queryErr := s.db.Where("key = ?", newSetting.Key).First(&setting).Error; queryErr != nil {
				return defaultValue == "true", fmt.Errorf("create and requery feature flag: %w", queryErr)
			}
			return setting.Value == "true", nil
		}

		return defaultValue == "true", nil
	}

	if err != nil {
		return false, fmt.Errorf("query feature flag: %w", err)
	}

	return setting.Value == "true", nil
}

// SendViaProviders dispatches security events to active providers.
// When feature flag is enabled, this is the authoritative dispatch path.
// Blocker 3: Discord-only enforcement for rollout - only Discord providers receive security events.
// Server-side guarantee holds for existing rows and all dispatch paths.
func (s *EnhancedSecurityNotificationService) SendViaProviders(ctx context.Context, event models.SecurityEvent) error {
	// Query active providers that have the relevant event type enabled
	var providers []models.NotificationProvider
	err := s.db.Where("enabled = ?", true).Find(&providers).Error
	if err != nil {
		return fmt.Errorf("query providers: %w", err)
	}

	// Blocker 3: Discord-only enforcement for rollout stage
	// ONLY Discord providers are allowed to receive security events
	// This is a server-side guarantee that prevents any non-Discord provider
	// from receiving security notifications, even if flags are enabled in DB
	supportedTypes := map[string]bool{
		"discord": true,
		// webhook, slack, gotify explicitly excluded for rollout
	}

	// Filter providers based on event type AND supported type
	var targetProviders []models.NotificationProvider
	for _, p := range providers {
		if !supportedTypes[p.Type] {
			continue
		}
		shouldNotify := false
		// Normalize event type to handle variations
		normalizedEventType := normalizeSecurityEventType(event.EventType)
		switch normalizedEventType {
		case "waf_block":
			shouldNotify = p.NotifySecurityWAFBlocks
		case "acl_deny":
			shouldNotify = p.NotifySecurityACLDenies
		case "rate_limit":
			shouldNotify = p.NotifySecurityRateLimitHits
		case "crowdsec_decision":
			shouldNotify = p.NotifySecurityCrowdSecDecisions
		}
		if shouldNotify {
			targetProviders = append(targetProviders, p)
		}
	}

	if len(targetProviders) == 0 {
		// No providers configured for this event type - fail closed (no notification)
		logger.Log().WithField("event_type", util.SanitizeForLog(event.EventType)).Debug("No providers configured for security event")
		return nil
	}

	// Dispatch to all target providers (best-effort, log failures but don't block)
	for _, p := range targetProviders {
		if err := s.dispatchToProvider(ctx, p, event); err != nil {
			logger.Log().WithError(err).WithField("provider_id", p.ID).Error("Failed to dispatch to provider")
			// Continue to next provider (best-effort)
		}
	}

	return nil
}

// dispatchToProvider sends the event to a single provider.
// Discord-only enforcement: rejects all non-Discord providers in this rollout.
func (s *EnhancedSecurityNotificationService) dispatchToProvider(ctx context.Context, provider models.NotificationProvider, event models.SecurityEvent) error {
	// Discord-only enforcement for rollout: reject non-Discord types explicitly
	if provider.Type != "discord" {
		return fmt.Errorf("discord-only rollout: provider type %q is not supported; only discord is enabled", provider.Type)
	}
	// Discord dispatch via webhook
	return s.sendWebhook(ctx, provider.URL, event)
}

// sendWebhook sends a security event to a webhook URL (shared with legacy service).
// Blocker 4: SSRF-safe URL validation before outbound requests.
func (s *EnhancedSecurityNotificationService) sendWebhook(ctx context.Context, webhookURL string, event models.SecurityEvent) error {
	// Blocker 4: Validate URL before making outbound request (SSRF protection)
	validatedURL, err := security.ValidateExternalURL(webhookURL,
		security.WithAllowHTTP(),      // Allow HTTP for backwards compatibility
		security.WithAllowLocalhost(), // Allow localhost for testing
	)
	if err != nil {
		return fmt.Errorf("ssrf validation failed: %w", err)
	}

	payload, err := json.Marshal(event)
	if err != nil {
		return fmt.Errorf("marshal event: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", validatedURL, bytes.NewBuffer(payload))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "Charon-Cerberus/1.0")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("execute request: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("webhook returned status %d", resp.StatusCode)
	}

	return nil
}

// normalizeSecurityEventType normalizes event type variations to canonical forms.
func normalizeSecurityEventType(eventType string) string {
	normalized := strings.ToLower(strings.TrimSpace(eventType))

	// Map variations to canonical forms
	switch {
	case strings.Contains(normalized, "waf"):
		return "waf_block"
	case strings.Contains(normalized, "acl"):
		return "acl_deny"
	case strings.Contains(normalized, "rate") && strings.Contains(normalized, "limit"):
		return "rate_limit"
	case strings.Contains(normalized, "crowdsec"):
		return "crowdsec_decision"
	default:
		return normalized
	}
}

// getDefaultFeatureFlagValue returns default based on environment (Spec Section 6).
// Blocker 1: Reliable prod=false, dev/test=true without fragile markers.
// Production detection: CHARON_ENV=production OR (CHARON_ENV unset AND GIN_MODE unset)
func (s *EnhancedSecurityNotificationService) getDefaultFeatureFlagValue() string {
	// Explicit production declaration
	charonEnv := os.Getenv("CHARON_ENV")
	if charonEnv == "production" || charonEnv == "prod" {
		return "false" // Production: default disabled
	}

	// Check if we're in a test environment via test marker (inserted by test setup)
	var testMarker models.Setting
	err := s.db.Where("key = ?", "_test_mode_marker").First(&testMarker).Error
	if err == nil && testMarker.Value == "true" {
		return "true" // Test environment
	}

	// Check GIN_MODE for dev/test detection
	ginMode := os.Getenv("GIN_MODE")
	if ginMode == "debug" || ginMode == "test" {
		return "true" // Development/test
	}

	// Blocker 1 Fix: When both CHARON_ENV and GIN_MODE are unset, assume production
	// Production systems should be explicit with CHARON_ENV=production, but default to safe (disabled)
	if charonEnv == "" && ginMode == "" {
		return "false" // Unset env vars = production default
	}

	// All other cases: enable for dev/test safety
	return "true"
}
