package webhook

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"strings"
	"time"

	agentdomain "github.com/codex-k8s/kodex/libs/go/domain/agent"
	floweventdomain "github.com/codex-k8s/kodex/libs/go/domain/flowevent"
	rundomain "github.com/codex-k8s/kodex/libs/go/domain/run"
	webhookdomain "github.com/codex-k8s/kodex/libs/go/domain/webhook"
	"github.com/google/uuid"

	floweventrepo "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/repository/flowevent"
	runstatusdomain "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/runstatus"
	querytypes "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/types/query"
)

func (s *Service) recordIgnoredWebhook(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, params ignoredWebhookParams) (IngestResult, error) {
	s.recordIgnoredWebhookWarning(ctx, cmd, envelope, params)
	if err := s.postIgnoredWebhookDiagnosticComment(ctx, cmd, envelope, params); err != nil {
		return IngestResult{}, err
	}

	payload, err := buildIgnoredEventPayload(ignoredEventPayloadInput{
		Command:           cmd,
		Envelope:          envelope,
		Reason:            params.Reason,
		RunKind:           params.RunKind,
		HasBinding:        params.HasBinding,
		ConflictingLabels: params.ConflictingLabels,
		SuggestedLabels:   params.SuggestedLabels,
	})
	if err != nil {
		return IngestResult{}, fmt.Errorf("build ignored flow event payload: %w", err)
	}

	if err := s.insertSystemFlowEvent(ctx, cmd.CorrelationID, floweventdomain.EventTypeWebhookIgnored, payload, cmd.ReceivedAt); err != nil {
		return IngestResult{}, fmt.Errorf("insert ignored flow event: %w", err)
	}

	return IngestResult{
		CorrelationID: cmd.CorrelationID,
		Status:        webhookdomain.IngestStatusIgnored,
		Duplicate:     false,
	}, nil
}

func (s *Service) recordReceivedWebhookWithoutRun(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope) (IngestResult, error) {
	payload, err := buildReceivedEventPayload(cmd, envelope)
	if err != nil {
		return IngestResult{}, fmt.Errorf("build flow event payload: %w", err)
	}

	if err := s.insertSystemFlowEvent(ctx, cmd.CorrelationID, floweventdomain.EventTypeWebhookReceived, payload, cmd.ReceivedAt); err != nil {
		return IngestResult{}, fmt.Errorf("insert flow event: %w", err)
	}

	return IngestResult{
		CorrelationID: cmd.CorrelationID,
		Status:        webhookdomain.IngestStatusAccepted,
		Duplicate:     false,
	}, nil
}

func (s *Service) insertSystemFlowEvent(ctx context.Context, correlationID string, eventType floweventdomain.EventType, payload json.RawMessage, createdAt time.Time) error {
	return s.flowEvents.Insert(ctx, floweventrepo.InsertParams{
		CorrelationID: correlationID,
		ActorType:     floweventdomain.ActorTypeSystem,
		ActorID:       githubWebhookActorID,
		EventType:     eventType,
		Payload:       payload,
		CreatedAt:     createdAt,
	})
}

type pullRequestReviewFlowEventPayload struct {
	RepositoryFullName string                    `json:"repository_full_name"`
	PullRequestNumber  int64                     `json:"pull_request_number,omitempty"`
	IssueNumber        int64                     `json:"issue_number,omitempty"`
	ResolverSource     string                    `json:"resolver_source,omitempty"`
	TriggerLabel       string                    `json:"trigger_label,omitempty"`
	TriggerKind        webhookdomain.TriggerKind `json:"trigger_kind,omitempty"`
	Reason             string                    `json:"reason,omitempty"`
	ConflictingLabels  []string                  `json:"conflicting_labels,omitempty"`
	SuggestedLabels    []string                  `json:"suggested_labels,omitempty"`
}

func (s *Service) recordPullRequestReviewChangesRequestedEvent(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope) {
	payload, err := json.Marshal(pullRequestReviewFlowEventPayload{
		RepositoryFullName: strings.TrimSpace(envelope.Repository.FullName),
		PullRequestNumber:  envelope.PullRequest.Number,
		IssueNumber:        envelope.Issue.Number,
	})
	if err != nil {
		return
	}
	_ = s.insertSystemFlowEvent(ctx, cmd.CorrelationID, floweventdomain.EventTypeRunReviewChangesRequested, payload, cmd.ReceivedAt)
}

