package service

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"regexp"
	"slices"
	"strings"
	"time"

	"gorm.io/gorm"

	"security-center-backend/internal/middleware"
	coremodel "security-center-backend/internal/model"
	vulnmodel "security-center-backend/internal/modules/vuln/model"
	"security-center-backend/internal/modules/vuln/repository"
	corerepo "security-center-backend/internal/repository"
	"security-center-backend/internal/service/directory"
)

const (
	permissionViewFindings     = "vuln:view_findings"
	permissionManageFindings   = "vuln:manage_findings"
	permissionViewSensitive    = "vuln:view_sensitive_details"
	permissionEditSensitive    = "vuln:edit_sensitive_details"
	permissionEditDetails      = "vuln:edit_details"
	permissionAuthorizeViewers = "vuln:authorize_viewers"
	permissionAssignOwner      = "vuln:assign_owner"
	permissionRequestRetest    = "vuln:request_retest"
	permissionCloseFindings    = "vuln:close_findings"
	actionViewSensitiveDetails = "view_sensitive_details"
	actionEditSensitiveDetails = "edit_sensitive_details"
	actionEditDetails          = "edit_details"
	actionAuthorizeViewers     = "authorize_viewers"
	actionRequestRetest        = "request_retest"
	actionReopen               = "reopen"
)

type Service struct {
	repo               repository.VulnerabilityRepository
	channelRepo        repository.DiscoveryChannelRepository
	typeRepo           repository.VulnerabilityTypeRepository
	sourceRepo         repository.SourceRepository
	eventsRepo         repository.VulnerabilityEventRepository
	commentsRepo       repository.VulnerabilityCommentRepository
	attachRepo         repository.VulnerabilityAttachmentRepository
	slaConfig          map[string]int
	slaRepo            repository.SLARuleRepository
	requiredFieldRepo  repository.SubmissionRequiredFieldRepository
	userRepo           corerepo.UserRepository
	employeeRepo       directory.EmployeeDirectory
	pickerReadsBlocked func() bool
}

const (
	SubmissionFieldTitle                = "title"
	SubmissionFieldSeverityLevel        = "severity_level"
	SubmissionFieldVulnerabilityType    = "vulnerability_type"
	SubmissionFieldAffectedAssetsCount  = "affected_assets_count"
	SubmissionFieldDiscovererEmployeeID = "discoverer_employee_id"
	SubmissionFieldFixerEmployeeID      = "fixer_employee_id"
	SubmissionFieldOwnerEmployeeID      = "owner_employee_id"
	SubmissionFieldDepartmentID         = "department_id"
	SubmissionFieldApplication          = "application"
	SubmissionFieldCodeRepository       = "code_repository"
	SubmissionFieldSource               = "source"
	SubmissionFieldDiscoveryChannel     = "discovery_channel"
	SubmissionFieldDiscoveryPhase       = "discovery_phase"
	SubmissionFieldNetworkEnv           = "network_env"
	SubmissionFieldInterfacePath        = "interface_path"
	SubmissionFieldDescription          = "description"
	SubmissionFieldVerificationSteps    = "verification_steps"
	SubmissionFieldPOCText              = "poc_text"
	SubmissionFieldTags                 = "tags"
	SubmissionFieldDependencyName       = "dependency_name"
	SubmissionFieldDependencyVersion    = "dependency_version"
)

var (
	submissionRequiredFieldOrder = []string{
		SubmissionFieldTitle,
		SubmissionFieldSeverityLevel,
		SubmissionFieldVulnerabilityType,
		SubmissionFieldAffectedAssetsCount,
		SubmissionFieldDiscovererEmployeeID,
		SubmissionFieldFixerEmployeeID,
		SubmissionFieldOwnerEmployeeID,
		SubmissionFieldDepartmentID,
		SubmissionFieldApplication,
		SubmissionFieldCodeRepository,
		SubmissionFieldSource,
		SubmissionFieldDiscoveryChannel,
		SubmissionFieldDiscoveryPhase,
		SubmissionFieldNetworkEnv,
		SubmissionFieldInterfacePath,
		SubmissionFieldDescription,
		SubmissionFieldVerificationSteps,
		SubmissionFieldPOCText,
		SubmissionFieldTags,
		SubmissionFieldDependencyName,
		SubmissionFieldDependencyVersion,
	}
	defaultSubmissionRequiredFields = []string{
		SubmissionFieldTitle,
		SubmissionFieldSeverityLevel,
		SubmissionFieldDiscovererEmployeeID,
		SubmissionFieldVulnerabilityType,
		SubmissionFieldApplication,
		SubmissionFieldSource,
		SubmissionFieldDiscoveryPhase,
		SubmissionFieldNetworkEnv,
		SubmissionFieldDescription,
	}
	lockedSubmissionRequiredFields = []string{
		SubmissionFieldTitle,
		SubmissionFieldSeverityLevel,
		SubmissionFieldDiscovererEmployeeID,
	}
	allowedSubmissionRequiredFields = func() map[string]struct{} {
		out := make(map[string]struct{}, len(submissionRequiredFieldOrder))
		for _, field := range submissionRequiredFieldOrder {
			out[field] = struct{}{}
		}
		return out
	}()
	htmlTagRe         = regexp.MustCompile(`<[^>]*>`)
	attachmentImageRe = regexp.MustCompile(`(?i)<img\b[^>]*\bdata-sc-attachment-id\s*=\s*["'][^"']+["'][^>]*>`)
)

const pickerReadsBlockedMessage = "org sync snapshot is expired; Feishu-backed picker reads are blocked until a fresh sync succeeds"

type Configuration struct {
	SeverityLevels           []string                `json:"severity_levels"`
	DiscoveryChannels        []string                `json:"discovery_channels"`
	DiscoveryPhases          []string                `json:"discovery_phases"`
	NetworkEnvs              []string                `json:"network_envs"`
	Sources                  []string                `json:"sources"`
	Types                    []VulnerabilityTypeInfo `json:"types"`
	SLARules                 map[string]int          `json:"sla_rules"`
	SubmissionRequiredFields []string                `json:"submission_required_fields"`
}

type VulnerabilityTypeInfo struct {
	Name            string `json:"name"`
	Category        string `json:"category"`
	DefaultSeverity string `json:"default_severity"`
	Description     string `json:"description"`
}

type SubmissionRequiredFieldValues struct {
	Title                string
	SeverityLevel        vulnmodel.SeverityLevel
	Application          string
	CodeRepository       string
	VulnerabilityType    string
	DiscoveryChannel     string
	DiscoveryPhase       string
	NetworkEnv           string
	InterfacePath        string
	Source               string
	AffectedAssetsCount  *int
	Tags                 []string
	DependencyName       string
	DependencyVersion    string
	DiscovererEmployeeID *uint64
	FixerEmployeeID      *uint64
	OwnerEmployeeID      *uint64
	DepartmentID         *string
	Description          string
	VerificationSteps    string
	POCText              string
}

type ListQuery struct {
	Page              int
	PageSize          int
	Statuses          []vulnmodel.VulnerabilityStatus
	Severities        []vulnmodel.SeverityLevel
	ActorUserID       *uint64
	OwnerEmployeeID   *uint64
	SubmitterUserID   *uint64
	Application       string
	DiscoveryPhase    string
	DiscoveryChannel  string
	NetworkEnv        string
	HasPOC            *bool
	IsOverdue         *bool
	IsThirdParty      *bool
	Tags              []string
	DependencyName    string
	DependencyVersion string
	BatchGroupID      *uint64
	HasBatchGroup     *bool
	Keyword           string
	SubmittedFrom     *time.Time
	SubmittedTo       *time.Time
	SortBy            string
	SortOrder         string
}

