package roles

import (
	"context"
	"fmt"
	"strings"

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

// RoleVersionContentProjection is the verified Content Store projection for
// one existing immutable RoleVersion. Migration changes only content-location
// metadata; it never creates a RoleVersion or advances the active pointer.
type RoleVersionContentProjection struct {
	WorkspaceID              string
	RoleID                   string
	RoleVersionID            string
	ExpectedContentVersionID string
	ContentID                string
	ContentVersionID         string
	ManifestItems            []RolePackageManifestItem
}

// ProjectRoleVersionContent atomically attaches one active legacy RoleVersion
// and all of its manifest items to a verified Content Store version. Replays
// of the same exact projection are idempotent; mixed or stale projections fail
// closed without partial writes.
func (s *Store) ProjectRoleVersionContent(ctx context.Context, projection RoleVersionContentProjection) error {
	if s == nil || s.db == nil {
		return fmt.Errorf("role store is not configured")
	}
	projection.WorkspaceID = strings.TrimSpace(projection.WorkspaceID)
	projection.RoleID = strings.TrimSpace(projection.RoleID)
	projection.RoleVersionID = strings.TrimSpace(projection.RoleVersionID)
	projection.ExpectedContentVersionID = strings.TrimSpace(projection.ExpectedContentVersionID)
	projection.ContentID = strings.TrimSpace(projection.ContentID)
	projection.ContentVersionID = strings.TrimSpace(projection.ContentVersionID)
	if projection.WorkspaceID == "" || projection.RoleID == "" || projection.RoleVersionID == "" || projection.ContentID == "" || projection.ContentVersionID == "" || len(projection.ManifestItems) == 0 {
		return fmt.Errorf("role content migration exact projection is required")
	}

	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var role Role
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Select("id", "workspace_id", "active_version_id").
			Where("workspace_id = ? AND id = ? AND deleted_at IS NULL", projection.WorkspaceID, projection.RoleID).
			First(&role).Error; err != nil {
			return err
		}
		if strings.TrimSpace(role.ActiveVersionID) != projection.RoleVersionID {
			return gorm.ErrRecordNotFound
		}

		var version RoleVersion
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND role_id = ? AND id = ?", projection.WorkspaceID, projection.RoleID, projection.RoleVersionID).
			First(&version).Error; err != nil {
			return err
		}
		versionAlreadyProjected := strings.TrimSpace(version.ContentID) == projection.ContentID && strings.TrimSpace(version.ContentVersionID) == projection.ContentVersionID
		if !versionAlreadyProjected {
			if strings.TrimSpace(version.ContentVersionID) != projection.ExpectedContentVersionID {
				return gorm.ErrRecordNotFound
			}
			if projection.ExpectedContentVersionID == "" {
				if strings.TrimSpace(version.ContentID) != "" {
					return gorm.ErrRecordNotFound
				}
			} else if strings.TrimSpace(version.ContentID) != projection.ContentID {
				return gorm.ErrRecordNotFound
			}
		}

		var current []RolePackageManifestItem
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND role_id = ? AND role_version_id = ?", projection.WorkspaceID, projection.RoleID, projection.RoleVersionID).
			Order("sort_order ASC, id ASC").Limit(MaxRolePackageManifestItems + 1).Find(&current).Error; err != nil {
			return err
		}
		if len(current) != len(projection.ManifestItems) {
			return fmt.Errorf("role content migration manifest changed")
		}
		byID := make(map[string]RolePackageManifestItem, len(projection.ManifestItems))
		for _, item := range projection.ManifestItems {
			item.ID = strings.TrimSpace(item.ID)
			if item.ID == "" || strings.TrimSpace(item.WorkspaceID) != projection.WorkspaceID || strings.TrimSpace(item.RoleID) != projection.RoleID || strings.TrimSpace(item.RoleVersionID) != projection.RoleVersionID {
				return fmt.Errorf("role content migration manifest identity changed")
			}
			if _, duplicate := byID[item.ID]; duplicate {
				return fmt.Errorf("role content migration manifest contains duplicate item")
			}
			byID[item.ID] = item
		}
		for _, existing := range current {
			item, ok := byID[existing.ID]
			if !ok || item.ItemType != existing.ItemType || item.Name != existing.Name || item.SortOrder != existing.SortOrder || item.SourceResourceID != existing.SourceResourceID {
				return fmt.Errorf("role content migration manifest changed")
			}
			alreadyProjected := sameRoleManifestContentProjection(existing, item)
			if versionAlreadyProjected && !alreadyProjected {
				return fmt.Errorf("role content migration manifest projection is incomplete")
			}
			if !versionAlreadyProjected && strings.TrimSpace(item.ContentID) != "" {
				if alreadyProjected || strings.TrimSpace(existing.ContentID) != "" || strings.TrimSpace(existing.ContentVersionID) != "" || strings.TrimSpace(existing.ContentPath) != "" {
					return fmt.Errorf("role content migration manifest already has a mixed projection")
				}
			}
			if alreadyProjected {
				continue
			}
			result := tx.Model(&RolePackageManifestItem{}).
				Where("workspace_id = ? AND role_id = ? AND role_version_id = ? AND id = ?", projection.WorkspaceID, projection.RoleID, projection.RoleVersionID, existing.ID).
				Updates(map[string]any{
					"content_ref": item.ContentRef, "content_id": item.ContentID, "content_version_id": item.ContentVersionID,
					"content_path": item.ContentPath, "content_hash": item.ContentHash, "content": "",
				})
			if result.Error != nil {
				return result.Error
			}
			if result.RowsAffected != 1 {
				return gorm.ErrRecordNotFound
			}
		}

		if versionAlreadyProjected {
			return nil
		}
		result := tx.Model(&RoleVersion{}).
			Where("workspace_id = ? AND role_id = ? AND id = ? AND content_version_id = ?", projection.WorkspaceID, projection.RoleID, projection.RoleVersionID, projection.ExpectedContentVersionID).
			Updates(map[string]any{"content_id": projection.ContentID, "content_version_id": projection.ContentVersionID})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected != 1 {
			return gorm.ErrRecordNotFound
		}
		return nil
	})
}

func sameRoleManifestContentProjection(left, right RolePackageManifestItem) bool {
	return strings.TrimSpace(left.ContentRef) == strings.TrimSpace(right.ContentRef) &&
		strings.TrimSpace(left.ContentID) == strings.TrimSpace(right.ContentID) &&
		strings.TrimSpace(left.ContentVersionID) == strings.TrimSpace(right.ContentVersionID) &&
		strings.TrimSpace(left.ContentPath) == strings.TrimSpace(right.ContentPath) &&
		strings.EqualFold(strings.TrimSpace(left.ContentHash), strings.TrimSpace(right.ContentHash)) &&
		strings.TrimSpace(left.Content) == ""
}