func (s *Service) recordPullRequestReviewStageResolvedEvent(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, trigger issueRunTrigger, meta pullRequestReviewResolutionMeta) {
	payload, err := json.Marshal(pullRequestReviewFlowEventPayload{
		RepositoryFullName: strings.TrimSpace(envelope.Repository.FullName),
		PullRequestNumber:  envelope.PullRequest.Number,
		IssueNumber:        meta.ResolvedIssueNumber,
		ResolverSource:     strings.TrimSpace(meta.ResolverSource),
		TriggerLabel:       strings.TrimSpace(trigger.Label),
		TriggerKind:        trigger.Kind,
		SuggestedLabels:    normalizeWebhookLabels(meta.SuggestedLabels),
	})
	if err != nil {
		return
	}
	_ = s.insertSystemFlowEvent(ctx, cmd.CorrelationID, floweventdomain.EventTypeRunReviseStageResolved, payload, cmd.ReceivedAt)
}

func (s *Service) recordPullRequestReviewStageAmbiguousEvent(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, conflict triggerConflictResult, meta pullRequestReviewResolutionMeta) {
	payload, err := json.Marshal(pullRequestReviewFlowEventPayload{
		RepositoryFullName: strings.TrimSpace(envelope.Repository.FullName),
		PullRequestNumber:  envelope.PullRequest.Number,
		IssueNumber:        meta.ResolvedIssueNumber,
		ResolverSource:     strings.TrimSpace(meta.ResolverSource),
		Reason:             strings.TrimSpace(conflict.IgnoreReason),
		ConflictingLabels:  normalizeWebhookLabels(conflict.ConflictingLabels),
		SuggestedLabels:    normalizeWebhookLabels(meta.SuggestedLabels),
	})
	if err != nil {
		return
	}
	_ = s.insertSystemFlowEvent(ctx, cmd.CorrelationID, floweventdomain.EventTypeRunReviseStageAmbiguous, payload, cmd.ReceivedAt)
}

func (s *Service) maybeCleanupRunNamespaces(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, hasBinding bool) error {
	if s.runStatus == nil || !hasBinding {
		return nil
	}

	repositoryFullName := strings.TrimSpace(envelope.Repository.FullName)
	if repositoryFullName == "" {
		return nil
	}

	eventType := strings.ToLower(strings.TrimSpace(cmd.EventType))
	action := strings.ToLower(strings.TrimSpace(envelope.Action))
	requestedByID := strings.TrimSpace(envelope.Sender.Login)
	if requestedByID == "" {
		requestedByID = string(githubWebhookActorID)
	}

	var cleanupErr error
	switch {
	case eventType == string(webhookdomain.GitHubEventIssues) &&
		(action == string(webhookdomain.GitHubActionClosed) || action == string(webhookdomain.GitHubActionDeleted)) &&
		envelope.Issue.Number > 0:
		if err := s.cleanupDiscussionRunsByIssue(ctx, requestedByID, repositoryFullName, envelope.Issue.Number); err != nil {
			return err
		}
		_, cleanupErr = s.runStatus.CleanupNamespacesByIssue(ctx, runstatusdomain.CleanupByIssueParams{
			RepositoryFullName: repositoryFullName,
			IssueNumber:        envelope.Issue.Number,
			RequestedByID:      requestedByID,
		})
	case eventType == string(webhookdomain.GitHubEventIssues) &&
		envelope.Issue.Number > 0 &&
		((action == string(webhookdomain.GitHubActionUnlabeled) && s.triggerLabels.isModeDiscussionLabel(envelope.Label.Name)) ||
			(action == string(webhookdomain.GitHubActionLabeled) && s.triggerLabels.isRunTriggerLabel(envelope.Label.Name))):
		cleanupErr = s.cleanupDiscussionRunsByIssue(ctx, requestedByID, repositoryFullName, envelope.Issue.Number)
	case eventType == string(webhookdomain.GitHubEventPullRequest) && action == "closed" && envelope.PullRequest.Number > 0:
		_, cleanupErr = s.runStatus.CleanupNamespacesByPullRequest(ctx, runstatusdomain.CleanupByPullRequestParams{
			RepositoryFullName: repositoryFullName,
			PRNumber:           envelope.PullRequest.Number,
			RequestedByID:      requestedByID,
		})
	}
	if cleanupErr != nil {
		return cleanupErr
	}

	return nil
}

