// Package cerberus provides lightweight security checks (WAF, ACL, CrowdSec) with notification support.
package cerberus

import (
	"context"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"github.com/Wikid82/charon/backend/internal/config"
	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/Wikid82/charon/backend/internal/metrics"
	"github.com/Wikid82/charon/backend/internal/models"
	securitypkg "github.com/Wikid82/charon/backend/internal/security"
	"github.com/Wikid82/charon/backend/internal/services"
	"github.com/Wikid82/charon/backend/internal/util"
)

// Cerberus provides a lightweight facade for security checks (WAF, CrowdSec, ACL).
type Cerberus struct {
	cfg               config.SecurityConfig
	db                *gorm.DB
	accessSvc         *services.AccessListService
	securityNotifySvc *services.SecurityNotificationService
	enhancedNotifySvc *services.EnhancedSecurityNotificationService

	// Settings cache for performance - avoids DB queries on every request
	settingsCache     map[string]string
	settingsCacheMu   sync.RWMutex
	settingsCacheTime time.Time
	settingsCacheTTL  time.Duration
}

// New creates a new Cerberus instance
func New(cfg config.SecurityConfig, db *gorm.DB) *Cerberus {
	return &Cerberus{
		cfg:               cfg,
		db:                db,
		accessSvc:         services.NewAccessListService(db),
		securityNotifySvc: services.NewSecurityNotificationService(db),
		enhancedNotifySvc: services.NewEnhancedSecurityNotificationService(db),
		settingsCache:     make(map[string]string),
		settingsCacheTTL:  60 * time.Second,
	}
}

// getSetting retrieves a setting with in-memory caching.
// Returns the value and a boolean indicating if the key was found.
func (c *Cerberus) getSetting(key string) (string, bool) {
	if c.db == nil {
		return "", false
	}

	// Fast path: check cache with read lock
	c.settingsCacheMu.RLock()
	if time.Since(c.settingsCacheTime) < c.settingsCacheTTL {
		val, ok := c.settingsCache[key]
		c.settingsCacheMu.RUnlock()
		return val, ok
	}
	c.settingsCacheMu.RUnlock()

	// Slow path: refresh cache with write lock
	c.settingsCacheMu.Lock()
	defer c.settingsCacheMu.Unlock()

	// Double-check: another goroutine might have refreshed cache
	if time.Since(c.settingsCacheTime) < c.settingsCacheTTL {
		val, ok := c.settingsCache[key]
		return val, ok
	}

	// Refresh entire cache from DB (batch query is faster than individual queries)
	var settings []models.Setting
	if err := c.db.Where("key LIKE ?", "security.%").Find(&settings).Error; err != nil {
		logger.Log().WithError(err).Debug("Failed to refresh settings cache")
		return "", false
	}

	// Update cache
	c.settingsCache = make(map[string]string)
	for _, s := range settings {
		c.settingsCache[s.Key] = s.Value
	}
	c.settingsCacheTime = time.Now()

	val, ok := c.settingsCache[key]
	return val, ok
}

// InvalidateCache forces cache refresh on next access.
// Call this after updating security settings.
func (c *Cerberus) InvalidateCache() {
	c.settingsCacheMu.Lock()
	c.settingsCacheTime = time.Time{} // Zero time forces refresh
	c.settingsCacheMu.Unlock()
}