type ListResult struct {
	Items    []vulnmodel.Vulnerability
	Total    int64
	Page     int
	PageSize int
}

type UpdateVulnerabilityInput struct {
	Title                *string
	SeverityLevel        *vulnmodel.SeverityLevel
	Application          *string
	CodeRepository       *string
	VulnerabilityType    *string
	DiscoveryChannel     *string
	DiscoveryPhase       *string
	NetworkEnv           *string
	InterfacePath        *string
	Source               *string
	AffectedAssetsCount  *int
	Tags                 *[]string
	HasPOCVerification   *bool
	IsThirdParty         *bool
	DependencyName       *string
	DependencyVersion    *string
	DiscovererEmployeeID *uint64
	FixerEmployeeID      *uint64
	FixerEmployeeIDSet   bool
	OwnerEmployeeID      *uint64
	OwnerEmployeeIDSet   bool
	DepartmentID         *string
	DepartmentIDSet      bool
	Description          *string
	VerificationSteps    *string
	POCText              *string
	SubmittedAt          *time.Time
	SLADeadline          *time.Time
}

func New(
	repo repository.VulnerabilityRepository,
	channelRepo repository.DiscoveryChannelRepository,
	typeRepo repository.VulnerabilityTypeRepository,
	sourceRepo repository.SourceRepository,
	eventsRepo repository.VulnerabilityEventRepository,
	commentsRepo repository.VulnerabilityCommentRepository,
	attachRepo repository.VulnerabilityAttachmentRepository,
	slaRepo repository.SLARuleRepository,
	requiredFieldRepo repository.SubmissionRequiredFieldRepository,
	userRepo corerepo.UserRepository,
	employeeRepo directory.EmployeeDirectory,
) *Service {
	return &Service{
		repo:              repo,
		channelRepo:       channelRepo,
		typeRepo:          typeRepo,
		sourceRepo:        sourceRepo,
		eventsRepo:        eventsRepo,
		commentsRepo:      commentsRepo,
		attachRepo:        attachRepo,
		slaRepo:           slaRepo,
		requiredFieldRepo: requiredFieldRepo,
		userRepo:          userRepo,
		employeeRepo:      employeeRepo,
		slaConfig: map[string]int{
			"critical": 24,
			"high":     72,
			"medium":   7 * 24,
			"low":      14 * 24,
		},
	}
}

func (s *Service) SetPickerReadsBlocked(check func() bool) {
	s.pickerReadsBlocked = check
}

func (s *Service) List(ctx context.Context, query ListQuery) (*ListResult, error) {
	if s.repo == nil {
		return &ListResult{}, nil
	}

	params := repository.ListParams{
		Page:              query.Page,
		PageSize:          query.PageSize,
		Statuses:          query.Statuses,
		Severities:        query.Severities,
		OwnerEmployeeID:   query.OwnerEmployeeID,
		SubmitterUserID:   query.SubmitterUserID,
		Application:       query.Application,
		DiscoveryPhase:    query.DiscoveryPhase,
		DiscoveryChannel:  query.DiscoveryChannel,
		NetworkEnv:        query.NetworkEnv,
		HasPOC:            query.HasPOC,
		IsOverdue:         query.IsOverdue,
		IsThirdParty:      query.IsThirdParty,
		Tags:              query.Tags,
		DependencyName:    query.DependencyName,
		DependencyVersion: query.DependencyVersion,
		BatchGroupID:      query.BatchGroupID,
		HasBatchGroup:     query.HasBatchGroup,
		Keyword:           query.Keyword,
		SubmittedFrom:     query.SubmittedFrom,
		SubmittedTo:       query.SubmittedTo,
		SortBy:            query.SortBy,
		SortOrder:         query.SortOrder,
	}

	if query.ActorUserID != nil {
		actor, err := s.resolveActor(ctx, *query.ActorUserID)
		if err != nil {
			return nil, err
		}
		if actor != nil && !s.canActorBypassRestrictedVisibility(actor) {
			params.ActorUserID = query.ActorUserID
			params.ActorEmployeeID = actor.EmployeeID
			params.ActorCanViewFixed = s.actorHasPermission(actor, permissionViewFindings)
			params.RestrictVisibility = true
		}
	} else {
		params.RestrictVisibility = true
	}

	items, total, err := s.repo.List(ctx, params)
	if err != nil {
		return nil, err
	}
	return &ListResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}

func (s *Service) Create(ctx context.Context, vuln *vulnmodel.Vulnerability) error {
	if s.repo == nil {
		return nil
	}
	if claims, ok := middleware.AgentClaimsFromContext(ctx); ok {
		// 决策 8（防冒充，plan 002 §3.4）：submitter_user_id 强制为 Key 主人，忽略请求体；
		// 发现人/修复人/负责人仍可指定（扫描代报需要）。
		if vuln.SubmitterUserID != claims.UserID {
			slog.Warn("agent submitter_user_id overridden to key owner",
				"agent_id", claims.AgentID,
				"agent_name", claims.AgentName,
				"field", "submitter_user_id",
				"requested_submitter_user_id", vuln.SubmitterUserID,
				"owner_user_id", claims.UserID,
			)
			vuln.SubmitterUserID = claims.UserID
		}
		// 决策 7（防 SLA 造假）：submitted_at/sla_deadline 强制服务端默认值（人类补录不受影响）。
		vuln.SubmittedAt = time.Now().UTC()
		vuln.SLADeadline = vuln.SubmittedAt.Add(agentCreateSLADuration(vuln.SeverityLevel))
	}
	return s.repo.Create(ctx, vuln)
}

// agentCreateSLADuration 与 handler 的 defaultSLADuration 保持一致：agent Create 被强制
// 使用服务端默认 SLA，必须与人类请求体缺省时 handler 计算的默认值相同（静态表，不读 slaConfig）。
func agentCreateSLADuration(severity vulnmodel.SeverityLevel) time.Duration {
	switch severity {
	case vulnmodel.SeverityCritical:
		return 24 * time.Hour
	case vulnmodel.SeverityHigh:
		return 72 * time.Hour
	case vulnmodel.SeverityMedium:
		return 7 * 24 * time.Hour
	default:
		return 14 * 24 * time.Hour
	}
}

func (s *Service) Get(ctx context.Context, id uint64) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	return s.repo.FindByID(ctx, id)
}

func (s *Service) GetByPublicID(ctx context.Context, publicID string) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	publicID = strings.TrimSpace(publicID)
	if publicID == "" {
		return nil, errors.New("public_id required")
	}
	return s.repo.FindByPublicID(ctx, publicID)
}

func (s *Service) ListEvents(ctx context.Context, vulnID uint64) ([]vulnmodel.VulnerabilityEvent, error) {
	if s.eventsRepo == nil {
		return []vulnmodel.VulnerabilityEvent{}, nil
	}
	return s.eventsRepo.List(ctx, vulnID)
}

func (s *Service) TransitionStatus(ctx context.Context, vulnID uint64, newStatus vulnmodel.VulnerabilityStatus, message string, actorName string, actorID *uint64) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorTransitionToStatus(actor, vuln, newStatus) {
		return nil, errors.New("not authorized to change vulnerability status")
	}

	now := time.Now().UTC()
	updateFields := map[string]interface{}{
		"status": newStatus,
	}
	switch newStatus {
	case vulnmodel.StatusRetesting:
		updateFields["fix_requested_at"] = now
		vuln.FixRequestedAt = &now
	case vulnmodel.StatusFixed:
		updateFields["fixed_at"] = now
		updateFields["closed_at"] = nil
		vuln.FixedAt = &now
	case vulnmodel.StatusFalsePositive, vulnmodel.StatusRiskAccepted:
		updateFields["closed_at"] = now
		vuln.ClosedAt = &now
	case vulnmodel.StatusActive:
		updateFields["fixed_at"] = nil
		updateFields["closed_at"] = nil
		updateFields["fix_requested_at"] = nil
		vuln.FixedAt = nil
		vuln.ClosedAt = nil
		vuln.FixRequestedAt = nil
	}

	if err := s.repo.UpdateFields(ctx, vulnID, updateFields); err != nil {
		return nil, err
	}
	// refresh
	vuln, _ = s.repo.FindByID(ctx, vulnID)

	_ = s.appendEvent(ctx, vulnID, "status_changed", message, map[string]interface{}{
		"actor_name": actorName,
		"actor_id":   actorID,
		"status":     string(newStatus),
	})
	return vuln, nil
}