func (s *Service) cleanupDiscussionRunsByIssue(ctx context.Context, requestedByID string, repositoryFullName string, issueNumber int64) error {
	if s.agentRuns == nil || strings.TrimSpace(repositoryFullName) == "" || issueNumber <= 0 {
		return nil
	}

	runIDs, err := s.agentRuns.ListRunIDsByRepositoryIssue(ctx, repositoryFullName, issueNumber, 200)
	if err != nil {
		return fmt.Errorf("list discussion run ids by repository/issue: %w", err)
	}

	discussionLabel := normalizeLabelToken(s.triggerLabels.withDefaults().ModeDiscussion)
	for _, runID := range runIDs {
		run, ok, err := s.agentRuns.GetByID(ctx, runID)
		if err != nil {
			return fmt.Errorf("get run %s for discussion cleanup: %w", runID, err)
		}
		if !ok {
			continue
		}
		if normalizeLabelToken(extractTriggerLabel(run.RunPayload)) != discussionLabel {
			continue
		}

		switch rundomain.Status(strings.TrimSpace(run.Status)) {
		case rundomain.StatusPending, rundomain.StatusRunning:
			if _, err := s.agentRuns.CancelActiveByID(ctx, run.ID); err != nil {
				return fmt.Errorf("cancel active discussion run %s: %w", run.ID, err)
			}
		}

		if _, err := s.runStatus.DeleteRunNamespace(ctx, runstatusdomain.DeleteNamespaceParams{
			RunID:           run.ID,
			RequestedByType: runstatusdomain.RequestedByTypeSystem,
			RequestedByID:   requestedByID,
		}); err != nil {
			return fmt.Errorf("delete discussion namespace for run %s: %w", run.ID, err)
		}
	}

	return nil
}

func extractTriggerLabel(runPayload json.RawMessage) string {
	if len(runPayload) == 0 {
		return ""
	}
	var payload querytypes.RunPayload
	if err := json.Unmarshal(runPayload, &payload); err != nil {
		return ""
	}
	if payload.Trigger == nil {
		return ""
	}
	return strings.TrimSpace(payload.Trigger.Label)
}

