package automation

import (
	"context"
	"fmt"
	"strings"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	MaxAutomationDefinitionsPerVersion    = 100
	MaxAutomationRepositoriesPerWorkspace = 1000
)

type ContentPublicationRecordMode uint8

const (
	ContentPublicationRecordStrict ContentPublicationRecordMode = iota
	// ContentPublicationRecordRecovery completes the Automation-owned
	// projection for a durable Content Store operation receipt. If a later
	// publication already advanced the repository projection, recovery
	// backfills only the immutable publication and definitions and never moves
	// current backward.
	ContentPublicationRecordRecovery
)

func (store *Store) RecordContentPublication(ctx context.Context, publication AutomationContentPublication, definitions []AutomationContentDefinition, expectedCurrentVersionID string, mode ContentPublicationRecordMode) error {
	if err := store.ensureDB(); err != nil {
		return err
	}
	if mode != ContentPublicationRecordStrict && mode != ContentPublicationRecordRecovery {
		return fmt.Errorf("automation content publication record mode is invalid")
	}
	recoveringOperation := mode == ContentPublicationRecordRecovery
	publication.WorkspaceID = strings.TrimSpace(publication.WorkspaceID)
	publication.ContentID = strings.TrimSpace(publication.ContentID)
	publication.ContentVersionID = strings.TrimSpace(publication.ContentVersionID)
	publication.RepositoryOwner = strings.TrimSpace(publication.RepositoryOwner)
	publication.RepositoryName = strings.TrimSpace(publication.RepositoryName)
	publication.Ref = strings.TrimSpace(publication.Ref)
	publication.ActorID = strings.TrimSpace(publication.ActorID)
	expectedCurrentVersionID = strings.TrimSpace(expectedCurrentVersionID)
	if publication.WorkspaceID == "" || publication.ContentID == "" || publication.ContentVersionID == "" ||
		publication.RepositoryOwner == "" || publication.RepositoryName == "" || publication.ActorID == "" ||
		publication.CreatedAt.IsZero() || len(definitions) > MaxAutomationDefinitionsPerVersion {
		return fmt.Errorf("automation content publication is invalid")
	}
	seen := make(map[string]struct{}, len(definitions))
	expected := make(map[string]AutomationContentDefinition, len(definitions))
	for index := range definitions {
		definition := &definitions[index]
		definition.WorkspaceID = publication.WorkspaceID
		definition.ContentID = publication.ContentID
		definition.ContentVersionID = publication.ContentVersionID
		definition.DefinitionPath = strings.TrimSpace(definition.DefinitionPath)
		definition.Digest = strings.TrimSpace(definition.Digest)
		definition.CreatedAt = publication.CreatedAt
		if definition.DefinitionPath == "" || len(definition.Digest) != 64 || definition.SizeBytes < 0 {
			return fmt.Errorf("automation content definition is invalid")
		}
		if _, ok := seen[definition.DefinitionPath]; ok {
			return fmt.Errorf("automation content definition path is duplicated")
		}
		seen[definition.DefinitionPath] = struct{}{}
		expected[definition.DefinitionPath] = *definition
	}
	return store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		created := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&publication)
		if created.Error != nil {
			return fmt.Errorf("record automation content publication: %w", created.Error)
		}
		var existing AutomationContentPublication
		if err := tx.Where("workspace_id = ? AND content_id = ? AND content_version_id = ?", publication.WorkspaceID, publication.ContentID, publication.ContentVersionID).First(&existing).Error; err != nil {
			return fmt.Errorf("reload automation content publication: %w", err)
		}
		if !sameAutomationContentPublication(existing, publication) {
			return fmt.Errorf("automation content publication replay conflicts")
		}
		publicationReplayed := created.RowsAffected == 0
		current := AutomationContentRepository{
			WorkspaceID: publication.WorkspaceID, RepositoryOwner: publication.RepositoryOwner,
			RepositoryName: publication.RepositoryName, ContentID: publication.ContentID,
			ContentVersionID: publication.ContentVersionID, Ref: publication.Ref, UpdatedAt: publication.CreatedAt,
		}
		createdCurrent := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&current)
		if createdCurrent.Error != nil {
			return fmt.Errorf("record current automation content repository: %w", createdCurrent.Error)
		}
		if createdCurrent.RowsAffected == 0 {
			updated := tx.Model(&AutomationContentRepository{}).
				Where("workspace_id = ? AND repository_owner = ? AND repository_name = ? AND content_version_id IN ?", current.WorkspaceID, current.RepositoryOwner, current.RepositoryName, []string{expectedCurrentVersionID, current.ContentVersionID}).
				Updates(map[string]any{
					"content_id": current.ContentID, "content_version_id": current.ContentVersionID,
					"ref": current.Ref, "updated_at": current.UpdatedAt,
				})
			if updated.Error != nil {
				return fmt.Errorf("advance current automation content repository: %w", updated.Error)
			}
		}
		var storedCurrent AutomationContentRepository
		if err := tx.Where("workspace_id = ? AND repository_owner = ? AND repository_name = ?", current.WorkspaceID, current.RepositoryOwner, current.RepositoryName).First(&storedCurrent).Error; err != nil {
			return fmt.Errorf("reload current automation content repository: %w", err)
		}
		if storedCurrent.ContentID != current.ContentID || (storedCurrent.ContentVersionID != current.ContentVersionID && !publicationReplayed && !recoveringOperation) {
			return fmt.Errorf("automation current content repository conflicts")
		}
		if len(definitions) > 0 {
			if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&definitions).Error; err != nil {
				return fmt.Errorf("record automation content definitions: %w", err)
			}
		}
		var stored []AutomationContentDefinition
		if err := tx.Where("workspace_id = ? AND content_id = ? AND content_version_id = ?", publication.WorkspaceID, publication.ContentID, publication.ContentVersionID).
			Order("definition_path").Limit(MaxAutomationDefinitionsPerVersion + 1).Find(&stored).Error; err != nil {
			return err
		}
		if len(stored) != len(definitions) {
			return fmt.Errorf("automation content definition replay conflicts")
		}
		for _, definition := range stored {
			wanted, ok := expected[definition.DefinitionPath]
			if !ok || wanted.Digest != definition.Digest || wanted.SizeBytes != definition.SizeBytes {
				return fmt.Errorf("automation content definition replay conflicts")
			}
		}
		return nil
	})
}

