package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/errs"
	providerrepo "github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/repository/provider"
	"github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/types/entity"
	"github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/types/enum"
	"github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/types/query"
	"github.com/codex-k8s/kodex/services/internal/provider-hub/internal/domain/types/value"
)

const (
	watermarkStart                       = "<!-- kodex:artifact v1"
	watermarkEnd                         = "-->"
	relationshipSource                   = "source"
	relationshipParent                   = "parent"
	relationshipNext                     = "next"
	relationshipProjectRepositoryBinding = "project_repository_binding"
	onboardingMergeSignalStatus          = "merged"
	projectionSummarySize                = 320
)

var whitespacePattern = regexp.MustCompile(`\s+`)

type watermarkParseResult struct {
	status enum.WorkItemWatermarkStatus
	fields map[string]string
}

func (s *Service) projectionUpdateFromFacts(ctx context.Context, webhook entity.WebhookEvent, facts value.ProviderWebhookFacts) (providerrepo.ProjectionUpdate, []entity.OutboxEvent, error) {
	now := s.clock.Now().UTC()
	var update providerrepo.ProjectionUpdate
	var events []entity.OutboxEvent
	if facts.WorkItem != nil {
		workItem, relationships, err := workItemProjectionFromSnapshot(*facts.WorkItem, now)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		if facts.MergeSignal != nil {
			enriched, err := s.enrichOnboardingMergeProjection(ctx, workItem)
			if err != nil {
				return providerrepo.ProjectionUpdate{}, nil, err
			}
			workItem = enriched
		}
		update.WorkItem = &workItem
		update.Relationships = append(update.Relationships, relationships...)
		event, err := s.workItemSyncedOutbox(webhook, workItem)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		events = append(events, event)
		if facts.MergeSignal != nil {
			mergeSignal, ok, err := s.repositoryMergeSignalFromFacts(ctx, webhook, facts, workItem)
			if err != nil {
				return providerrepo.ProjectionUpdate{}, nil, err
			}
			if ok {
				update.MergeSignal = &mergeSignal
				event, err := s.repositoryMergedOutbox(webhook, mergeSignal)
				if err != nil {
					return providerrepo.ProjectionUpdate{}, nil, err
				}
				events = append(events, event)
			}
		}
	}
	if facts.RepositoryChange != nil {
		changeSignal, err := repositoryChangeSignalFromFacts(webhook, *facts.RepositoryChange, now)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		update.ChangeSignal = &changeSignal
		event, err := s.repositoryChangedOutbox(webhook, changeSignal)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		events = append(events, event)
	}
	if facts.Comment != nil && update.WorkItem != nil {
		comment := commentProjectionFromSnapshot(*facts.Comment, now)
		update.Comments = append(update.Comments, comment)
		event, err := s.commentSyncedOutbox(webhook, comment, facts)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		events = append(events, event)
	}
	for _, relationship := range update.Relationships {
		event, err := s.relationshipSyncedOutbox(webhook, relationship, facts)
		if err != nil {
			return providerrepo.ProjectionUpdate{}, nil, err
		}
		events = append(events, event)
	}
	return update, events, nil
}

func workItemProjectionFromSnapshot(snapshot value.ProviderWorkItemSnapshot, now time.Time) (entity.ProviderWorkItemProjection, []entity.ProviderRelationship, error) {
	providerSlug := enum.ProviderSlug(strings.TrimSpace(snapshot.ProviderSlug))
	kind := enum.WorkItemKind(strings.TrimSpace(snapshot.Kind))
	providerID := strings.TrimSpace(snapshot.ProviderWorkItemID)
	repositoryFullName := strings.TrimSpace(snapshot.RepositoryFullName)
	body := strings.TrimSpace(snapshot.Body)
	watermark := parseWatermark(body, kind)
	watermarkJSON, err := json.Marshal(watermark.fields)
	if err != nil {
		return entity.ProviderWorkItemProjection{}, nil, err
	}
	labels := trimStrings(snapshot.Labels)
	labelsJSON, err := json.Marshal(labels)
	if err != nil {
		return entity.ProviderWorkItemProjection{}, nil, err
	}
	projectID, err := optionalSnapshotUUID(snapshot.ProjectID)
	if err != nil {
		return entity.ProviderWorkItemProjection{}, nil, err
	}
	repositoryID, err := optionalSnapshotUUID(snapshot.RepositoryID)
	if err != nil {
		return entity.ProviderWorkItemProjection{}, nil, err
	}
	assigneesJSON, err := json.Marshal(trimStrings(snapshot.Assignees))
	if err != nil {
		return entity.ProviderWorkItemProjection{}, nil, err
	}
	providerUpdatedAt := timePtrIfSet(snapshot.ProviderUpdatedAt)
	workItemID := stableUUID("work-item", string(providerSlug), providerID)
	workItem := entity.ProviderWorkItemProjection{
		Base:               entity.Base{ID: workItemID, Version: 1, CreatedAt: now, UpdatedAt: now},
		ProjectID:          projectID,
		RepositoryID:       repositoryID,
		ProviderSlug:       providerSlug,
		ProviderWorkItemID: providerID,
		RepositoryFullName: repositoryFullName,
		Kind:               kind,
		Number:             snapshot.Number,
		URL:                strings.TrimSpace(snapshot.URL),
		Title:              strings.TrimSpace(snapshot.Title),
		State:              strings.TrimSpace(snapshot.State),
		WorkItemType:       workItemType(watermark.fields, labels),
		LabelsJSON:         labelsJSON,
		AssigneesJSON:      assigneesJSON,
		Milestone:          strings.TrimSpace(snapshot.Milestone),
		ProjectFieldsJSON:  []byte(`{}`),
		WatermarkStatus:    watermark.status,
		WatermarkJSON:      watermarkJSON,
		BodyDigest:         bodyDigest(body),
		ProviderUpdatedAt:  providerUpdatedAt,
		SyncedAt:           now,
		DriftStatus:        enum.WorkItemDriftStatusFresh,
	}
	relationships := relationshipsFromWatermark(workItem.ID, watermark.fields, now)
	return workItem, relationships, nil
}

