package services

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"

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

var (
	ErrSecurityConfigNotFound = errors.New("security config not found")
	ErrInvalidAdminCIDR       = errors.New("invalid admin whitelist CIDR")
	ErrBreakGlassInvalid      = errors.New("break-glass token invalid")
)

type SecurityService struct {
	db *gorm.DB
}

// NewSecurityService returns a SecurityService using the provided DB
func NewSecurityService(db *gorm.DB) *SecurityService {
	return &SecurityService{db: db}
}

// Get returns the first SecurityConfig row (singleton config)
func (s *SecurityService) Get() (*models.SecurityConfig, error) {
	var cfg models.SecurityConfig
	if err := s.db.First(&cfg).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrSecurityConfigNotFound
		}
		return nil, err
	}
	return &cfg, nil
}

// Upsert validates and saves a security config
func (s *SecurityService) Upsert(cfg *models.SecurityConfig) error {
	// Validate AdminWhitelist - comma-separated list of CIDRs
	if cfg.AdminWhitelist != "" {
		parts := strings.Split(cfg.AdminWhitelist, ",")
		for _, p := range parts {
			p = strings.TrimSpace(p)
			if p == "" {
				continue
			}
			// Validate as IP or CIDR using the same helper as AccessListService
			if !isValidCIDR(p) {
				return ErrInvalidAdminCIDR
			}
		}
	}

	// If a breakglass token is present in BreakGlassHash as empty string,
	// do not overwrite it here. Token generation should be done explicitly.

	// Validate CrowdSec mode on input prior to any DB operations: only 'local' or 'disabled' supported
	if cfg.CrowdSecMode != "" && cfg.CrowdSecMode != "local" && cfg.CrowdSecMode != "disabled" {
		return fmt.Errorf("invalid crowdsec mode: %s", cfg.CrowdSecMode)
	}

	// Upsert behaviour: try to find existing record
	var existing models.SecurityConfig
	if err := s.db.Where("name = ?", cfg.Name).First(&existing).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			// New record
			return s.db.Create(cfg).Error
		}
		return err
	}

	// Preserve existing BreakGlassHash if not provided
	if cfg.BreakGlassHash == "" {
		cfg.BreakGlassHash = existing.BreakGlassHash
	}
	existing.Enabled = cfg.Enabled
	existing.AdminWhitelist = cfg.AdminWhitelist
	// Validate CrowdSec mode: only 'local' or 'disabled' supported. Reject external/remote values.
	if cfg.CrowdSecMode != "" && cfg.CrowdSecMode != "local" && cfg.CrowdSecMode != "disabled" {
		return fmt.Errorf("invalid crowdsec mode: %s", cfg.CrowdSecMode)
	}
	existing.CrowdSecMode = cfg.CrowdSecMode
	existing.CrowdSecAPIURL = cfg.CrowdSecAPIURL
	existing.WAFMode = cfg.WAFMode
	existing.WAFRulesSource = cfg.WAFRulesSource
	existing.WAFLearning = cfg.WAFLearning
	existing.WAFParanoiaLevel = cfg.WAFParanoiaLevel
	existing.WAFExclusions = cfg.WAFExclusions
	existing.RateLimitEnable = cfg.RateLimitEnable
	existing.RateLimitBurst = cfg.RateLimitBurst
	existing.RateLimitRequests = cfg.RateLimitRequests
	existing.RateLimitWindowSec = cfg.RateLimitWindowSec
	existing.RateLimitBypassList = cfg.RateLimitBypassList

	return s.db.Save(&existing).Error
}

// GenerateBreakGlassToken generates a token, stores its bcrypt hash, and returns the plaintext token
func (s *SecurityService) GenerateBreakGlassToken(name string) (string, error) {
	tokenBytes := make([]byte, 24)
	if _, err := rand.Read(tokenBytes); err != nil {
		return "", err
	}
	token := hex.EncodeToString(tokenBytes)

	hash, err := bcrypt.GenerateFromPassword([]byte(token), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}

	var cfg models.SecurityConfig
	if err := s.db.Where("name = ?", name).First(&cfg).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			cfg = models.SecurityConfig{Name: name, BreakGlassHash: string(hash)}
			if err := s.db.Create(&cfg).Error; err != nil {
				return "", err
			}
			return token, nil
		}
		return "", err
	}

	cfg.BreakGlassHash = string(hash)
	if err := s.db.Save(&cfg).Error; err != nil {
		return "", err
	}
	return token, nil
}