func (store *Store) GetContentPublication(ctx context.Context, workspaceID, contentID, versionID string) (AutomationContentPublication, error) {
	if err := store.ensureDB(); err != nil {
		return AutomationContentPublication{}, err
	}
	var publication AutomationContentPublication
	err := store.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ? AND content_version_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(versionID)).First(&publication).Error
	return publication, err
}

func (store *Store) GetCurrentContentRepository(ctx context.Context, workspaceID, owner, repository string) (AutomationContentRepository, error) {
	if err := store.ensureDB(); err != nil {
		return AutomationContentRepository{}, err
	}
	var current AutomationContentRepository
	err := store.db.WithContext(ctx).
		Where("workspace_id = ? AND repository_owner = ? AND repository_name = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(owner), strings.TrimSpace(repository)).
		First(&current).Error
	return current, err
}

func (store *Store) ListLatestContentPublicationRepositories(ctx context.Context, workspaceID string, offset, limit int) ([]AutomationContentPublication, error) {
	if err := store.ensureDB(); err != nil {
		return nil, err
	}
	if offset < 0 || limit <= 0 || limit > 100 || offset+limit > MaxAutomationRepositoriesPerWorkspace {
		return nil, fmt.Errorf("automation content repository page is invalid")
	}
	var publications []AutomationContentPublication
	err := store.db.WithContext(ctx).Model(&AutomationContentRepository{}).
		Select("workspace_id, content_id, content_version_id, repository_owner, repository_name, ref").
		Where("workspace_id = ?", strings.TrimSpace(workspaceID)).
		Order("repository_owner, repository_name").Offset(offset).Limit(limit).Find(&publications).Error
	return publications, err
}

func (store *Store) ListContentDefinitions(ctx context.Context, workspaceID, contentID, versionID string, limit int) ([]AutomationContentDefinition, error) {
	if err := store.ensureDB(); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > MaxAutomationDefinitionsPerVersion {
		return nil, fmt.Errorf("automation content definition limit is invalid")
	}
	var definitions []AutomationContentDefinition
	err := store.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ? AND content_version_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(versionID)).
		Order("definition_path").Limit(limit + 1).Find(&definitions).Error
	if err != nil {
		return nil, err
	}
	if len(definitions) > limit {
		return nil, fmt.Errorf("automation content definitions exceed limit")
	}
	return definitions, nil
}

func sameAutomationContentPublication(left, right AutomationContentPublication) bool {
	return left.WorkspaceID == right.WorkspaceID && left.ContentID == right.ContentID &&
		left.ContentVersionID == right.ContentVersionID && left.RepositoryOwner == right.RepositoryOwner &&
		left.RepositoryName == right.RepositoryName && left.Ref == right.Ref &&
		left.ChangedPathsJSON == right.ChangedPathsJSON && left.ActorID == right.ActorID
}