func optionalSnapshotUUID(raw string) (*uuid.UUID, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return nil, nil
	}
	id, err := uuid.Parse(value)
	if err != nil || id == uuid.Nil {
		return nil, fmt.Errorf("invalid snapshot uuid %q", value)
	}
	return &id, nil
}

func projectRepositoryBindingRelationships(workItemID uuid.UUID, projectID *uuid.UUID, repositoryID *uuid.UUID, now time.Time) []entity.ProviderRelationship {
	if projectID == nil || repositoryID == nil {
		return nil
	}
	targetProviderRef := "project-catalog:project:" + projectID.String() + ":repository:" + repositoryID.String()
	return []entity.ProviderRelationship{{
		ID:                stableUUID("relationship", workItemID.String(), relationshipProjectRepositoryBinding, targetProviderRef),
		Version:           1,
		SourceWorkItemID:  workItemID,
		TargetProviderRef: targetProviderRef,
		RelationshipType:  relationshipProjectRepositoryBinding,
		Source:            enum.RelationshipSourceManual,
		Confidence:        enum.RelationshipConfidenceConfirmed,
		CreatedAt:         now,
	}}
}

func (s *Service) enrichOnboardingMergeProjection(ctx context.Context, workItem entity.ProviderWorkItemProjection) (entity.ProviderWorkItemProjection, error) {
	existing, err := s.repository.GetWorkItemProjection(ctx, query.ProviderTargetLookup{
		ProviderSlug:     workItem.ProviderSlug,
		ProviderObjectID: workItem.ProviderWorkItemID,
	})
	if errors.Is(err, errs.ErrNotFound) {
		return workItem, nil
	}
	if err != nil {
		return entity.ProviderWorkItemProjection{}, err
	}
	if workItem.ProjectID == nil {
		workItem.ProjectID = existing.ProjectID
	}
	if workItem.RepositoryID == nil {
		workItem.RepositoryID = existing.RepositoryID
	}
	if strings.TrimSpace(workItem.WorkItemType) == "" {
		workItem.WorkItemType = existing.WorkItemType
	}
	if existing.WatermarkStatus == enum.WorkItemWatermarkStatusValid {
		if _, ok := onboardingMergeWatermarkKind(existing); ok {
			workItem.WorkItemType = existing.WorkItemType
			workItem.WatermarkStatus = existing.WatermarkStatus
			workItem.WatermarkJSON = append(workItem.WatermarkJSON[:0], existing.WatermarkJSON...)
			return workItem, nil
		}
	}
	if workItem.WatermarkStatus == enum.WorkItemWatermarkStatusMissing && len(existing.WatermarkJSON) > 0 {
		workItem.WatermarkStatus = existing.WatermarkStatus
		workItem.WatermarkJSON = append(workItem.WatermarkJSON[:0], existing.WatermarkJSON...)
	}
	return workItem, nil
}

type onboardingMergeAnchor struct {
	kind         enum.RepositoryMergeSignalKind
	sourceRef    string
	operationRef string
}