// IsEnabled returns whether Cerberus features are enabled via config or settings.
func (c *Cerberus) IsEnabled() bool {
	// DB-backed break-glass disable must take effect even when static config defaults to enabled.
	// This keeps the API reachable and prevents accidental lockouts when Cerberus/ACL is disabled via /security/disable.
	if c.db != nil {
		var sc models.SecurityConfig
		if err := c.db.Where("name = ?", "default").First(&sc).Error; err == nil {
			if !sc.Enabled {
				return false
			}
		}

		var s models.Setting
		// Runtime feature flag (highest priority after break-glass disable)
		if err := c.db.Where("key = ?", "feature.cerberus.enabled").First(&s).Error; err == nil {
			return strings.EqualFold(s.Value, "true")
		}
		// Fallback to legacy setting for backward compatibility
		s = models.Setting{} // Reset to prevent ID leakage from previous query
		if err := c.db.Where("key = ?", "security.cerberus.enabled").First(&s).Error; err == nil {
			return strings.EqualFold(s.Value, "true")
		}
	}

	if c.cfg.CerberusEnabled {
		return true
	}

	// If any of the security modes are explicitly enabled, consider Cerberus enabled.
	// Treat empty values as disabled to avoid treating zero-values ("") as enabled.
	if c.cfg.CrowdSecMode == "local" {
		return true
	}
	if (c.cfg.WAFMode != "" && c.cfg.WAFMode != "disabled") || c.cfg.RateLimitMode == "enabled" || c.cfg.ACLMode == "enabled" {
		return true
	}

	// Back-compat: check if all config fields are their zero values (implies defaults = enabled)
	// Note: cannot use == for struct comparison when it contains slices
	if c.cfg.CrowdSecMode == "" && c.cfg.CrowdSecAPIURL == "" && c.cfg.CrowdSecAPIKey == "" &&
		c.cfg.CrowdSecConfigDir == "" && c.cfg.WAFMode == "" && c.cfg.RateLimitMode == "" &&
		c.cfg.ACLMode == "" && !c.cfg.CerberusEnabled && len(c.cfg.ManagementCIDRs) == 0 {
		return true
	}

	return false
}

// Middleware returns a Gin middleware that enforces Cerberus checks when enabled.
func (c *Cerberus) Middleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// Check for emergency bypass flag (set by EmergencyBypass middleware)
		if bypass, exists := ctx.Get("emergency_bypass"); exists && bypass.(bool) {
			logger.Log().WithField("path", util.SanitizeForLog(ctx.Request.URL.Path)).Debug("Cerberus: Skipping security checks (emergency bypass)")
			ctx.Next()
			return
		}

		if !c.IsEnabled() {
			ctx.Next()
			return
		}

		// WAF: The actual WAF protection is handled by the Coraza plugin at the Caddy layer.
		// This middleware just tracks metrics for requests when WAF is enabled.
		// Check runtime setting first (from cache), then fall back to static config.
		wafEnabled := c.cfg.WAFMode != "" && c.cfg.WAFMode != "disabled"
		if val, ok := c.getSetting("security.waf.enabled"); ok {
			wafEnabled = strings.EqualFold(val, "true")
		}
		if wafEnabled {
			metrics.IncWAFRequest()
			// Note: Actual blocking is done by Coraza in Caddy. This middleware
			// provides defense-in-depth tracking and ACL enforcement only.
		}

		// Rate Limit: Actual rate limiting is done by Caddy middleware.
		// Notifications are sent when Caddy middleware detects limit exceeded via webhook.
		// No per-request tracking needed here (Blocker 1: Production runtime dispatch for rate limit hits).

		// ACL: simple per-request evaluation against all access lists if enabled
		// Check runtime setting first (from cache), then fall back to static config.
		aclEnabled := c.cfg.ACLMode == "enabled"
		if val, ok := c.getSetting("security.acl.enabled"); ok {
			aclEnabled = strings.EqualFold(val, "true")
		}

		if aclEnabled {
			clientIP := util.CanonicalizeIPForSecurity(ctx.ClientIP())
			isAdmin := c.isAuthenticatedAdmin(ctx)
			adminWhitelistConfigured := false
			if isAdmin {
				whitelisted, hasWhitelist := c.adminWhitelistStatus(clientIP)
				adminWhitelistConfigured = hasWhitelist
				if whitelisted {
					ctx.Next()
					return
				}
			}

			acls, err := c.accessSvc.List()
			if err == nil {
				activeCount := 0
				for _, acl := range acls {
					if !acl.Enabled {
						continue
					}
					activeCount++
					allowed, _, err := c.accessSvc.TestIP(acl.ID, clientIP)
					if err == nil && !allowed {
						// Send security notification via appropriate dispatch path
						_ = c.sendSecurityNotification(context.Background(), models.SecurityEvent{
							EventType: "acl_deny",
							Severity:  "warn",
							Message:   "Access control list blocked request",
							ClientIP:  clientIP,
							Path:      ctx.Request.URL.Path,
							Timestamp: time.Now(),
							Metadata: map[string]any{
								"acl_name": acl.Name,
								"acl_id":   acl.ID,
							},
						})

						ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Blocked by access control list"})
						return
					}
				}
				if activeCount == 0 {
					if isAdmin && !adminWhitelistConfigured {
						ctx.Next()
						return
					}
					ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Blocked by access control list"})
					return
				}
			}
		}

		// CrowdSec integration: The actual IP blocking is handled by the caddy-crowdsec-bouncer
		// plugin at the Caddy layer. This middleware provides defense-in-depth tracking.
		// When CrowdSec mode is "local", the bouncer communicates directly with the LAPI
		// to receive ban decisions and block malicious IPs before they reach the application.
		if c.cfg.CrowdSecMode == "local" {
			// Track that this request passed through CrowdSec evaluation
			// Note: Blocking decisions are made by Caddy bouncer, not here
			metrics.IncCrowdSecRequest()
			logger.Log().WithField("client_ip", util.SanitizeForLog(ctx.ClientIP())).WithField("path", util.SanitizeForLog(ctx.Request.URL.Path)).Debug("Request evaluated by CrowdSec bouncer at Caddy layer")
			// Blocker 1: Production runtime dispatch for CrowdSec decisions
			// CrowdSec decisions trigger notifications when bouncer blocks at Caddy layer
			// The actual notification is sent by Caddy via webhook callback to /api/v1/security/events
		}

		ctx.Next()
	}
}