func (s *Service) resolveIssueRunTrigger(ctx context.Context, projectID string, eventType string, envelope githubWebhookEnvelope) (issueRunTrigger, bool, triggerConflictResult, pullRequestReviewResolutionMeta, error) {
	switch strings.TrimSpace(strings.ToLower(eventType)) {
	case string(webhookdomain.GitHubEventIssues):
		if !strings.EqualFold(strings.TrimSpace(envelope.Action), string(webhookdomain.GitHubActionLabeled)) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}

		label := strings.TrimSpace(envelope.Label.Name)
		if label == "" {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if s.triggerLabels.isModeDiscussionLabel(label) {
			trigger, conflict := s.resolveDiscussionTrigger(envelope.Issue.Labels, "", webhookdomain.TriggerSourceIssueLabel)
			if strings.TrimSpace(conflict.IgnoreReason) != "" {
				return issueRunTrigger{}, false, conflict, pullRequestReviewResolutionMeta{}, nil
			}
			active, err := s.hasActiveDiscussionRun(ctx, projectID, strings.TrimSpace(envelope.Repository.FullName), envelope.Issue.Number)
			if err != nil {
				return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, err
			}
			if active {
				return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
			}
			return trigger, true, conflict, pullRequestReviewResolutionMeta{}, nil
		}
		kind, ok := s.triggerLabels.resolveKind(label)
		if !ok {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		conflictingLabels := s.triggerLabels.collectIssueTriggerLabels(envelope.Issue.Labels)
		return issueRunTrigger{
				Source: webhookdomain.TriggerSourceIssueLabel,
				Label:  label,
				Kind:   kind,
			}, true, triggerConflictResult{
				ConflictingLabels: conflictingLabels,
			}, pullRequestReviewResolutionMeta{}, nil
	case string(webhookdomain.GitHubEventIssueComment):
		if !strings.EqualFold(strings.TrimSpace(envelope.Action), string(webhookdomain.GitHubActionCreated)) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if envelope.Issue.Number <= 0 || envelope.Issue.PullRequest != nil {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if !s.triggerLabels.hasModeDiscussionLabel(envelope.Issue.Labels) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if !s.isDiscussionCommentSenderAllowed(envelope.Sender) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		trigger, conflict := s.resolveDiscussionTrigger(envelope.Issue.Labels, "", webhookdomain.TriggerSourceIssueComment)
		if strings.TrimSpace(conflict.IgnoreReason) != "" {
			return issueRunTrigger{}, false, conflict, pullRequestReviewResolutionMeta{}, nil
		}
		active, err := s.hasActiveDiscussionRun(ctx, projectID, strings.TrimSpace(envelope.Repository.FullName), envelope.Issue.Number)
		if err != nil {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, err
		}
		if active {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		return trigger, true, conflict, pullRequestReviewResolutionMeta{}, nil
	case string(webhookdomain.GitHubEventPullRequest):
		if !strings.EqualFold(strings.TrimSpace(envelope.Action), string(webhookdomain.GitHubActionLabeled)) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if envelope.PullRequest.Number <= 0 {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}

		label := strings.TrimSpace(envelope.Label.Name)
		if label == "" || !s.triggerLabels.isNeedReviewerLabel(label) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		return issueRunTrigger{
			Source: triggerSourcePullRequestLabel,
			Label:  label,
			Kind:   webhookdomain.TriggerKindDev,
		}, true, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
	case string(webhookdomain.GitHubEventPullRequestReview):
		if !strings.EqualFold(strings.TrimSpace(envelope.Action), string(webhookdomain.GitHubActionSubmitted)) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		if !strings.EqualFold(strings.TrimSpace(envelope.Review.State), webhookdomain.GitHubReviewStateChangesRequested) {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
		}
		resolution, err := s.resolvePullRequestReviewReviseTrigger(ctx, projectID, envelope)
		if err != nil {
			return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, err
		}
		meta := pullRequestReviewResolutionMeta{
			ReceivedChangesRequested: true,
			ResolverSource:           resolution.resolverSource,
			ResolvedIssueNumber:      resolution.resolvedIssue,
			SuggestedLabels:          resolution.suggestedLabels,
		}
		if !resolution.ok {
			return issueRunTrigger{}, false, triggerConflictResult{
				ConflictingLabels: resolution.conflicting,
				IgnoreReason:      resolution.ignoreReason,
				SuggestedLabels:   resolution.suggestedLabels,
				ResolverSource:    resolution.resolverSource,
				ResolvedIssue:     resolution.resolvedIssue,
			}, meta, nil
		}
		return resolution.trigger, true, triggerConflictResult{
			ResolverSource: resolution.resolverSource,
			ResolvedIssue:  resolution.resolvedIssue,
		}, meta, nil
	default:
		return issueRunTrigger{}, false, triggerConflictResult{}, pullRequestReviewResolutionMeta{}, nil
	}
}

func (s *Service) resolvePushMainDeploy(eventType string, envelope githubWebhookEnvelope) (pushMainDeployTarget, bool) {
	if !strings.EqualFold(strings.TrimSpace(eventType), string(webhookdomain.GitHubEventPush)) {
		return pushMainDeployTarget{}, false
	}
	if !isMainBranchRef(envelope.Ref) {
		return pushMainDeployTarget{}, false
	}
	if envelope.Deleted || isDeletedGitCommitSHA(envelope.After) {
		return pushMainDeployTarget{}, false
	}

	buildRef := strings.TrimSpace(envelope.After)
	if buildRef == "" {
		return pushMainDeployTarget{}, false
	}

	target := pushMainDeployTarget{
		BuildRef:  buildRef,
		TargetEnv: "production",
		Namespace: s.platformNamespace,
	}
	return target, true
}

func isMainBranchRef(ref string) bool {
	normalized := strings.ToLower(strings.TrimSpace(ref))
	return normalized == "refs/heads/main" || normalized == "refs/heads/master"
}

func isDeletedGitCommitSHA(sha string) bool {
	normalized := strings.ToLower(strings.TrimSpace(sha))
	if normalized == "" {
		return false
	}
	for _, ch := range normalized {
		if ch != '0' {
			return false
		}
	}
	return true
}

func (s *Service) resolveCorrelationID(cmd IngestCommand, envelope githubWebhookEnvelope, trigger issueRunTrigger, hasIssueRunTrigger bool) string {
	correlationID := strings.TrimSpace(cmd.CorrelationID)
	if correlationID == "" {
		return correlationID
	}
	if !hasIssueRunTrigger {
		return correlationID
	}
	if !strings.EqualFold(strings.TrimSpace(trigger.Source), triggerSourcePullRequestLabel) {
		return correlationID
	}
	if !s.triggerLabels.isNeedReviewerLabel(trigger.Label) {
		return correlationID
	}

	repositoryFullName := strings.ToLower(strings.TrimSpace(envelope.Repository.FullName))
	if repositoryFullName == "" || envelope.PullRequest.Number <= 0 {
		return correlationID
	}

	action := strings.ToLower(strings.TrimSpace(envelope.Action))
	triggerLabel := strings.ToLower(strings.TrimSpace(trigger.Label))
	updatedAt := normalizeReviewerTriggerTimestamp(envelope.PullRequest.UpdatedAt)
	if action == "" || triggerLabel == "" || updatedAt == "" {
		return correlationID
	}

	signature := fmt.Sprintf("pull_request_label|%s|%d|%s|%s|%s", repositoryFullName, envelope.PullRequest.Number, action, triggerLabel, updatedAt)
	return "pr-label-reviewer-" + uuid.NewSHA1(uuid.NameSpaceURL, []byte(signature)).String()
}

func normalizeReviewerTriggerTimestamp(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return ""
	}
	parsed, err := time.Parse(time.RFC3339Nano, trimmed)
	if err != nil {
		return trimmed
	}
	return parsed.UTC().Format(time.RFC3339Nano)
}

func containsLabel(labels []githubLabelRecord, expected string) bool {
	target := normalizeLabelToken(expected)
	if target == "" {
		return false
	}
	for _, item := range labels {
		if normalizeLabelToken(item.Name) == target {
			return true
		}
	}
	return false
}

func hasAnyLabel(labels []githubLabelRecord, candidates ...string) bool {
	for _, label := range candidates {
		if containsLabel(labels, label) {
			return true
		}
	}
	return false
}

func normalizeWebhookLabels(labels []string) []string {
	result := make([]string, 0, len(labels))
	seen := make(map[string]struct{}, len(labels))
	for _, raw := range labels {
		label := strings.TrimSpace(raw)
		if label == "" {
			continue
		}
		if _, exists := seen[label]; exists {
			continue
		}
		seen[label] = struct{}{}
		result = append(result, label)
	}
	slices.Sort(result)
	return result
}

func (s *Service) postTriggerConflictComment(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, trigger issueRunTrigger, conflictingLabels []string) error {
	if s.runStatus == nil || envelope.Issue.Number <= 0 {
		return nil
	}
	repositoryFullName := strings.TrimSpace(envelope.Repository.FullName)
	if repositoryFullName == "" {
		return nil
	}
	_, err := s.runStatus.PostTriggerLabelConflictComment(ctx, runstatusdomain.TriggerLabelConflictCommentParams{
		CorrelationID:      cmd.CorrelationID,
		RepositoryFullName: repositoryFullName,
		IssueNumber:        int(envelope.Issue.Number),
		Locale:             localeFromEventType(cmd.EventType),
		TriggerLabel:       strings.TrimSpace(trigger.Label),
		ConflictingLabels:  conflictingLabels,
	})
	return err
}

func localeFromEventType(_ string) string {
	return "ru"
}

func (s *Service) recordIgnoredWebhookWarning(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, params ignoredWebhookParams) {
	if s.runtimeErr == nil || !isRunCreationWarningReason(params.Reason) {
		return
	}
	details, _ := json.Marshal(map[string]any{
		"reason":              strings.TrimSpace(params.Reason),
		"event_type":          strings.TrimSpace(cmd.EventType),
		"repository_fullname": strings.TrimSpace(envelope.Repository.FullName),
		"issue_number":        envelope.Issue.Number,
		"pull_request_number": envelope.PullRequest.Number,
		"run_kind":            strings.TrimSpace(string(params.RunKind)),
		"conflicting_labels":  params.ConflictingLabels,
		"suggested_labels":    params.SuggestedLabels,
	})
	message := "Run was not created: " + strings.TrimSpace(params.Reason)
	s.runtimeErr.RecordBestEffort(ctx, querytypes.RuntimeErrorRecordParams{
		Source:        "webhook.trigger",
		Level:         "warning",
		Message:       message,
		CorrelationID: strings.TrimSpace(cmd.CorrelationID),
		ProjectID:     "",
		DetailsJSON:   details,
	})
}

func (s *Service) postIgnoredWebhookDiagnosticComment(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, params ignoredWebhookParams) error {
	if s.runStatus == nil || !isRunCreationWarningReason(params.Reason) {
		return nil
	}
	repositoryFullName := strings.TrimSpace(envelope.Repository.FullName)
	if repositoryFullName == "" {
		return nil
	}

	threadKind := ""
	threadNumber := 0
	if strings.EqualFold(strings.TrimSpace(cmd.EventType), string(webhookdomain.GitHubEventPullRequestReview)) && envelope.PullRequest.Number > 0 {
		threadKind = "pull_request"
		threadNumber = int(envelope.PullRequest.Number)
	} else if envelope.Issue.Number > 0 {
		threadKind = "issue"
		threadNumber = int(envelope.Issue.Number)
	}
	if threadKind == "" || threadNumber <= 0 {
		return nil
	}

	if shouldEnsureNeedInputLabel(params.Reason) {
		_, err := s.runStatus.EnsureNeedInputLabel(ctx, runstatusdomain.EnsureNeedInputLabelParams{
			CorrelationID:      cmd.CorrelationID,
			RepositoryFullName: repositoryFullName,
			ThreadKind:         threadKind,
			ThreadNumber:       threadNumber,
		})
		if err != nil {
			s.recordNeedInputLabelFailure(ctx, cmd, envelope, threadKind, threadNumber, params.Reason, err)
			return fmt.Errorf("ensure need:input label before warning comment: %w", err)
		}
	}

	_, _ = s.runStatus.PostTriggerWarningComment(ctx, runstatusdomain.TriggerWarningCommentParams{
		CorrelationID:      cmd.CorrelationID,
		RepositoryFullName: repositoryFullName,
		ThreadKind:         threadKind,
		ThreadNumber:       threadNumber,
		Locale:             localeFromEventType(cmd.EventType),
		ReasonCode:         runstatusdomain.TriggerWarningReasonCode(strings.TrimSpace(params.Reason)),
		ConflictingLabels:  params.ConflictingLabels,
		SuggestedLabels:    params.SuggestedLabels,
	})
	return nil
}

func (s *Service) recordNeedInputLabelFailure(ctx context.Context, cmd IngestCommand, envelope githubWebhookEnvelope, threadKind string, threadNumber int, reason string, ensureErr error) {
	if s.runtimeErr == nil {
		return
	}
	details, _ := json.Marshal(map[string]any{
		"reason":              strings.TrimSpace(reason),
		"event_type":          strings.TrimSpace(cmd.EventType),
		"repository_fullname": strings.TrimSpace(envelope.Repository.FullName),
		"issue_number":        envelope.Issue.Number,
		"pull_request_number": envelope.PullRequest.Number,
		"thread_kind":         strings.TrimSpace(threadKind),
		"thread_number":       threadNumber,
		"error":               strings.TrimSpace(ensureErr.Error()),
	})
	s.runtimeErr.RecordBestEffort(ctx, querytypes.RuntimeErrorRecordParams{
		Source:        "webhook.trigger",
		Level:         "error",
		Message:       "Failed to set need:input label before warning comment",
		CorrelationID: strings.TrimSpace(cmd.CorrelationID),
		ProjectID:     "",
		DetailsJSON:   details,
	})
}

func shouldEnsureNeedInputLabel(reason string) bool {
	normalized := strings.TrimSpace(reason)
	switch normalized {
	case string(runstatusdomain.TriggerWarningReasonPullRequestReviewMissingStageLabel),
		string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageLabelConflict),
		string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageNotResolved),
		string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageAmbiguous),
		string(runstatusdomain.TriggerWarningReasonIssueTriggerCandidateNotFound):
		return true
	default:
		return false
	}
}