func (s *Service) UpdateMeta(
	ctx context.Context,
	vulnID uint64,
	ownerEmployeeID *uint64,
	tags []string,
	verificationSteps *string,
	pocText *string,
	actorName string,
	actorID *uint64,
) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}

	updateFields := map[string]interface{}{}

	if ownerEmployeeID != nil && (vuln.OwnerEmployeeID == nil || *vuln.OwnerEmployeeID != *ownerEmployeeID) {
		if !s.canActorAssignOwner(actor) {
			return nil, errors.New("not authorized to change vulnerability owner")
		}
		updateFields["owner_employee_id"] = ownerEmployeeID
		_ = s.appendEvent(ctx, vulnID, "owner_changed", "Owner updated", map[string]interface{}{
			"actor_name":        actorName,
			"actor_id":          actorID,
			"owner_employee_id": ownerEmployeeID,
			"previous_values":   map[string]interface{}{"owner_employee_id": vuln.OwnerEmployeeID},
		})
	}
	if tags != nil {
		if !s.canActorEditTags(actor, vuln) {
			return nil, errors.New("not authorized to update vulnerability metadata")
		}
		if !slices.Equal([]string(vuln.Tags), tags) {
			updateFields["tags"] = vulnmodel.StringArray(tags)
			_ = s.appendEvent(ctx, vulnID, "tags_updated", "Tags updated", map[string]interface{}{
				"actor_name":      actorName,
				"actor_id":        actorID,
				"tags":            tags,
				"previous_values": map[string]interface{}{"tags": append([]string(nil), vuln.Tags...)},
			})
		}
	}
	if verificationSteps != nil {
		if !s.canActorEditSensitiveDetails(actor, vuln) {
			return nil, errors.New("not authorized to update vulnerability metadata")
		}
		if vuln.VerificationSteps != *verificationSteps {
			updateFields["verification_steps"] = *verificationSteps
			_ = s.appendEvent(ctx, vulnID, "verification_steps_updated", "Verification steps updated", map[string]interface{}{
				"actor_name": actorName,
				"actor_id":   actorID,
			})
		}
	}
	if pocText != nil {
		if !s.canActorEditSensitiveDetails(actor, vuln) {
			return nil, errors.New("not authorized to update vulnerability metadata")
		}
		if vuln.POCText != *pocText {
			updateFields["poc_text"] = *pocText
			_ = s.appendEvent(ctx, vulnID, "poc_updated", "Proof of concept updated", map[string]interface{}{
				"actor_name": actorName,
				"actor_id":   actorID,
			})
		}
	}

	if len(updateFields) > 0 {
		if err := s.repo.UpdateFields(ctx, vulnID, updateFields); err != nil {
			return nil, err
		}
		// refresh
		vuln, _ = s.repo.FindByID(ctx, vulnID)
	}

	return vuln, nil
}

func (s *Service) UpdateAuthorizedViewers(
	ctx context.Context,
	vulnID uint64,
	authorizedViewerEmployeeIDs []uint64,
	actorName string,
	actorID *uint64,
) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorAuthorizeViewers(actor) {
		return nil, errors.New("not authorized to manage vulnerability viewers")
	}
	normalized := normalizeAuthorizedViewerIDs(authorizedViewerEmployeeIDs)
	if err := s.ValidateEmployeeIDs(ctx, uint64Pointers(normalized)...); err != nil {
		return nil, err
	}
	if slices.Equal([]uint64(vuln.AuthorizedViewerEmployeeIDs), normalized) {
		return vuln, nil
	}
	if err := s.repo.UpdateFields(ctx, vulnID, map[string]interface{}{
		"authorized_viewer_employee_ids": vulnmodel.UInt64Array(normalized),
	}); err != nil {
		return nil, err
	}
	_ = s.appendEvent(ctx, vulnID, "authorized_viewers_updated", "Authorized viewers updated", map[string]interface{}{
		"actor_name":   actorName,
		"actor_id":     actorID,
		"employee_ids": normalized,
	})
	return s.repo.FindByID(ctx, vulnID)
}

func (s *Service) UpdatePublicVisibility(
	ctx context.Context,
	vulnID uint64,
	isPublicViewable bool,
	actorName string,
	actorID *uint64,
) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorAuthorizeViewers(actor) {
		return nil, errors.New("not authorized to manage vulnerability visibility")
	}
	if vuln.IsPublicViewable == isPublicViewable {
		return vuln, nil
	}
	if err := s.repo.UpdateFields(ctx, vulnID, map[string]interface{}{
		"is_public_viewable": isPublicViewable,
	}); err != nil {
		return nil, err
	}
	_ = s.appendEvent(ctx, vulnID, "public_visibility_updated", "Public visibility updated", map[string]interface{}{
		"actor_name":         actorName,
		"actor_id":           actorID,
		"is_public_viewable": isPublicViewable,
	})
	return s.repo.FindByID(ctx, vulnID)
}

func (s *Service) SearchAuthorizedViewerCandidates(ctx context.Context, vulnID uint64, query string, ids []uint64, actorID *uint64) ([]directory.Employee, error) {
	if err := s.EnsureCanSearchAuthorizedViewerCandidates(ctx, actorID); err != nil {
		return []directory.Employee{}, err
	}
	if s.pickerReadsBlocked != nil && s.pickerReadsBlocked() {
		return []directory.Employee{}, errors.New(pickerReadsBlockedMessage)
	}
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	if _, err := s.repo.FindByID(ctx, vulnID); err != nil {
		return nil, err
	}
	if s.employeeRepo == nil {
		return []directory.Employee{}, nil
	}

	trimmed := strings.TrimSpace(query)
	normalizedIDs := normalizeAuthorizedViewerIDs(ids)
	if trimmed == "" && len(normalizedIDs) == 0 {
		return []directory.Employee{}, nil
	}
	employees := make([]directory.Employee, 0)
	seen := make(map[uint64]struct{})
	for _, id := range normalizedIDs {
		employee, err := s.employeeRepo.FindByID(ctx, id)
		if err != nil {
			return nil, err
		}
		if employee == nil || !employeeIsActive(*employee) {
			continue
		}
		if _, ok := seen[employee.ID]; ok {
			continue
		}
		seen[employee.ID] = struct{}{}
		employees = append(employees, *employee)
	}
	if trimmed != "" {
		searchResults, err := searchActiveDirectoryEmployees(ctx, s.employeeRepo, trimmed)
		if err != nil {
			return nil, err
		}
		for _, employee := range searchResults {
			if _, ok := seen[employee.ID]; ok {
				continue
			}
			seen[employee.ID] = struct{}{}
			employees = append(employees, employee)
		}
	}
	return activeEmployeesOnly(employees), nil
}

func (s *Service) EnsureCanSearchAuthorizedViewerCandidates(ctx context.Context, actorID *uint64) error {
	if actorID == nil || *actorID == 0 {
		return errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return err
	}
	if !s.canActorAuthorizeViewers(actor) {
		return errors.New("not authorized to manage vulnerability viewers")
	}
	return nil
}