func (s *Service) repositoryMergeSignalFromFacts(ctx context.Context, webhook entity.WebhookEvent, facts value.ProviderWebhookFacts, workItem entity.ProviderWorkItemProjection) (entity.RepositoryMergeSignal, bool, error) {
	if facts.MergeSignal == nil || workItem.Kind != enum.WorkItemKindPullRequest || strings.TrimSpace(workItem.State) != onboardingMergeSignalStatus {
		return entity.RepositoryMergeSignal{}, false, nil
	}
	if workItem.ProjectID == nil || workItem.RepositoryID == nil {
		return entity.RepositoryMergeSignal{}, false, nil
	}
	anchor, ok, err := s.onboardingMergeAnchor(ctx, workItem, *facts.MergeSignal)
	if err != nil {
		return entity.RepositoryMergeSignal{}, false, err
	}
	if !ok {
		return entity.RepositoryMergeSignal{}, false, nil
	}
	if strings.TrimSpace(facts.MergeSignal.MergeCommitSHA) == "" ||
		strings.TrimSpace(facts.MergeSignal.BaseBranch) == "" ||
		strings.TrimSpace(facts.MergeSignal.HeadBranch) == "" {
		return entity.RepositoryMergeSignal{}, false, errs.ErrInvalidArgument
	}
	now := s.clock.Now().UTC()
	mergedAt := facts.MergeSignal.MergedAt.UTC()
	if mergedAt.IsZero() {
		mergedAt = facts.OccurredAt.UTC()
	}
	if mergedAt.IsZero() {
		mergedAt = webhook.ReceivedAt.UTC()
	}
	kind := anchor.kind
	signalKey := repositoryMergeSignalKey(workItem.ProviderSlug, kind, workItem.ProviderWorkItemID)
	signal := entity.RepositoryMergeSignal{
		Base: entity.Base{
			ID:        stableUUID("repository-merge-signal", signalKey),
			Version:   1,
			CreatedAt: now,
			UpdatedAt: now,
		},
		SignalKey:                   signalKey,
		Kind:                        kind,
		ProviderSlug:                workItem.ProviderSlug,
		ProjectID:                   workItem.ProjectID,
		RepositoryID:                workItem.RepositoryID,
		RepositoryFullName:          workItem.RepositoryFullName,
		ProviderRepositoryID:        strings.TrimSpace(facts.RepositoryProviderID),
		WorkItemProjectionID:        workItem.ID,
		ProviderWorkItemID:          workItem.ProviderWorkItemID,
		PullRequestNumber:           workItem.Number,
		PullRequestProviderID:       strings.TrimSpace(facts.MergeSignal.PullRequestProviderID),
		PullRequestURL:              strings.TrimSpace(facts.MergeSignal.PullRequestURL),
		BaseBranch:                  strings.TrimSpace(facts.MergeSignal.BaseBranch),
		HeadBranch:                  strings.TrimSpace(facts.MergeSignal.HeadBranch),
		MergeCommitSHA:              strings.TrimSpace(facts.MergeSignal.MergeCommitSHA),
		SourceRef:                   anchor.sourceRef,
		RelatedProviderOperationRef: anchor.operationRef,
		WatermarkDigest:             bodyDigest(string(workItem.WatermarkJSON)),
		ObservedAt:                  webhook.ReceivedAt.UTC(),
		MergedAt:                    mergedAt,
		Status:                      enum.RepositoryMergeSignalStatusMerged,
	}
	return signal, true, nil
}

func repositoryChangeSignalFromFacts(webhook entity.WebhookEvent, snapshot value.ProviderRepositoryChangeSignalSnapshot, now time.Time) (entity.RepositoryChangeSignal, error) {
	providerSlug := webhook.ProviderSlug
	signalKey := strings.TrimSpace(snapshot.SignalKey)
	kind := enum.RepositoryChangeSignalKind(strings.TrimSpace(snapshot.EventKind))
	repositoryFullName := strings.TrimSpace(snapshot.RepositoryFullName)
	providerRepositoryID := strings.TrimSpace(snapshot.ProviderRepositoryID)
	if providerRepositoryID == "" {
		providerRepositoryID = strings.TrimSpace(webhook.RepositoryProviderID)
	}
	baseBranch := strings.TrimSpace(snapshot.BaseBranch)
	commitSHA := strings.TrimSpace(snapshot.CommitSHA)
	pathSummaryStatus := enum.RepositoryChangePathSummaryStatus(strings.TrimSpace(snapshot.PathSummaryStatus))
	if signalKey == "" ||
		kind == "" ||
		repositoryFullName == "" ||
		providerRepositoryID == "" ||
		baseBranch == "" ||
		commitSHA == "" ||
		pathSummaryStatus == "" ||
		strings.TrimSpace(snapshot.ChangeFingerprint) == "" {
		return entity.RepositoryChangeSignal{}, errs.ErrInvalidArgument
	}
	observedAt := snapshot.ObservedAt.UTC()
	if observedAt.IsZero() {
		observedAt = webhook.ReceivedAt.UTC()
	}
	if observedAt.IsZero() {
		observedAt = now
	}
	return entity.RepositoryChangeSignal{
		Base: entity.Base{
			ID:        stableUUID("repository-change-signal", signalKey),
			Version:   1,
			CreatedAt: now,
			UpdatedAt: now,
		},
		SignalKey:             signalKey,
		Kind:                  kind,
		ProviderSlug:          providerSlug,
		RepositoryFullName:    repositoryFullName,
		ProviderRepositoryID:  providerRepositoryID,
		Ref:                   strings.TrimSpace(snapshot.Ref),
		BaseBranch:            baseBranch,
		CommitSHA:             commitSHA,
		BeforeSHA:             strings.TrimSpace(snapshot.BeforeSHA),
		SourceRef:             strings.TrimSpace(snapshot.SourceRef),
		PullRequestNumber:     snapshot.PullRequestNumber,
		PullRequestProviderID: strings.TrimSpace(snapshot.PullRequestProviderID),
		PullRequestURL:        strings.TrimSpace(snapshot.PullRequestURL),
		PathSummaryStatus:     pathSummaryStatus,
		ChangedPathCount:      snapshot.ChangedPathCount,
		PathDigest:            strings.TrimSpace(snapshot.PathDigest),
		PathCategories:        repositoryChangePathCategoriesFromFacts(snapshot.PathCategories),
		ServicesPolicyChanged: snapshot.ServicesPolicyChanged,
		DeployRelevantChanged: snapshot.DeployRelevantChanged,
		ChangeFingerprint:     strings.TrimSpace(snapshot.ChangeFingerprint),
		ObservedAt:            observedAt,
		Status:                enum.RepositoryChangeSignalStatusObserved,
	}, nil
}