func isRunCreationWarningReason(reason string) bool {
	normalized := strings.TrimSpace(reason)
	if normalized == "" {
		return false
	}
	if normalized == string(runstatusdomain.TriggerWarningReasonPullRequestReviewMissingStageLabel) ||
		normalized == string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageLabelConflict) ||
		normalized == string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageNotResolved) ||
		normalized == string(runstatusdomain.TriggerWarningReasonPullRequestReviewStageAmbiguous) ||
		normalized == string(runstatusdomain.TriggerWarningReasonIssueTriggerCandidateNotFound) {
		return true
	}
	if normalized == string(runstatusdomain.TriggerWarningReasonRepositoryNotBoundForIssueLabel) {
		return true
	}
	return strings.HasPrefix(normalized, "sender_")
}

func (s *Service) postRunLaunchPlannedFeedback(ctx context.Context, runID string, trigger issueRunTrigger, runtimeMode agentdomain.RuntimeMode, runtimeNamespace string, eventType string) {
	if s.runStatus == nil {
		return
	}
	trimmedRunID := strings.TrimSpace(runID)
	if trimmedRunID == "" {
		return
	}

	_, _ = s.runStatus.UpsertRunStatusComment(ctx, runstatusdomain.UpsertCommentParams{
		RunID:        trimmedRunID,
		Phase:        runstatusdomain.PhaseCreated,
		RuntimeMode:  string(runtimeMode),
		Namespace:    strings.TrimSpace(runtimeNamespace),
		TriggerKind:  string(trigger.Kind),
		PromptLocale: localeFromEventType(eventType),
		RunStatus:    string(rundomain.StatusPending),
	})
}