// VerifyBreakGlassToken validates a provided token against the stored hash
func (s *SecurityService) VerifyBreakGlassToken(name, token string) (bool, error) {
	var cfg models.SecurityConfig
	if err := s.db.Where("name = ?", name).First(&cfg).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, ErrSecurityConfigNotFound
		}
		return false, err
	}
	if cfg.BreakGlassHash == "" {
		return false, ErrBreakGlassInvalid
	}
	if err := bcrypt.CompareHashAndPassword([]byte(cfg.BreakGlassHash), []byte(token)); err != nil {
		return false, ErrBreakGlassInvalid
	}
	return true, nil
}

// LogDecision stores a security decision record
func (s *SecurityService) LogDecision(d *models.SecurityDecision) error {
	if d == nil {
		return nil
	}
	if d.UUID == "" {
		d.UUID = uuid.NewString()
	}
	if d.CreatedAt.IsZero() {
		d.CreatedAt = time.Now()
	}
	return s.db.Create(d).Error
}

// ListDecisions returns recent security decisions, ordered by created_at desc
func (s *SecurityService) ListDecisions(limit int) ([]models.SecurityDecision, error) {
	var res []models.SecurityDecision
	q := s.db.Order("created_at desc")
	if limit > 0 {
		q = q.Limit(limit)
	}
	if err := q.Find(&res).Error; err != nil {
		return nil, err
	}
	return res, nil
}

// LogAudit stores an audit entry
func (s *SecurityService) LogAudit(a *models.SecurityAudit) error {
	if a == nil {
		return nil
	}
	if a.UUID == "" {
		a.UUID = uuid.NewString()
	}
	if a.CreatedAt.IsZero() {
		a.CreatedAt = time.Now()
	}
	return s.db.Create(a).Error
}

// UpsertRuleSet saves or updates a ruleset content
func (s *SecurityService) UpsertRuleSet(r *models.SecurityRuleSet) error {
	if r == nil {
		return nil
	}
	// Basic validations
	if r.Name == "" {
		return fmt.Errorf("rule set name required")
	}
	// Prevent huge payloads from being stored in DB (e.g., limit 2MB)
	if len(r.Content) > 2*1024*1024 {
		return fmt.Errorf("ruleset content too large")
	}
	var existing models.SecurityRuleSet
	if err := s.db.Where("name = ?", r.Name).First(&existing).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			if r.UUID == "" {
				r.UUID = uuid.NewString()
			}
			if r.LastUpdated.IsZero() {
				r.LastUpdated = time.Now()
			}
			return s.db.Create(r).Error
		}
		return err
	}
	existing.SourceURL = r.SourceURL
	existing.Content = r.Content
	existing.Mode = r.Mode
	existing.LastUpdated = r.LastUpdated
	return s.db.Save(&existing).Error
}

// DeleteRuleSet removes a ruleset by id
func (s *SecurityService) DeleteRuleSet(id uint) error {
	var rs models.SecurityRuleSet
	if err := s.db.First(&rs, id).Error; err != nil {
		return err
	}
	return s.db.Delete(&rs).Error
}

// ListRuleSets returns all known rulesets
func (s *SecurityService) ListRuleSets() ([]models.SecurityRuleSet, error) {
	var res []models.SecurityRuleSet
	if err := s.db.Find(&res).Error; err != nil {
		return nil, err
	}
	return res, nil
}

// helper: reused from access_list_service validation for CIDR/IP parsing
func isValidCIDR(cidr string) bool {
	// Try parsing as single IP
	if ip := net.ParseIP(cidr); ip != nil {
		return true
	}
	// Try parsing as CIDR
	_, _, err := net.ParseCIDR(cidr)
	return err == nil
}