func (s *Service) UpdateVulnerability(
	ctx context.Context,
	vulnID uint64,
	input UpdateVulnerabilityInput,
	actorName string,
	actorID *uint64,
) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorEditDetails(actor, vuln) {
		return nil, errors.New("not authorized to update vulnerability details")
	}

	if claims, ok := middleware.AgentClaimsFromContext(ctx); ok {
		// 决策 6/7（plan 002 §3.4）：agent 变更敏感字段或时间字段 → 整个请求 403，
		// 不做静默剥离（LLM 调用方需要明确的拒绝信号）。人类请求不走此分支，行为不变。
		if field := agentGuardedFieldChange(vuln, input); field != "" {
			slog.Warn("agent write guard rejected field change",
				"agent_id", claims.AgentID,
				"agent_name", claims.AgentName,
				"field", field,
				"vuln_public_id", vuln.PublicID,
			)
			return nil, fmt.Errorf("not authorized: agent keys cannot modify %s", field)
		}
	}

	employeeIDs := []*uint64{input.DiscovererEmployeeID}
	if input.FixerEmployeeIDSet && input.FixerEmployeeID != nil {
		employeeIDs = append(employeeIDs, input.FixerEmployeeID)
	}
	if input.OwnerEmployeeIDSet && input.OwnerEmployeeID != nil {
		employeeIDs = append(employeeIDs, input.OwnerEmployeeID)
	}
	if err := s.ValidateEmployeeIDs(ctx, employeeIDs...); err != nil {
		return nil, err
	}

	updateFields := map[string]interface{}{}
	changedFields := make([]string, 0, 24)

	if input.Title != nil && vuln.Title != *input.Title {
		updateFields["title"] = *input.Title
		changedFields = append(changedFields, "title")
	}
	if input.SeverityLevel != nil && vuln.SeverityLevel != *input.SeverityLevel {
		updateFields["severity_level"] = *input.SeverityLevel
		changedFields = append(changedFields, "severity_level")
	}
	if input.Application != nil && vuln.Application != *input.Application {
		updateFields["application"] = *input.Application
		changedFields = append(changedFields, "application")
	}
	if input.CodeRepository != nil && vuln.CodeRepository != *input.CodeRepository {
		updateFields["code_repository"] = *input.CodeRepository
		changedFields = append(changedFields, "code_repository")
	}
	if input.VulnerabilityType != nil && vuln.VulnerabilityType != *input.VulnerabilityType {
		updateFields["vulnerability_type"] = *input.VulnerabilityType
		changedFields = append(changedFields, "vulnerability_type")
	}
	if input.DiscoveryChannel != nil && vuln.DiscoveryChannel != *input.DiscoveryChannel {
		updateFields["discovery_channel"] = *input.DiscoveryChannel
		changedFields = append(changedFields, "discovery_channel")
	}
	if input.DiscoveryPhase != nil && vuln.DiscoveryPhase != *input.DiscoveryPhase {
		updateFields["discovery_phase"] = *input.DiscoveryPhase
		changedFields = append(changedFields, "discovery_phase")
	}
	if input.NetworkEnv != nil && vuln.NetworkEnv != *input.NetworkEnv {
		updateFields["network_env"] = *input.NetworkEnv
		changedFields = append(changedFields, "network_env")
	}
	if input.InterfacePath != nil && vuln.InterfacePath != *input.InterfacePath {
		updateFields["interface_path"] = *input.InterfacePath
		changedFields = append(changedFields, "interface_path")
	}
	if input.Source != nil && vuln.Source != *input.Source {
		updateFields["source"] = *input.Source
		changedFields = append(changedFields, "source")
	}
	if input.AffectedAssetsCount != nil && vuln.AffectedAssetsCount != *input.AffectedAssetsCount {
		updateFields["affected_assets_count"] = *input.AffectedAssetsCount
		changedFields = append(changedFields, "affected_assets_count")
	}
	if input.Tags != nil {
		normalizedTags := normalizeTags(*input.Tags)
		if !slices.Equal([]string(vuln.Tags), normalizedTags) {
			updateFields["tags"] = vulnmodel.StringArray(normalizedTags)
			changedFields = append(changedFields, "tags")
		}
	}
	if input.HasPOCVerification != nil && vuln.HasPOCVerification != *input.HasPOCVerification {
		updateFields["has_poc_verification"] = *input.HasPOCVerification
		changedFields = append(changedFields, "has_poc_verification")
	}
	if input.IsThirdParty != nil && vuln.IsThirdParty != *input.IsThirdParty {
		updateFields["is_third_party"] = *input.IsThirdParty
		changedFields = append(changedFields, "is_third_party")
	}
	if input.DependencyName != nil && vuln.DependencyName != *input.DependencyName {
		updateFields["dependency_name"] = *input.DependencyName
		changedFields = append(changedFields, "dependency_name")
	}
	if input.DependencyVersion != nil && vuln.DependencyVersion != *input.DependencyVersion {
		updateFields["dependency_version"] = *input.DependencyVersion
		changedFields = append(changedFields, "dependency_version")
	}
	if input.DiscovererEmployeeID != nil && !matchesEmployee(vuln.DiscovererEmployeeID, input.DiscovererEmployeeID) {
		updateFields["discoverer_employee_id"] = input.DiscovererEmployeeID
		changedFields = append(changedFields, "discoverer_employee_id")
	}
	if input.FixerEmployeeIDSet && !matchesEmployee(vuln.FixerEmployeeID, input.FixerEmployeeID) {
		updateFields["fixer_employee_id"] = input.FixerEmployeeID
		changedFields = append(changedFields, "fixer_employee_id")
	}
	if input.OwnerEmployeeIDSet && !matchesEmployee(vuln.OwnerEmployeeID, input.OwnerEmployeeID) {
		updateFields["owner_employee_id"] = input.OwnerEmployeeID
		changedFields = append(changedFields, "owner_employee_id")
	}
	if input.DepartmentIDSet && !matchesOptionalString(vuln.DepartmentID, input.DepartmentID) {
		updateFields["department_id"] = input.DepartmentID
		changedFields = append(changedFields, "department_id")
	}
	if input.Description != nil && vuln.Description != *input.Description {
		updateFields["description"] = *input.Description
		changedFields = append(changedFields, "description")
	}
	if input.VerificationSteps != nil && vuln.VerificationSteps != *input.VerificationSteps {
		updateFields["verification_steps"] = *input.VerificationSteps
		changedFields = append(changedFields, "verification_steps")
	}
	if input.POCText != nil && vuln.POCText != *input.POCText {
		updateFields["poc_text"] = *input.POCText
		changedFields = append(changedFields, "poc_text")
	}
	if input.SubmittedAt != nil && !vuln.SubmittedAt.Equal(*input.SubmittedAt) {
		updateFields["submitted_at"] = *input.SubmittedAt
		changedFields = append(changedFields, "submitted_at")
	}
	if input.SLADeadline != nil && !vuln.SLADeadline.Equal(*input.SLADeadline) {
		updateFields["sla_deadline"] = *input.SLADeadline
		changedFields = append(changedFields, "sla_deadline")
	}

	nextSLADeadline := vuln.SLADeadline
	if input.SLADeadline != nil {
		nextSLADeadline = *input.SLADeadline
	}
	nextIsOverdue := vulnerabilityIsOverdue(vuln.Status, nextSLADeadline, time.Now().UTC())
	if vuln.IsOverdue != nextIsOverdue {
		updateFields["is_overdue"] = nextIsOverdue
	}

	if len(updateFields) == 0 {
		return vuln, nil
	}
	if err := s.repo.UpdateFields(ctx, vulnID, updateFields); err != nil {
		return nil, err
	}
	eventMeta := map[string]interface{}{
		"actor_name":     actorName,
		"actor_id":       actorID,
		"changed_fields": changedFields,
	}
	// 决策 8（plan 002 §3.5/§4.1）：记录被改字段的旧值，任何误写都能查事件精确改回。
	if prev := previousValuesForChangedFields(vuln, changedFields); len(prev) > 0 {
		eventMeta["previous_values"] = prev
	}
	if err := s.appendEvent(ctx, vulnID, "details_updated", "Vulnerability details updated", eventMeta); err != nil {
		return nil, err
	}
	return s.repo.FindByID(ctx, vulnID)
}