func (s *Service) isActorAllowedForIssueTrigger(ctx context.Context, projectID string, senderLogin string, senderType string) (bool, string, error) {
	actorType := normalizeLabelToken(senderType)
	if actorType != "" && actorType != gitHubSenderTypeUser {
		return false, "sender_type_" + actorType + "_not_permitted", nil
	}

	login := strings.TrimSpace(senderLogin)
	if login == "" {
		return false, "sender_login_missing", nil
	}
	if s.users == nil {
		return false, "users_repository_unavailable", nil
	}

	u, ok, err := s.users.GetByGitHubLogin(ctx, login)
	if err != nil {
		return false, "", err
	}
	if !ok || u.ID == "" {
		return false, "sender_not_allowed", nil
	}
	if u.IsPlatformOwner {
		return true, "platform_owner", nil
	}
	if u.IsPlatformAdmin {
		return true, "platform_admin", nil
	}
	if s.members == nil {
		return false, "project_membership_repository_unavailable", nil
	}

	role, ok, err := s.members.GetRole(ctx, projectID, u.ID)
	if err != nil {
		return false, "", err
	}
	if !ok {
		return false, "sender_not_project_member", nil
	}

	switch strings.ToLower(strings.TrimSpace(role)) {
	case "admin", "read_write":
		return true, "project_member_" + strings.ToLower(strings.TrimSpace(role)), nil
	default:
		if strings.TrimSpace(role) == "" {
			return false, "sender_role_not_permitted", nil
		}
		return false, "sender_role_" + strings.ToLower(strings.TrimSpace(role)) + "_not_permitted", nil
	}
}