func repositoryChangePathCategoriesFromFacts(categories []value.ProviderRepositoryChangePathCategoryCount) []entity.RepositoryChangePathCategoryCount {
	result := make([]entity.RepositoryChangePathCategoryCount, 0, len(categories))
	for _, category := range categories {
		name := enum.RepositoryChangePathCategory(strings.TrimSpace(category.Category))
		if name == "" || category.Count <= 0 {
			continue
		}
		result = append(result, entity.RepositoryChangePathCategoryCount{
			Category: name,
			Count:    category.Count,
		})
	}
	return result
}

func (s *Service) onboardingMergeAnchor(ctx context.Context, workItem entity.ProviderWorkItemProjection, signal value.ProviderRepositoryMergeSignalSnapshot) (onboardingMergeAnchor, bool, error) {
	kind, fieldsOK := onboardingMergeWatermarkKind(workItem)
	if !fieldsOK {
		return onboardingMergeAnchor{}, false, nil
	}
	hasBinding, err := s.hasProjectRepositoryBinding(ctx, workItem)
	if err != nil {
		return onboardingMergeAnchor{}, false, err
	}
	if !hasBinding {
		return onboardingMergeAnchor{}, false, nil
	}
	if operationRef, sourceRef, ok, err := s.onboardingMergeOperationAnchor(ctx, kind, workItem, signal); err != nil {
		return onboardingMergeAnchor{}, false, err
	} else if ok {
		return onboardingMergeAnchor{kind: kind, sourceRef: sourceRef, operationRef: operationRef}, true, nil
	}
	fields := watermarkFields(workItem)
	sourceRef := strings.TrimSpace(fields["source_ref"])
	operationRef := onboardingMergeWatermarkOperationRef(fields)
	if sourceRef == "" || operationRef == "" || !onboardingSourceRefMatchesProvider(sourceRef, signal) {
		return onboardingMergeAnchor{}, false, nil
	}
	return onboardingMergeAnchor{kind: kind, sourceRef: sourceRef, operationRef: operationRef}, true, nil
}

func onboardingMergeWatermarkKind(workItem entity.ProviderWorkItemProjection) (enum.RepositoryMergeSignalKind, bool) {
	fields := watermarkFields(workItem)
	if fields["managed_by"] != "kodex" || strings.TrimSpace(fields["source_ref"]) == "" {
		return "", false
	}
	watermarkKind := strings.TrimSpace(fields["kind"])
	workType := strings.ToLower(strings.TrimSpace(fields["work_type"]))
	switch watermarkKind {
	case string(enum.WorkItemKindPullRequest):
		if workItem.WatermarkStatus != enum.WorkItemWatermarkStatusValid {
			return "", false
		}
		return onboardingMergeWorkTypeKind(workType)
	case "provider_pr":
		return onboardingMergeProviderPRWorkTypeKind(workType)
	default:
		return "", false
	}
}

func onboardingMergeWorkTypeKind(workType string) (enum.RepositoryMergeSignalKind, bool) {
	switch workType {
	case string(enum.RepositoryMergeSignalKindBootstrap), "repository_bootstrap":
		return enum.RepositoryMergeSignalKindBootstrap, true
	case string(enum.RepositoryMergeSignalKindAdoption), "repository_adoption":
		return enum.RepositoryMergeSignalKindAdoption, true
	default:
		return "", false
	}
}