// NotifySecurityEvent provides external notification hook for security events from Caddy/Coraza.
// Blocker 1: Production runtime dispatch path for WAF blocks, Rate limit hits, and CrowdSec decisions.
func (c *Cerberus) NotifySecurityEvent(ctx *gin.Context, event models.SecurityEvent) error {
	if !c.IsEnabled() {
		return nil
	}
	return c.sendSecurityNotification(ctx.Request.Context(), event)
}

func (c *Cerberus) isAuthenticatedAdmin(ctx *gin.Context) bool {
	role, exists := ctx.Get("role")
	if !exists {
		return false
	}
	roleStr, ok := role.(string)
	if !ok || roleStr != string(models.RoleAdmin) {
		return false
	}
	userID, exists := ctx.Get("userID")
	if !exists {
		return false
	}
	switch id := userID.(type) {
	case uint:
		return id > 0
	case int:
		return id > 0
	case int64:
		return id > 0
	default:
		return false
	}
}

func (c *Cerberus) adminWhitelistStatus(clientIP string) (bool, bool) {
	if c.db == nil {
		return false, false
	}

	var sc models.SecurityConfig
	if err := c.db.Where("name = ?", "default").First(&sc).Error; err != nil {
		return false, false
	}
	if strings.TrimSpace(sc.AdminWhitelist) == "" {
		return false, false
	}

	return securitypkg.IsIPInCIDRList(clientIP, sc.AdminWhitelist), true
}

// sendSecurityNotification dispatches a security event notification.
// Blocker 1: Wires runtime dispatch to provider-event authority under feature flag semantics.
// Blocker 2: Enforces notify-only fail-closed behavior - no legacy fallback when flag absent/false.
func (c *Cerberus) sendSecurityNotification(ctx context.Context, event models.SecurityEvent) error {
	if c.db == nil {
		return nil
	}

	// Check feature flag
	var setting models.Setting
	err := c.db.Where("key = ?", "feature.notifications.security_provider_events.enabled").First(&setting).Error

	// If feature flag is enabled, use provider-based dispatch
	if err == nil && strings.EqualFold(setting.Value, "true") {
		return c.enhancedNotifySvc.SendViaProviders(ctx, event)
	}

	// Blocker 2: Feature flag disabled or not found - fail closed (no notification, no legacy fallback)
	logger.Log().WithField("event_type", event.EventType).Debug("Security notification suppressed: feature flag disabled or absent")
	return nil
}