func triggerPtr(trigger issueRunTrigger, ok bool) *issueRunTrigger {
	if !ok {
		return nil
	}
	t := trigger
	return &t
}

func (s *Service) resolveRunAgent(ctx context.Context, projectID string, trigger *issueRunTrigger) (runAgentProfile, error) {
	agentKey := resolveRunAgentKey(trigger)
	if s.agents == nil {
		return runAgentProfile{}, fmt.Errorf("agent repository is not configured")
	}

	agent, ok, err := s.agents.FindEffectiveByKey(ctx, projectID, agentKey)
	if err != nil {
		return runAgentProfile{}, err
	}
	if !ok {
		return runAgentProfile{}, fmt.Errorf("failed_precondition: no active agent configured for key %q", agentKey)
	}

	return runAgentProfile{
		ID:   agent.ID,
		Key:  agent.AgentKey,
		Name: agent.Name,
	}, nil
}

func resolveRunAgentKey(trigger *issueRunTrigger) string {
	if trigger == nil {
		return defaultRunAgentKey
	}
	if strings.EqualFold(strings.TrimSpace(trigger.Source), triggerSourcePullRequestLabel) {
		return agentKeyReviewer
	}
	switch trigger.Kind {
	case webhookdomain.TriggerKindDev, webhookdomain.TriggerKindDevRevise:
		return defaultRunAgentKey
	case webhookdomain.TriggerKindIntake,
		webhookdomain.TriggerKindIntakeRevise,
		webhookdomain.TriggerKindVision,
		webhookdomain.TriggerKindVisionRevise,
		webhookdomain.TriggerKindPRD,
		webhookdomain.TriggerKindPRDRevise:
		return agentKeyPM
	case webhookdomain.TriggerKindArch,
		webhookdomain.TriggerKindArchRevise,
		webhookdomain.TriggerKindDesign,
		webhookdomain.TriggerKindDesignRevise:
		return agentKeySA
	case webhookdomain.TriggerKindPlan,
		webhookdomain.TriggerKindPlanRevise,
		webhookdomain.TriggerKindRelease,
		webhookdomain.TriggerKindReleaseRevise,
		webhookdomain.TriggerKindRethink:
		return agentKeyEM
	case webhookdomain.TriggerKindDocAudit,
		webhookdomain.TriggerKindDocAuditRevise:
		return agentKeyKM
	case webhookdomain.TriggerKindQA, webhookdomain.TriggerKindQARevise:
		return agentKeyQA
	case webhookdomain.TriggerKindAIRepair,
		webhookdomain.TriggerKindPostDeploy,
		webhookdomain.TriggerKindPostDeployRevise,
		webhookdomain.TriggerKindOps,
		webhookdomain.TriggerKindOpsRevise:
		return agentKeySRE
	case webhookdomain.TriggerKindSelfImprove,
		webhookdomain.TriggerKindSelfImproveRevise:
		return agentKeyKM
	default:
		return defaultRunAgentKey
	}
}