func onboardingMergeProviderPRWorkTypeKind(workType string) (enum.RepositoryMergeSignalKind, bool) {
	switch workType {
	case "repository_bootstrap":
		return enum.RepositoryMergeSignalKindBootstrap, true
	case "repository_adoption":
		return enum.RepositoryMergeSignalKindAdoption, true
	default:
		return "", false
	}
}

func watermarkFields(workItem entity.ProviderWorkItemProjection) map[string]string {
	fields := map[string]string{}
	if len(workItem.WatermarkJSON) > 0 {
		_ = json.Unmarshal(workItem.WatermarkJSON, &fields)
	}
	return fields
}

func onboardingMergeWatermarkOperationRef(fields map[string]string) string {
	for _, field := range []string{"provider_operation_ref", "operation_ref"} {
		if operationRef := strings.TrimSpace(fields[field]); operationRef != "" {
			return operationRef
		}
	}
	return ""
}

func onboardingSourceRefMatchesProvider(sourceRef string, signal value.ProviderRepositoryMergeSignalSnapshot) bool {
	sourceRef = strings.TrimSpace(sourceRef)
	if sourceRef == "" {
		return false
	}
	for _, candidate := range []string{signal.SourceRef, signal.HeadBranch} {
		if strings.TrimSpace(candidate) == sourceRef {
			return true
		}
	}
	return false
}

func (s *Service) onboardingMergeOperationAnchor(
	ctx context.Context,
	kind enum.RepositoryMergeSignalKind,
	workItem entity.ProviderWorkItemProjection,
	signal value.ProviderRepositoryMergeSignalSnapshot,
) (string, string, bool, error) {
	operation, sourceRef, ok, err := s.onboardingMergeOperation(ctx, kind, workItem, signal)
	if err != nil || !ok {
		return "", "", ok, err
	}
	return "provider-hub:operation:" + operation.ID.String(), sourceRef, true, nil
}

func (s *Service) onboardingMergeOperation(
	ctx context.Context,
	kind enum.RepositoryMergeSignalKind,
	workItem entity.ProviderWorkItemProjection,
	signal value.ProviderRepositoryMergeSignalSnapshot,
) (entity.ProviderOperation, string, bool, error) {
	if workItem.RepositoryID == nil {
		return entity.ProviderOperation{}, "", false, nil
	}
	operationType, targetKind := onboardingMergeOperationIdentity(kind)
	if operationType == "" || targetKind == "" {
		return entity.ProviderOperation{}, "", false, nil
	}
	for _, sourceRef := range uniqueNonEmptyStrings(signal.SourceRef, signal.HeadBranch) {
		targetRef := repositoryTargetRef(workItem.ProviderSlug, workItem.RepositoryID.String()) + "#" + targetKind + ":" + sourceRef
		operations, _, err := s.repository.ListProviderOperations(ctx, query.ProviderOperationFilter{
			ProviderSlug:   workItem.ProviderSlug,
			OperationTypes: []enum.ProviderOperationType{operationType},
			Statuses:       []enum.ProviderOperationStatus{enum.ProviderOperationStatusSucceeded},
			TargetRef:      targetRef,
			Page:           value.PageRequest{PageSize: 2},
		})
		if err != nil {
			return entity.ProviderOperation{}, "", false, err
		}
		for _, operation := range operations {
			if onboardingMergeOperationMatches(operation, workItem, signal) {
				return operation, sourceRef, true, nil
			}
		}
	}
	return entity.ProviderOperation{}, "", false, nil
}

func onboardingMergeOperationIdentity(kind enum.RepositoryMergeSignalKind) (enum.ProviderOperationType, string) {
	switch kind {
	case enum.RepositoryMergeSignalKindBootstrap:
		return enum.ProviderOperationCreateBootstrapPullRequest, "bootstrap_pull_request"
	case enum.RepositoryMergeSignalKindAdoption:
		return enum.ProviderOperationCreateAdoptionPullRequest, "adoption_pull_request"
	default:
		return "", ""
	}
}

func onboardingMergeOperationMatches(operation entity.ProviderOperation, workItem entity.ProviderWorkItemProjection, signal value.ProviderRepositoryMergeSignalSnapshot) bool {
	if operation.ID == uuid.Nil {
		return false
	}
	if repository := strings.TrimSpace(operation.RepositoryFullName); repository != "" && repository != strings.TrimSpace(workItem.RepositoryFullName) {
		return false
	}
	if providerObjectID := strings.TrimSpace(operation.ProviderObjectID); providerObjectID != "" && providerObjectID != strings.TrimSpace(workItem.ProviderWorkItemID) {
		return false
	}
	if resultRef := strings.TrimSpace(operation.ResultRef); resultRef != "" && resultRef != strings.TrimSpace(workItem.URL) && resultRef != strings.TrimSpace(signal.PullRequestURL) {
		return false
	}
	return true
}