// sensitivePreviousValueFields 的旧值不得写入事件 meta（plan 002 §6.2）：
// 事件对所有可见者开放，写入会向无敏感查看权的人泄密。
var sensitivePreviousValueFields = map[string]bool{
	"poc_text":             true,
	"verification_steps":   true,
	"has_poc_verification": true,
}

// previousValuesForChangedFields 从更新前的 vuln 取被改字段的旧值；
// 敏感字段与未变更字段不出现，读取方须容忍旧事件缺失 previous_values。
func previousValuesForChangedFields(vuln *vulnmodel.Vulnerability, changedFields []string) map[string]interface{} {
	prev := map[string]interface{}{}
	for _, field := range changedFields {
		if sensitivePreviousValueFields[field] {
			continue
		}
		switch field {
		case "title":
			prev[field] = vuln.Title
		case "severity_level":
			prev[field] = vuln.SeverityLevel
		case "application":
			prev[field] = vuln.Application
		case "code_repository":
			prev[field] = vuln.CodeRepository
		case "vulnerability_type":
			prev[field] = vuln.VulnerabilityType
		case "discovery_channel":
			prev[field] = vuln.DiscoveryChannel
		case "discovery_phase":
			prev[field] = vuln.DiscoveryPhase
		case "network_env":
			prev[field] = vuln.NetworkEnv
		case "interface_path":
			prev[field] = vuln.InterfacePath
		case "source":
			prev[field] = vuln.Source
		case "affected_assets_count":
			prev[field] = vuln.AffectedAssetsCount
		case "tags":
			prev[field] = append([]string(nil), vuln.Tags...)
		case "is_third_party":
			prev[field] = vuln.IsThirdParty
		case "dependency_name":
			prev[field] = vuln.DependencyName
		case "dependency_version":
			prev[field] = vuln.DependencyVersion
		case "discoverer_employee_id":
			prev[field] = vuln.DiscovererEmployeeID
		case "fixer_employee_id":
			prev[field] = vuln.FixerEmployeeID
		case "owner_employee_id":
			prev[field] = vuln.OwnerEmployeeID
		case "department_id":
			prev[field] = vuln.DepartmentID
		case "description":
			prev[field] = vuln.Description
		case "submitted_at":
			prev[field] = vuln.SubmittedAt
		case "sla_deadline":
			prev[field] = vuln.SLADeadline
		}
	}
	return prev
}

// agentGuardedFieldChange 返回 agent 试图变更的受防护字段名，无变更时返回空串。
// 决策 6（防盲改）：poc_text/verification_steps/has_poc_verification；
// 决策 7（防 SLA 造假）：submitted_at/sla_deadline。
// "变更"指新值与现值不同——请求体带同值不算变更，放行。
func agentGuardedFieldChange(vuln *vulnmodel.Vulnerability, input UpdateVulnerabilityInput) string {
	if input.POCText != nil && vuln.POCText != *input.POCText {
		return "poc_text"
	}
	if input.VerificationSteps != nil && vuln.VerificationSteps != *input.VerificationSteps {
		return "verification_steps"
	}
	if input.HasPOCVerification != nil && vuln.HasPOCVerification != *input.HasPOCVerification {
		return "has_poc_verification"
	}
	if input.SubmittedAt != nil && !vuln.SubmittedAt.Equal(*input.SubmittedAt) {
		return "submitted_at"
	}
	if input.SLADeadline != nil && !vuln.SLADeadline.Equal(*input.SLADeadline) {
		return "sla_deadline"
	}
	return ""
}

func (s *Service) CanViewVulnerability(ctx context.Context, userID uint64, vulnID uint64) (bool, error) {
	if s.repo == nil || s.userRepo == nil {
		return false, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return false, err
	}
	actor, err := s.resolveActor(ctx, userID)
	if err != nil {
		return false, err
	}
	return s.canActorViewVulnerability(actor, vuln), nil
}

func (s *Service) appendEvent(ctx context.Context, vulnID uint64, eventType, msg string, meta map[string]interface{}) error {
	if s.eventsRepo == nil {
		return nil
	}
	if meta == nil {
		meta = map[string]interface{}{}
	}
	if _, ok := meta["actor_name"]; !ok {
		meta["actor_name"] = "System"
	}
	ev := &vulnmodel.VulnerabilityEvent{
		VulnerabilityID: vulnID,
		EventType:       eventType,
		Message:         msg,
		Metadata:        meta,
	}
	return s.eventsRepo.Create(ctx, ev)
}

// AppendEvent is an exported helper to log lifecycle events from handlers.
func (s *Service) AppendEvent(ctx context.Context, vulnID uint64, eventType, msg string, meta map[string]interface{}) error {
	return s.appendEvent(ctx, vulnID, eventType, msg, meta)
}

func (s *Service) Claim(ctx context.Context, vulnID uint64, actorName string, actorID *uint64) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if actor == nil || !s.canActorClaimVulnerability(actor, vuln) {
		return nil, errors.New("not authorized to claim vulnerability")
	}
	if actor.EmployeeID == nil {
		return nil, errors.New("employee link required to claim vulnerability")
	}
	now := time.Now().UTC()
	updateFields := map[string]interface{}{
		"owner_employee_id": actor.EmployeeID,
		"fixer_employee_id": actor.EmployeeID,
		"claimed_at":        now,
	}
	if err := s.repo.UpdateFields(ctx, vulnID, updateFields); err != nil {
		return nil, err
	}
	// refresh
	vuln, _ = s.repo.FindByID(ctx, vulnID)
	_ = s.appendEvent(ctx, vulnID, "claimed", "Vulnerability claimed", map[string]interface{}{
		"actor_name":        actorName,
		"actor_id":          actorID,
		"owner_employee_id": actor.EmployeeID,
		"fixer_employee_id": actor.EmployeeID,
		"status":            vuln.Status,
	})
	return vuln, nil
}