func (s *Service) resolveProjectBinding(ctx context.Context, envelope githubWebhookEnvelope) (projectID string, repositoryID string, servicesYAMLPath string, defaultRef string, ok bool, err error) {
	if s.repos == nil || envelope.Repository.ID == 0 {
		return "", "", "", "", false, nil
	}
	res, ok, err := s.repos.FindByProviderExternalID(ctx, "github", envelope.Repository.ID)
	if err != nil {
		return "", "", "", "", false, err
	}
	if !ok {
		return "", "", "", "", false, nil
	}
	return res.ProjectID, res.RepositoryID, res.ServicesYAMLPath, strings.TrimSpace(res.DefaultRef), true, nil
}

func (s *Service) resolveLearningMode(ctx context.Context, projectID string, senderLogin string) (bool, error) {
	if projectID == "" || s.projects == nil {
		return s.learningModeDefault, nil
	}
	projectDefault, ok, err := s.projects.GetLearningModeDefault(ctx, projectID)
	if err != nil {
		return false, err
	}
	if !ok {
		projectDefault = s.learningModeDefault
	}

	// Member override is optional and best-effort: if we can't map sender->user->member,
	// we fall back to project default.
	if s.users == nil || s.members == nil || senderLogin == "" {
		return projectDefault, nil
	}

	u, ok, err := s.users.GetByGitHubLogin(ctx, senderLogin)
	if err != nil {
		return false, err
	}
	if !ok || u.ID == "" {
		return projectDefault, nil
	}

	override, isMember, err := s.members.GetLearningModeOverride(ctx, projectID, u.ID)
	if err != nil {
		return false, err
	}
	if !isMember || override == nil {
		return projectDefault, nil
	}
	return *override, nil
}

func (s *Service) resolveRunRuntimeMode(trigger *issueRunTrigger) (agentdomain.RuntimeMode, string) {
	return s.runtimeModePolicy.resolve(trigger)
}

func deriveProjectID(correlationID string, envelope githubWebhookEnvelope) string {
	fullName := strings.TrimSpace(envelope.Repository.FullName)
	if fullName != "" {
		return uuid.NewSHA1(uuid.NameSpaceDNS, []byte("repo:"+strings.ToLower(fullName))).String()
	}
	return uuid.NewSHA1(uuid.NameSpaceDNS, []byte("correlation:"+correlationID)).String()
}