func uniqueNonEmptyStrings(values ...string) []string {
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func (s *Service) hasProjectRepositoryBinding(ctx context.Context, workItem entity.ProviderWorkItemProjection) (bool, error) {
	if workItem.ProjectID == nil || workItem.RepositoryID == nil {
		return false, nil
	}
	targetProviderRef := "project-catalog:project:" + workItem.ProjectID.String() + ":repository:" + workItem.RepositoryID.String()
	relationship, err := s.repository.GetRelationshipByIdentity(ctx, query.RelationshipLookup{
		SourceWorkItemID:  workItem.ID,
		TargetProviderRef: targetProviderRef,
		RelationshipType:  relationshipProjectRepositoryBinding,
	})
	if errors.Is(err, errs.ErrNotFound) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return relationship.Source == enum.RelationshipSourceManual && relationship.Confidence == enum.RelationshipConfidenceConfirmed, nil
}

func repositoryMergeSignalKey(providerSlug enum.ProviderSlug, kind enum.RepositoryMergeSignalKind, providerWorkItemID string) string {
	return "provider:" + string(providerSlug) + ":repository_merge:" + string(kind) + ":" + strings.TrimSpace(providerWorkItemID)
}

func commentProjectionFromSnapshot(snapshot value.ProviderCommentSnapshot, now time.Time) entity.ProviderCommentProjection {
	providerCommentID := strings.TrimSpace(snapshot.ProviderCommentID)
	providerWorkItemID := strings.TrimSpace(snapshot.ProviderWorkItemID)
	body := strings.TrimSpace(snapshot.Body)
	providerSlug := strings.TrimSpace(snapshot.ProviderSlug)
	return entity.ProviderCommentProjection{
		Base:                 entity.Base{ID: stableUUID("comment", providerSlug, providerWorkItemID, providerCommentID), Version: 1, CreatedAt: now, UpdatedAt: now},
		WorkItemProjectionID: stableUUID("work-item", providerSlug, providerWorkItemID),
		ProviderCommentID:    providerCommentID,
		Kind:                 enum.CommentKind(strings.TrimSpace(snapshot.Kind)),
		ReviewState:          enum.ReviewState(strings.TrimSpace(snapshot.ReviewState)),
		AuthorProviderLogin:  strings.TrimSpace(snapshot.AuthorLogin),
		BodyDigest:           bodyDigest(body),
		Summary:              summary(body),
		ProviderCreatedAt:    timePtrIfSet(snapshot.ProviderCreatedAt),
		ProviderUpdatedAt:    timePtrIfSet(snapshot.ProviderUpdatedAt),
	}
}

func parseWatermark(body string, actualKind enum.WorkItemKind) watermarkParseResult {
	start := strings.Index(body, watermarkStart)
	if start < 0 {
		return watermarkParseResult{status: enum.WorkItemWatermarkStatusMissing, fields: map[string]string{}}
	}
	rest := body[start+len(watermarkStart):]
	end := strings.Index(rest, watermarkEnd)
	if end < 0 {
		return watermarkParseResult{status: enum.WorkItemWatermarkStatusInvalid, fields: map[string]string{"error": "watermark end marker missing"}}
	}
	fields := make(map[string]string)
	for _, line := range strings.Split(rest[:end], "\n") {
		key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		value = strings.TrimSpace(value)
		if key != "" && value != "" {
			fields[key] = value
		}
	}
	if fields["kind"] == "" || fields["managed_by"] == "" || fields["work_type"] == "" {
		fields["error"] = "required watermark fields missing"
		return watermarkParseResult{status: enum.WorkItemWatermarkStatusInvalid, fields: fields}
	}
	if fields["kind"] != string(actualKind) {
		fields["error"] = "watermark kind mismatch"
		return watermarkParseResult{status: enum.WorkItemWatermarkStatusInvalid, fields: fields}
	}
	if actualKind != enum.WorkItemKindIssue && strings.TrimSpace(fields["source_ref"]) == "" {
		fields["error"] = "source_ref is required for pull request or merge request"
		return watermarkParseResult{status: enum.WorkItemWatermarkStatusInvalid, fields: fields}
	}
	return watermarkParseResult{status: enum.WorkItemWatermarkStatusValid, fields: fields}
}

func workItemType(watermark map[string]string, labels []string) string {
	if value := strings.TrimSpace(watermark["work_type"]); value != "" {
		return value
	}
	for _, label := range labels {
		trimmed := strings.TrimSpace(label)
		for _, prefix := range []string{"type:", "type/", "work_type:"} {
			if strings.HasPrefix(trimmed, prefix) {
				return strings.TrimSpace(strings.TrimPrefix(trimmed, prefix))
			}
		}
	}
	return ""
}

func relationshipsFromWatermark(sourceID uuid.UUID, watermark map[string]string, now time.Time) []entity.ProviderRelationship {
	candidates := []struct {
		field string
		kind  string
	}{
		{field: "source_ref", kind: relationshipSource},
		{field: "parent_ref", kind: relationshipParent},
		{field: "next_ref", kind: relationshipNext},
	}
	relationships := make([]entity.ProviderRelationship, 0, len(candidates))
	for _, candidate := range candidates {
		target := strings.TrimSpace(watermark[candidate.field])
		if target == "" {
			continue
		}
		relationships = append(relationships, entity.ProviderRelationship{
			ID:                stableUUID("relationship", sourceID.String(), candidate.kind, target),
			SourceWorkItemID:  sourceID,
			TargetProviderRef: target,
			RelationshipType:  candidate.kind,
			Source:            enum.RelationshipSourceWatermark,
			Confidence:        enum.RelationshipConfidenceConfirmed,
			CreatedAt:         now,
		})
	}
	return relationships
}

func (s *Service) workItemSyncedOutbox(webhook entity.WebhookEvent, workItem entity.ProviderWorkItemProjection) (entity.OutboxEvent, error) {
	payload, err := marshalProviderEventPayload(value.ProviderEventPayload{
		ProviderSlug:         string(webhook.ProviderSlug),
		WebhookEventID:       webhook.ID.String(),
		RepositoryFullName:   workItem.RepositoryFullName,
		WorkItemProjectionID: workItem.ID.String(),
		ProviderWorkItemID:   workItem.ProviderWorkItemID,
		Kind:                 string(workItem.Kind),
		Number:               workItem.Number,
		WatermarkStatus:      string(workItem.WatermarkStatus),
		DriftStatus:          string(workItem.DriftStatus),
		Version:              workItem.Version,
	})
	if err != nil {
		return entity.OutboxEvent{}, err
	}
	return outboxEventRecord(s.ids.New(), providerEventWorkItemSynced, providerAggregateWorkItem, workItem.ID, payload, workItem.SyncedAt), nil
}

func (s *Service) commentSyncedOutbox(webhook entity.WebhookEvent, comment entity.ProviderCommentProjection, facts value.ProviderWebhookFacts) (entity.OutboxEvent, error) {
	payload, err := marshalProviderEventPayload(value.ProviderEventPayload{
		ProviderSlug:         string(webhook.ProviderSlug),
		WebhookEventID:       webhook.ID.String(),
		RepositoryFullName:   facts.RepositoryFullName,
		WorkItemProjectionID: comment.WorkItemProjectionID.String(),
		CommentProjectionID:  comment.ID.String(),
		ProviderWorkItemID:   facts.ProviderWorkItemID,
		ProviderCommentID:    comment.ProviderCommentID,
		Kind:                 string(comment.Kind),
		ReviewState:          string(comment.ReviewState),
		Number:               facts.Number,
		Version:              comment.Version,
	})
	if err != nil {
		return entity.OutboxEvent{}, err
	}
	occurredAt := comment.UpdatedAt
	if comment.ProviderUpdatedAt != nil {
		occurredAt = *comment.ProviderUpdatedAt
	}
	return outboxEventRecord(s.ids.New(), providerEventCommentSynced, providerAggregateComment, comment.ID, payload, occurredAt), nil
}

func (s *Service) relationshipSyncedOutbox(webhook entity.WebhookEvent, relationship entity.ProviderRelationship, facts value.ProviderWebhookFacts) (entity.OutboxEvent, error) {
	payload, err := marshalProviderEventPayload(value.ProviderEventPayload{
		ProviderSlug:         string(webhook.ProviderSlug),
		WebhookEventID:       webhook.ID.String(),
		RepositoryFullName:   facts.RepositoryFullName,
		WorkItemProjectionID: relationship.SourceWorkItemID.String(),
		ProviderWorkItemID:   facts.ProviderWorkItemID,
		RelationshipID:       relationship.ID.String(),
		RelationshipType:     relationship.RelationshipType,
		Source:               string(relationship.Source),
	})
	if err != nil {
		return entity.OutboxEvent{}, err
	}
	return outboxEventRecord(s.ids.New(), providerEventRelationshipSynced, providerAggregateRelationship, relationship.ID, payload, relationship.CreatedAt), nil
}

func (s *Service) repositoryMergedOutbox(webhook entity.WebhookEvent, signal entity.RepositoryMergeSignal) (entity.OutboxEvent, error) {
	eventType := providerEventRepositoryBootstrapMerged
	if signal.Kind == enum.RepositoryMergeSignalKindAdoption {
		eventType = providerEventRepositoryAdoptionMerged
	}
	projectID := ""
	if signal.ProjectID != nil {
		projectID = signal.ProjectID.String()
	}
	repositoryID := ""
	if signal.RepositoryID != nil {
		repositoryID = signal.RepositoryID.String()
	}
	payload, err := marshalProviderEventPayload(value.ProviderEventPayload{
		ProviderSlug:                string(signal.ProviderSlug),
		WebhookEventID:              webhook.ID.String(),
		DeliveryID:                  webhook.DeliveryID,
		EventName:                   webhook.EventName,
		RepositoryMergeSignalID:     signal.ID.String(),
		SignalKey:                   signal.SignalKey,
		SignalKind:                  string(signal.Kind),
		ProjectID:                   projectID,
		RepositoryID:                repositoryID,
		RepositoryFullName:          signal.RepositoryFullName,
		ProviderRepositoryID:        signal.ProviderRepositoryID,
		ProviderWorkItemID:          signal.ProviderWorkItemID,
		WorkItemProjectionID:        signal.WorkItemProjectionID.String(),
		Number:                      signal.PullRequestNumber,
		PullRequestProviderID:       signal.PullRequestProviderID,
		PullRequestURL:              signal.PullRequestURL,
		BaseBranch:                  signal.BaseBranch,
		HeadBranch:                  signal.HeadBranch,
		MergeCommitSHA:              signal.MergeCommitSHA,
		SourceRef:                   signal.SourceRef,
		RelatedProviderOperationRef: signal.RelatedProviderOperationRef,
		WatermarkDigest:             signal.WatermarkDigest,
		ObservedAt:                  signal.ObservedAt.Format(time.RFC3339Nano),
		MergedAt:                    signal.MergedAt.Format(time.RFC3339Nano),
		Status:                      string(signal.Status),
		Version:                     signal.Version,
	})
	if err != nil {
		return entity.OutboxEvent{}, err
	}
	return outboxEventRecord(s.ids.New(), eventType, providerAggregateRepositoryMergeSignal, signal.ID, payload, signal.MergedAt), nil
}

func (s *Service) repositoryChangedOutbox(webhook entity.WebhookEvent, signal entity.RepositoryChangeSignal) (entity.OutboxEvent, error) {
	projectID := ""
	if signal.ProjectID != nil {
		projectID = signal.ProjectID.String()
	}
	repositoryID := ""
	if signal.RepositoryID != nil {
		repositoryID = signal.RepositoryID.String()
	}
	payload, err := marshalProviderEventPayload(value.ProviderEventPayload{
		ProviderSlug:             string(signal.ProviderSlug),
		WebhookEventID:           webhook.ID.String(),
		DeliveryID:               webhook.DeliveryID,
		EventName:                webhook.EventName,
		RepositoryChangeSignalID: signal.ID.String(),
		SignalKey:                signal.SignalKey,
		SignalKind:               string(signal.Kind),
		ProjectID:                projectID,
		RepositoryID:             repositoryID,
		RepositoryFullName:       signal.RepositoryFullName,
		ProviderRepositoryID:     signal.ProviderRepositoryID,
		BaseBranch:               signal.BaseBranch,
		HeadSHA:                  signal.CommitSHA,
		MergeCommitSHA:           signal.CommitSHA,
		SourceRef:                signal.SourceRef,
		PullRequestProviderID:    signal.PullRequestProviderID,
		PullRequestURL:           signal.PullRequestURL,
		Number:                   signal.PullRequestNumber,
		PathSummaryStatus:        string(signal.PathSummaryStatus),
		ChangedPathCount:         signal.ChangedPathCount,
		PathDigest:               signal.PathDigest,
		ServicesPolicyChanged:    signal.ServicesPolicyChanged,
		DeployRelevantChanged:    signal.DeployRelevantChanged,
		ChangeFingerprint:        signal.ChangeFingerprint,
		ObservedAt:               signal.ObservedAt.Format(time.RFC3339Nano),
		Status:                   string(signal.Status),
		Version:                  signal.Version,
	})
	if err != nil {
		return entity.OutboxEvent{}, err
	}
	return outboxEventRecord(s.ids.New(), providerEventRepositoryChanged, providerAggregateRepositoryChangeSignal, signal.ID, payload, signal.ObservedAt), nil
}

func stableUUID(parts ...string) uuid.UUID {
	return uuid.NewSHA1(uuid.NameSpaceURL, []byte("kodex/provider-hub/"+strings.Join(parts, "/")))
}

func bodyDigest(body string) string {
	digest := sha256.Sum256([]byte(strings.TrimSpace(body)))
	return hex.EncodeToString(digest[:])
}

func summary(body string) string {
	text := strings.TrimSpace(whitespacePattern.ReplaceAllString(body, " "))
	runes := []rune(text)
	if len(runes) <= projectionSummarySize {
		return text
	}
	return strings.TrimSpace(string(runes[:projectionSummarySize]))
}

func timePtrIfSet(value time.Time) *time.Time {
	if value.IsZero() {
		return nil
	}
	converted := value.UTC()
	return &converted
}