func (s *Service) Assign(ctx context.Context, vulnID uint64, ownerEmployeeID *uint64, actorName string, actorID *uint64) (*vulnmodel.Vulnerability, error) {
	if s.repo == nil {
		return nil, fmt.Errorf("repository not configured")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	if actorID == nil || *actorID == 0 {
		return nil, errors.New("actor required")
	}
	actor, err := s.resolveActor(ctx, *actorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorAssignOwner(actor) {
		return nil, errors.New("not authorized to assign vulnerability owner")
	}
	if ownerEmployeeID != nil {
		vuln.OwnerEmployeeID = ownerEmployeeID
	}
	if err := s.repo.UpdateFields(ctx, vulnID, map[string]interface{}{
		"owner_employee_id": ownerEmployeeID,
	}); err != nil {
		return nil, err
	}
	_ = s.appendEvent(ctx, vulnID, "owner_changed", "Assigned owner", map[string]interface{}{
		"actor_name":        actorName,
		"actor_id":          actorID,
		"owner_employee_id": ownerEmployeeID,
	})
	return vuln, nil
}

func (s *Service) UpdateSLAConfig(ctx context.Context, rules map[string]int) error {
	if rules == nil {
		return nil
	}
	for k, v := range rules {
		if v <= 0 {
			return fmt.Errorf("invalid SLA hours for %s", k)
		}
		s.slaConfig[strings.ToLower(k)] = v
	}
	if s.slaRepo != nil {
		return s.slaRepo.Upsert(ctx, s.slaConfig)
	}
	return nil
}

func (s *Service) UpdateSubmissionRequiredFields(ctx context.Context, fieldKeys []string) error {
	normalized, err := NormalizeSubmissionRequiredFields(fieldKeys)
	if err != nil {
		return err
	}
	if s.requiredFieldRepo != nil {
		return s.requiredFieldRepo.Replace(ctx, normalized)
	}
	return nil
}

func NormalizeSubmissionRequiredFields(fieldKeys []string) ([]string, error) {
	seen := map[string]struct{}{}
	for _, fieldKey := range fieldKeys {
		key := strings.ToLower(strings.TrimSpace(fieldKey))
		if key == "" {
			continue
		}
		if _, ok := allowedSubmissionRequiredFields[key]; !ok {
			return nil, fmt.Errorf("unknown submission required field: %s", key)
		}
		seen[key] = struct{}{}
	}
	for _, fieldKey := range lockedSubmissionRequiredFields {
		seen[fieldKey] = struct{}{}
	}
	ordered := make([]string, 0, len(seen))
	for _, fieldKey := range submissionRequiredFieldOrder {
		if _, ok := seen[fieldKey]; ok {
			ordered = append(ordered, fieldKey)
		}
	}
	return ordered, nil
}

func DefaultSubmissionRequiredFields() []string {
	fields, _ := NormalizeSubmissionRequiredFields(defaultSubmissionRequiredFields)
	return fields
}

func (s *Service) submissionRequiredFields(ctx context.Context) []string {
	if s.requiredFieldRepo == nil {
		return DefaultSubmissionRequiredFields()
	}
	fields, err := s.requiredFieldRepo.List(ctx)
	if err != nil || len(fields) == 0 {
		return DefaultSubmissionRequiredFields()
	}
	normalized, err := NormalizeSubmissionRequiredFields(fields)
	if err != nil {
		return DefaultSubmissionRequiredFields()
	}
	return normalized
}

func (s *Service) ValidateSubmissionRequiredFields(ctx context.Context, values SubmissionRequiredFieldValues) error {
	for _, fieldKey := range s.submissionRequiredFields(ctx) {
		if submissionRequiredFieldMissing(fieldKey, values) {
			return fmt.Errorf("%s is required", fieldKey)
		}
	}
	return nil
}

func submissionRequiredFieldMissing(fieldKey string, values SubmissionRequiredFieldValues) bool {
	switch fieldKey {
	case SubmissionFieldTitle:
		return strings.TrimSpace(values.Title) == ""
	case SubmissionFieldSeverityLevel:
		return strings.TrimSpace(string(values.SeverityLevel)) == ""
	case SubmissionFieldVulnerabilityType:
		return strings.TrimSpace(values.VulnerabilityType) == ""
	case SubmissionFieldAffectedAssetsCount:
		return values.AffectedAssetsCount == nil || *values.AffectedAssetsCount < 0
	case SubmissionFieldDiscovererEmployeeID:
		return values.DiscovererEmployeeID == nil || *values.DiscovererEmployeeID == 0
	case SubmissionFieldFixerEmployeeID:
		return values.FixerEmployeeID == nil || *values.FixerEmployeeID == 0
	case SubmissionFieldOwnerEmployeeID:
		return values.OwnerEmployeeID == nil || *values.OwnerEmployeeID == 0
	case SubmissionFieldDepartmentID:
		return values.DepartmentID == nil || strings.TrimSpace(*values.DepartmentID) == ""
	case SubmissionFieldApplication:
		return strings.TrimSpace(values.Application) == ""
	case SubmissionFieldCodeRepository:
		return strings.TrimSpace(values.CodeRepository) == ""
	case SubmissionFieldSource:
		return strings.TrimSpace(values.Source) == ""
	case SubmissionFieldDiscoveryChannel:
		return strings.TrimSpace(values.DiscoveryChannel) == ""
	case SubmissionFieldDiscoveryPhase:
		return strings.TrimSpace(values.DiscoveryPhase) == ""
	case SubmissionFieldNetworkEnv:
		return strings.TrimSpace(values.NetworkEnv) == ""
	case SubmissionFieldInterfacePath:
		return strings.TrimSpace(values.InterfacePath) == ""
	case SubmissionFieldDescription:
		return richTextIsBlank(values.Description)
	case SubmissionFieldVerificationSteps:
		return strings.TrimSpace(values.VerificationSteps) == ""
	case SubmissionFieldPOCText:
		return strings.TrimSpace(values.POCText) == ""
	case SubmissionFieldTags:
		return len(values.Tags) == 0
	case SubmissionFieldDependencyName:
		return strings.TrimSpace(values.DependencyName) == ""
	case SubmissionFieldDependencyVersion:
		return strings.TrimSpace(values.DependencyVersion) == ""
	default:
		return false
	}
}

func richTextIsBlank(html string) bool {
	withAttachmentText := attachmentImageRe.ReplaceAllString(html, " attachment ")
	withoutTags := htmlTagRe.ReplaceAllString(withAttachmentText, " ")
	withoutEntities := strings.NewReplacer(
		"&nbsp;", " ",
		"&#160;", " ",
		"&amp;nbsp;", " ",
	).Replace(withoutTags)
	return strings.TrimSpace(withoutEntities) == ""
}

func (s *Service) ListComments(ctx context.Context, vulnID uint64) ([]vulnmodel.VulnerabilityComment, error) {
	if s.commentsRepo == nil {
		return []vulnmodel.VulnerabilityComment{}, nil
	}
	return s.commentsRepo.List(ctx, vulnID)
}

func (s *Service) AddComment(ctx context.Context, vulnID uint64, authorName string, authorID *uint64, content string) (*vulnmodel.VulnerabilityComment, error) {
	if s.commentsRepo == nil {
		return nil, fmt.Errorf("comments not configured")
	}
	if authorID == nil || *authorID == 0 {
		return nil, errors.New("actor required")
	}
	vuln, err := s.repo.FindByID(ctx, vulnID)
	if err != nil {
		return nil, err
	}
	actor, err := s.resolveActor(ctx, *authorID)
	if err != nil {
		return nil, err
	}
	if !s.canActorCommentOnVulnerability(actor, vuln) {
		return nil, errors.New("not authorized to comment on vulnerability")
	}
	comment := &vulnmodel.VulnerabilityComment{
		VulnerabilityID: vulnID,
		AuthorID:        authorID,
		AuthorName:      authorName,
		Content:         content,
	}
	if err := s.commentsRepo.Create(ctx, comment); err != nil {
		return nil, err
	}
	return comment, nil
}

func (s *Service) resolveEmployeeID(ctx context.Context, userID uint64) (*uint64, error) {
	actor, err := s.resolveActor(ctx, userID)
	if err != nil || actor == nil {
		return nil, err
	}
	return actor.EmployeeID, nil
}

type actorContext struct {
	User         *coremodel.User
	UserID       uint64
	EmployeeID   *uint64
	IsPrivileged bool
	Permissions  map[string]struct{}
}

func (s *Service) resolveActor(ctx context.Context, userID uint64) (*actorContext, error) {
	if s.userRepo == nil {
		return nil, nil
	}
	user, err := s.userRepo.FindByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, nil
	}
	permissions := make(map[string]struct{})
	for _, role := range user.Roles {
		for _, permission := range role.Permissions {
			if permission.Name == "" {
				continue
			}
			permissions[permission.Name] = struct{}{}
		}
	}
	if claims, ok := middleware.AgentClaimsFromContext(ctx); ok {
		// 修缺口一（plan 002 §3.2）：agent 请求的有效权限 = 主人 DB 权限 ∩ Key scope，
		// 并清空角色（不蹭主人的 admin/security 角色与 admin 特权绕过）。人类请求走原路径。
		intersected := make(map[string]struct{}, len(claims.Permissions))
		for _, scope := range claims.Permissions {
			if _, allowed := permissions[scope]; allowed {
				intersected[scope] = struct{}{}
			}
		}
		permissions = intersected
		userWithoutRoles := *user
		userWithoutRoles.Roles = nil
		user = &userWithoutRoles
		slog.Debug("agent actor permissions intersected with key scope",
			"agent_id", claims.AgentID,
			"agent_name", claims.AgentName,
			"owner_user_id", userID,
			"intersected_permission_count", len(permissions),
		)
	}
	return &actorContext{
		User:         user,
		UserID:       userID,
		EmployeeID:   user.EmployeeID,
		IsPrivileged: user.HasRole("admin") || user.HasRole("security") || user.HasRole("security_operator"),
		Permissions:  permissions,
	}, nil
}

func (s *Service) canActorViewVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if vuln.IsPublicViewable {
		return true
	}
	if vuln.Status == vulnmodel.StatusFixed {
		return s.actorHasPermission(actor, permissionViewFindings)
	}
	return s.canActorViewRestrictedVulnerability(actor, vuln)
}

func (s *Service) canActorMutateVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if s.isSecurityActor(actor) {
		return true
	}
	if actor.EmployeeID == nil {
		return false
	}
	return matchesEmployee(actor.EmployeeID, vuln.FixerEmployeeID) && s.actorHasPermission(actor, permissionRequestRetest)
}

func (s *Service) AvailableActions(ctx context.Context, userID uint64, vuln *vulnmodel.Vulnerability) ([]string, error) {
	actor, err := s.resolveActor(ctx, userID)
	if err != nil {
		return nil, err
	}
	if !s.canActorViewVulnerability(actor, vuln) {
		return []string{}, nil
	}
	actions := []string{"view"}
	if s.canActorCommentOnVulnerability(actor, vuln) {
		actions = append(actions, "comment")
	}
	if s.canActorClaimVulnerability(actor, vuln) {
		actions = append(actions, "claim")
	}
	if s.canActorAssignOwner(actor) {
		actions = append(actions, "assign_owner")
	}
	if s.canActorEditDetails(actor, vuln) {
		actions = append(actions, actionEditDetails)
	}
	if s.canActorViewSensitiveDetails(actor, vuln) {
		actions = append(actions, actionViewSensitiveDetails)
	}
	if s.canActorEditSensitiveDetails(actor, vuln) {
		actions = append(actions, actionEditSensitiveDetails)
	}
	if s.canActorEditTags(actor, vuln) {
		actions = append(actions, "edit_tags")
	}
	if s.canActorAuthorizeViewers(actor) {
		actions = append(actions, actionAuthorizeViewers)
	}
	if s.canActorRequestRetest(actor, vuln) {
		actions = append(actions, actionRequestRetest)
	}
	if s.canActorCloseVulnerability(actor, vuln) {
		if vuln.Status == vulnmodel.StatusRetesting {
			actions = append(actions, "mark_fixed")
		}
		if vuln.Status == vulnmodel.StatusActive || vuln.Status == vulnmodel.StatusRetesting {
			actions = append(actions, "mark_false_positive", "mark_risk_accepted")
		}
		if vuln.Status == vulnmodel.StatusFixed || vuln.Status == vulnmodel.StatusFalsePositive || vuln.Status == vulnmodel.StatusRiskAccepted {
			actions = append(actions, actionReopen)
		}
	}
	return actions, nil
}

func (s *Service) actorHasPermission(actor *actorContext, permission string) bool {
	if actor == nil {
		return false
	}
	if actor.User != nil && actor.User.HasRole("admin") {
		return true
	}
	if _, ok := actor.Permissions[permission]; ok {
		return true
	}
	return false
}

func (s *Service) isSecurityActor(actor *actorContext) bool {
	return s.actorHasPermission(actor, permissionManageFindings) ||
		s.actorHasPermission(actor, permissionEditDetails) ||
		s.actorHasPermission(actor, permissionAssignOwner) ||
		s.actorHasPermission(actor, permissionEditSensitive) ||
		s.actorHasPermission(actor, permissionAuthorizeViewers) ||
		s.actorHasPermission(actor, permissionCloseFindings)
}

func (s *Service) canActorBypassRestrictedVisibility(actor *actorContext) bool {
	return s.isSecurityActor(actor)
}

func (s *Service) canActorViewRestrictedVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if s.canActorBypassRestrictedVisibility(actor) {
		return true
	}
	if actor.UserID == vuln.SubmitterUserID {
		return true
	}
	if actor.EmployeeID == nil {
		return false
	}
	return matchesEmployee(actor.EmployeeID, vuln.FixerEmployeeID) ||
		matchesEmployee(actor.EmployeeID, vuln.OwnerEmployeeID) ||
		containsEmployeeID(vuln.AuthorizedViewerEmployeeIDs, *actor.EmployeeID)
}

func (s *Service) canActorViewSensitiveDetails(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if actor.UserID == vuln.SubmitterUserID {
		return true
	}
	return s.actorHasPermission(actor, permissionViewSensitive) || s.canActorEditSensitiveDetails(actor, vuln)
}

func (s *Service) canActorEditSensitiveDetails(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if actor.UserID == vuln.SubmitterUserID {
		return true
	}
	return s.actorHasPermission(actor, permissionEditSensitive) ||
		s.actorHasPermission(actor, permissionEditDetails) ||
		s.actorHasPermission(actor, permissionManageFindings)
}

func (s *Service) canActorEditDetails(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	return s.actorHasPermission(actor, permissionEditDetails) || s.actorHasPermission(actor, permissionManageFindings)
}

func (s *Service) canActorAuthorizeViewers(actor *actorContext) bool {
	return actorPermissionSet(actor, permissionAuthorizeViewers) || actorPermissionSet(actor, permissionManageFindings)
}

func (s *Service) canActorAssignOwner(actor *actorContext) bool {
	return s.actorHasPermission(actor, permissionAssignOwner) ||
		s.actorHasPermission(actor, permissionEditDetails) ||
		s.actorHasPermission(actor, permissionManageFindings)
}

func (s *Service) canActorEditTags(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	return s.canActorEditDetails(actor, vuln) || s.isSecurityActor(actor)
}

func (s *Service) canActorClaimVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if vuln == nil || vuln.Status != vulnmodel.StatusActive {
		return false
	}
	if !s.canActorViewVulnerability(actor, vuln) {
		return false
	}
	if actor == nil || actor.EmployeeID == nil {
		return false
	}
	if s.isSecurityActor(actor) {
		return false
	}
	return s.actorHasPermission(actor, permissionRequestRetest)
}

func (s *Service) canActorRequestRetest(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil || actor.EmployeeID == nil {
		return false
	}
	if vuln.Status != vulnmodel.StatusActive {
		return false
	}
	return matchesEmployee(actor.EmployeeID, vuln.FixerEmployeeID) &&
		(s.actorHasPermission(actor, permissionRequestRetest) || s.actorHasPermission(actor, permissionManageFindings))
}

func (s *Service) canActorCloseVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	return s.actorHasPermission(actor, permissionCloseFindings) || s.actorHasPermission(actor, permissionManageFindings)
}

func (s *Service) canActorTransitionToStatus(actor *actorContext, vuln *vulnmodel.Vulnerability, newStatus vulnmodel.VulnerabilityStatus) bool {
	switch newStatus {
	case vulnmodel.StatusRetesting:
		return s.canActorRequestRetest(actor, vuln)
	case vulnmodel.StatusFixed:
		return vuln != nil && vuln.Status == vulnmodel.StatusRetesting && s.canActorCloseVulnerability(actor, vuln)
	case vulnmodel.StatusFalsePositive, vulnmodel.StatusRiskAccepted:
		return vuln != nil &&
			(vuln.Status == vulnmodel.StatusActive || vuln.Status == vulnmodel.StatusRetesting) &&
			s.canActorCloseVulnerability(actor, vuln)
	case vulnmodel.StatusActive:
		return vuln != nil &&
			(vuln.Status == vulnmodel.StatusFixed || vuln.Status == vulnmodel.StatusFalsePositive || vuln.Status == vulnmodel.StatusRiskAccepted) &&
			s.canActorCloseVulnerability(actor, vuln)
	default:
		return false
	}
}

func (s *Service) canActorCommentOnVulnerability(actor *actorContext, vuln *vulnmodel.Vulnerability) bool {
	if actor == nil || vuln == nil {
		return false
	}
	if s.canActorBypassRestrictedVisibility(actor) || actor.UserID == vuln.SubmitterUserID {
		return true
	}
	if actor.EmployeeID == nil {
		return false
	}
	return matchesEmployee(actor.EmployeeID, vuln.FixerEmployeeID)
}

func matchesEmployee(left *uint64, right *uint64) bool {
	return left != nil && right != nil && *left == *right
}

func matchesOptionalString(left *string, right *string) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return *left == *right
}

func containsEmployeeID(ids vulnmodel.UInt64Array, target uint64) bool {
	for _, id := range ids {
		if id == target {
			return true
		}
	}
	return false
}

func actorPermissionSet(actor *actorContext, permission string) bool {
	if actor == nil {
		return false
	}
	_, ok := actor.Permissions[permission]
	return ok
}

func normalizeAuthorizedViewerIDs(ids []uint64) []uint64 {
	if len(ids) == 0 {
		return []uint64{}
	}
	seen := make(map[uint64]struct{}, len(ids))
	out := make([]uint64, 0, len(ids))
	for _, id := range ids {
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	slices.Sort(out)
	return out
}

func normalizeTags(tags []string) []string {
	if len(tags) == 0 {
		return []string{}
	}
	seen := make(map[string]struct{}, len(tags))
	out := make([]string, 0, len(tags))
	for _, tag := range tags {
		trimmed := strings.TrimSpace(tag)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	slices.Sort(out)
	return out
}

func uint64Pointers(ids []uint64) []*uint64 {
	out := make([]*uint64, 0, len(ids))
	for _, id := range ids {
		value := id
		out = append(out, &value)
	}
	return out
}

func activeEmployeesOnly(employees []directory.Employee) []directory.Employee {
	if len(employees) == 0 {
		return []directory.Employee{}
	}
	out := make([]directory.Employee, 0, len(employees))
	for _, employee := range employees {
		if employeeIsActive(employee) {
			out = append(out, employee)
		}
	}
	return out
}

func employeeIsActive(employee directory.Employee) bool {
	return strings.EqualFold(strings.TrimSpace(employee.Status), "active")
}

type activeDirectoryEmployeeSearcher interface {
	SearchActiveEmployees(ctx context.Context, query string) ([]directory.Employee, error)
}

func searchActiveDirectoryEmployees(ctx context.Context, employeeRepo directory.EmployeeDirectory, query string) ([]directory.Employee, error) {
	if searcher, ok := employeeRepo.(activeDirectoryEmployeeSearcher); ok {
		return searcher.SearchActiveEmployees(ctx, query)
	}
	employees, err := employeeRepo.SearchEmployees(ctx, query)
	if err != nil {
		return nil, err
	}
	return activeEmployeesOnly(employees), nil
}

func (s *Service) ValidateEmployeeIDs(ctx context.Context, ids ...*uint64) error {
	if s.employeeRepo == nil {
		return nil
	}
	for _, id := range ids {
		if id == nil {
			continue
		}
		if *id == 0 {
			return errors.New("employee id must be non-zero")
		}
		if _, err := s.employeeRepo.FindByID(ctx, *id); err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fmt.Errorf("employee %d not found", *id)
			}
			return err
		}
	}
	return nil
}

func vulnerabilityIsOverdue(status vulnmodel.VulnerabilityStatus, slaDeadline time.Time, now time.Time) bool {
	switch status {
	case vulnmodel.StatusFixed, vulnmodel.StatusFalsePositive, vulnmodel.StatusRiskAccepted:
		return false
	default:
		return !slaDeadline.IsZero() && now.After(slaDeadline)
	}
}

func (s *Service) GetConfiguration(ctx context.Context) (*Configuration, error) {
	// hydrate SLA rules from repo if available
	if s.slaRepo != nil {
		if dbRules, err := s.slaRepo.List(ctx); err == nil && len(dbRules) > 0 {
			for _, r := range dbRules {
				s.slaConfig[strings.ToLower(r.Severity)] = r.Hours
			}
		}
	}
	config := &Configuration{
		SeverityLevels:           []string{"critical", "high", "medium", "low"},
		DiscoveryPhases:          []string{"pre_release", "post_release"},
		NetworkEnvs:              []string{"internal", "external"},
		Sources:                  defaultSources(),
		SLARules:                 s.slaConfig,
		Types:                    defaultVulnerabilityTypes(),
		SubmissionRequiredFields: s.submissionRequiredFields(ctx),
	}

	if s.channelRepo != nil {
		channels, _, err := s.channelRepo.List(ctx, repository.DiscoveryChannelListParams{Page: 1, PageSize: 500})
		if err == nil && len(channels) > 0 {
			config.DiscoveryChannels = make([]string, 0, len(channels))
			for _, ch := range channels {
				config.DiscoveryChannels = append(config.DiscoveryChannels, ch.Label)
			}
		}
	}
	if len(config.DiscoveryChannels) == 0 {
		config.DiscoveryChannels = []string{"white_box", "black_box", "manual_audit", "threat_intel"}
	}

	if s.typeRepo != nil {
		types, _, err := s.typeRepo.List(ctx, repository.VulnerabilityTypeListParams{Page: 1, PageSize: 500})
		if err == nil && len(types) > 0 {
			config.Types = make([]VulnerabilityTypeInfo, 0, len(types))
			for _, t := range types {
				config.Types = append(config.Types, VulnerabilityTypeInfo{
					Name:            t.Name,
					Category:        t.Category,
					DefaultSeverity: string(t.DefaultSeverity),
					Description:     t.Description,
				})
			}
		}
	}

	if s.sourceRepo != nil {
		sources, _, err := s.sourceRepo.List(ctx, repository.SourceListParams{Page: 1, PageSize: 500})
		if err == nil && len(sources) > 0 {
			config.Sources = make([]string, 0, len(sources))
			for _, s := range sources {
				if s.Label != "" {
					config.Sources = append(config.Sources, s.Label)
				} else {
					config.Sources = append(config.Sources, s.Name)
				}
			}
		}
	}

	return config, nil
}

func defaultVulnerabilityTypes() []VulnerabilityTypeInfo {
	return []VulnerabilityTypeInfo{
		{
			Name:            "SQL Injection",
			Category:        "Web",
			DefaultSeverity: "high",
			Description:     "Detected by code scanning or manual testing on API endpoints.",
		},
		{
			Name:            "Dependency Vulnerability",
			Category:        "Third-Party",
			DefaultSeverity: "critical",
			Description:     "Imported from dependency advisories (e.g., log4j).",
		},
		{
			Name:            "Business Logic",
			Category:        "Application",
			DefaultSeverity: "medium",
			Description:     "Workflows missing authorization or rate limiting.",
		},
	}
}

func defaultSources() []string {
	return []string{
		"bug_bounty",
		"internal_scan",
		"red_team",
		"pen_test",
		"customer_report",
		"threat_intel",
	}
}
