package service

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type roleRecord struct {
	ID          string `gorm:"column:id;primaryKey"`
	Code        string `gorm:"column:code;uniqueIndex"`
	Name        string `gorm:"column:name"`
	Description string `gorm:"column:description"`
	CreatedAt   time.Time
}

func (roleRecord) TableName() string { return "roles" }

type userRecord struct {
	ID            string  `gorm:"column:id;primaryKey"`
	Login         string  `gorm:"column:login"`
	PasswordHash  string  `gorm:"column:password_hash"`
	DisplayName   string  `gorm:"column:display_name"`
	Email         *string `gorm:"column:email"`
	IsActive      bool    `gorm:"column:is_active"`
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Roles         []roleRecord         `gorm:"many2many:user_roles;joinForeignKey:UserID;joinReferences:RoleID"`
	Subscriptions []subscriptionRecord `gorm:"foreignKey:UserID"`
}

func (userRecord) TableName() string { return "users" }

type categoryRecord struct {
	ID          string `gorm:"column:id;primaryKey"`
	Title       string `gorm:"column:title"`
	Slug        string `gorm:"column:slug"`
	Description string `gorm:"column:description"`
	CreatedAt   time.Time
}

func (categoryRecord) TableName() string { return "categories" }

type tagRecord struct {
	ID        string `gorm:"column:id;primaryKey"`
	Title     string `gorm:"column:title"`
	Slug      string `gorm:"column:slug"`
	CreatedAt time.Time
}

func (tagRecord) TableName() string { return "tags" }

type speakerRecord struct {
	ID              string `gorm:"column:id;primaryKey"`
	DisplayName     string `gorm:"column:display_name"`
	RoleDescription string `gorm:"column:role_description"`
	Biography       string `gorm:"column:biography"`
	TopicsCSV       string `gorm:"column:topics_csv"`
	MaterialsCount  int    `gorm:"column:materials_count"`
	Subscribers     int    `gorm:"column:subscribers_count"`
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

func (speakerRecord) TableName() string { return "speakers" }

type contentRecord struct {
	ID               string           `gorm:"column:id;primaryKey"`
	Title            string           `gorm:"column:title"`
	Lead             string           `gorm:"column:lead"`
	Body             string           `gorm:"column:body"`
	ContentType      string           `gorm:"column:content_type"`
	Status           string           `gorm:"column:status"`
	Visibility       string           `gorm:"column:visibility"`
	CategoryID       *string          `gorm:"column:category_id"`
	Category         categoryRecord   `gorm:"foreignKey:CategoryID"`
	AuthorUserID     *string          `gorm:"column:author_user_id"`
	AuthorUser       userRecord       `gorm:"foreignKey:AuthorUserID"`
	AuthorLabel      string           `gorm:"column:author_label"`
	SpeakerID        *string          `gorm:"column:speaker_id"`
	PublishedAt      *time.Time       `gorm:"column:published_at"`
	CreatedAt        time.Time        `gorm:"column:created_at"`
	UpdatedAt        time.Time        `gorm:"column:updated_at"`
	ArchivedAt       *time.Time       `gorm:"column:archived_at"`
	Duration         string           `gorm:"column:duration"`
	ImageTone        string           `gorm:"column:image_tone"`
	ModeratorComment string           `gorm:"column:moderator_comment"`
	ReviewComment    string           `gorm:"column:review_comment"`
	RatingAverage    float64          `gorm:"column:rating_average"`
	RatingCount      int              `gorm:"column:rating_count"`
	Tags             []tagRecord      `gorm:"many2many:content_tags;joinForeignKey:ContentID;joinReferences:TagID"`
	MediaFile        *mediaFileRecord `gorm:"foreignKey:ContentID"`
	Views            int64            `gorm:"column:views_count;->"`
}

func (contentRecord) TableName() string { return "content_items" }

type mediaFileRecord struct {
	ID               string    `gorm:"column:id;primaryKey"`
	ContentID        *string   `gorm:"column:content_id"`
	UploadedByUserID *string   `gorm:"column:uploaded_by_user_id"`
	OriginalName     string    `gorm:"column:original_name"`
	MimeType         string    `gorm:"column:mime_type"`
	SizeBytes        int64     `gorm:"column:size_bytes"`
	StorageKey       string    `gorm:"column:storage_key"`
	PublicURL        string    `gorm:"column:public_url"`
	Checksum         string    `gorm:"column:checksum"`
	CreatedAt        time.Time `gorm:"column:created_at"`
}

func (mediaFileRecord) TableName() string { return "media_files" }

type fileBlobRecord struct {
	ID        string    `gorm:"column:id;primaryKey"`
	Name      string    `gorm:"column:name"`
	MimeType  string    `gorm:"column:mime_type"`
	SizeBytes int64     `gorm:"column:size_bytes"`
	Data      []byte    `gorm:"column:data"`
	CreatedAt time.Time `gorm:"column:created_at"`
}

func (fileBlobRecord) TableName() string { return "stored_file_blobs" }

type subscriptionRecord struct {
	ID               string         `gorm:"column:id;primaryKey"`
	UserID           string         `gorm:"column:user_id"`
	SubscriptionType string         `gorm:"column:subscription_type"`
	CategoryID       *string        `gorm:"column:category_id"`
	Category         categoryRecord `gorm:"foreignKey:CategoryID"`
	TagID            *string        `gorm:"column:tag_id"`
	Tag              tagRecord      `gorm:"foreignKey:TagID"`
	SpeakerID        *string        `gorm:"column:speaker_id"`
	Speaker          speakerRecord  `gorm:"foreignKey:SpeakerID"`
	CreatedAt        time.Time      `gorm:"column:created_at"`
}

func (subscriptionRecord) TableName() string { return "subscriptions" }

type commentRecord struct {
	ID        string     `gorm:"column:id;primaryKey"`
	ContentID string     `gorm:"column:content_id"`
	UserID    string     `gorm:"column:user_id"`
	User      userRecord `gorm:"foreignKey:UserID"`
	Body      string     `gorm:"column:body"`
	Status    string     `gorm:"column:status"`
	CreatedAt time.Time  `gorm:"column:created_at"`
	UpdatedAt time.Time  `gorm:"column:updated_at"`
}

func (commentRecord) TableName() string { return "comments" }

type notificationRecord struct {
	ID        string     `gorm:"column:id;primaryKey"`
	UserID    string     `gorm:"column:user_id"`
	Title     string     `gorm:"column:title"`
	Body      string     `gorm:"column:body"`
	IsRead    bool       `gorm:"column:is_read"`
	ContentID *string    `gorm:"column:content_id"`
	CreatedAt time.Time  `gorm:"column:created_at"`
	ReadAt    *time.Time `gorm:"column:read_at"`
}

func (notificationRecord) TableName() string { return "notifications" }

type actionLogRecord struct {
	ID          string     `gorm:"column:id;primaryKey"`
	ActorUserID *string    `gorm:"column:actor_user_id"`
	ActorUser   userRecord `gorm:"foreignKey:ActorUserID"`
	Action      string     `gorm:"column:action"`
	EntityType  string     `gorm:"column:entity_type"`
	EntityID    *string    `gorm:"column:entity_id"`
	RequestID   string     `gorm:"column:request_id"`
	CreatedAt   time.Time  `gorm:"column:created_at"`
}

func (actionLogRecord) TableName() string { return "action_logs" }

type contentViewRecord struct {
	ID           string    `gorm:"column:id;primaryKey"`
	ContentID    string    `gorm:"column:content_id"`
	UserID       *string   `gorm:"column:user_id"`
	AnonymousKey *string   `gorm:"column:anonymous_key"`
	CreatedAt    time.Time `gorm:"column:created_at"`
}

func (contentViewRecord) TableName() string { return "content_views" }

type gormStore struct {
	db *gorm.DB
}

func newGORMStore(db *gorm.DB) (*gormStore, error) {
	if db.Dialector.Name() == "sqlite" {
		if err := db.AutoMigrate(
			&roleRecord{},
			&userRecord{},
			&categoryRecord{},
			&tagRecord{},
			&speakerRecord{},
			&contentRecord{},
			&mediaFileRecord{},
			&fileBlobRecord{},
			&subscriptionRecord{},
			&commentRecord{},
			&notificationRecord{},
			&actionLogRecord{},
			&contentViewRecord{},
		); err != nil {
			return nil, fmt.Errorf("auto-migrate service tables: %w", err)
		}
	} else if err := ensurePostgresSchema(db); err != nil {
		return nil, err
	}
	store := &gormStore{db: db}
	if err := store.seed(context.Background()); err != nil {
		return nil, err
	}
	return store, nil
}

func ensurePostgresSchema(db *gorm.DB) error {
	statements := []string{
		`ALTER TABLE speakers ADD COLUMN IF NOT EXISTS topics_csv TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE speakers ADD COLUMN IF NOT EXISTS materials_count INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE speakers ADD COLUMN IF NOT EXISTS subscribers_count INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS duration TEXT`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS image_tone TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS moderator_comment TEXT`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS review_comment TEXT`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS rating_average DOUBLE PRECISION NOT NULL DEFAULT 0`,
		`ALTER TABLE content_items ADD COLUMN IF NOT EXISTS rating_count INTEGER NOT NULL DEFAULT 0`,
		`CREATE TABLE IF NOT EXISTS stored_file_blobs (
			id UUID PRIMARY KEY,
			name TEXT NOT NULL,
			mime_type TEXT NOT NULL,
			size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0),
			data BYTEA NOT NULL,
			created_at TIMESTAMPTZ NOT NULL DEFAULT now()
		)`,
	}
	for _, statement := range statements {
		if err := db.Exec(statement).Error; err != nil {
			return fmt.Errorf("ensure postgres schema: %w", err)
		}
	}
	return nil
}

func (s *gormStore) Ping(ctx context.Context) error {
	sqlDB, err := s.db.DB()
	if err != nil {
		return err
	}
	return sqlDB.PingContext(ctx)
}

func (s *gormStore) UserByLogin(ctx context.Context, login string) (UserProfile, bool, error) {
	login = resolveLoginAlias(login)
	var user userRecord
	err := s.db.WithContext(ctx).
		Preload("Roles").
		Preload("Subscriptions.Category").
		Preload("Subscriptions.Tag").
		Preload("Subscriptions.Speaker").
		Where("lower(login) = ?", strings.ToLower(strings.TrimSpace(login))).
		First(&user).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return UserProfile{}, false, nil
	}
	if err != nil {
		return UserProfile{}, false, err
	}
	return mapUserRecord(user), true, nil
}

func (s *gormStore) UserByID(ctx context.Context, id string) (UserProfile, bool, error) {
	var user userRecord
	err := s.db.WithContext(ctx).
		Preload("Roles").
		Preload("Subscriptions.Category").
		Preload("Subscriptions.Tag").
		Preload("Subscriptions.Speaker").
		First(&user, "id = ?", id).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return UserProfile{}, false, nil
	}
	if err != nil {
		return UserProfile{}, false, err
	}
	return mapUserRecord(user), true, nil
}

func (s *gormStore) AddUser(ctx context.Context, login, name, password string) (UserProfile, error) {
	hash, err := hashPassword(password)
	if err != nil {
		return UserProfile{}, err
	}
	user := userRecord{
		ID:           uuid.NewString(),
		Login:        strings.ToLower(strings.TrimSpace(login)),
		PasswordHash: hash,
		DisplayName:  strings.TrimSpace(name),
		IsActive:     true,
	}
	var role roleRecord
	if err := s.db.WithContext(ctx).Where("code = ?", roleCodeToDB(RoleUser)).First(&role).Error; err != nil {
		return UserProfile{}, err
	}
	if err := s.db.WithContext(ctx).Create(&user).Error; err != nil {
		return UserProfile{}, err
	}
	if err := s.db.WithContext(ctx).Model(&user).Association("Roles").Append(&role); err != nil {
		return UserProfile{}, err
	}
	profile, _, err := s.UserByID(ctx, user.ID)
	if err != nil {
		return UserProfile{}, err
	}
	return profile, nil
}

func (s *gormStore) UpdatePassword(ctx context.Context, userID, password string) error {
	hash, err := hashPassword(password)
	if err != nil {
		return err
	}
	return s.db.WithContext(ctx).Model(&userRecord{}).Where("id = ?", userID).Updates(map[string]any{
		"password_hash": hash,
		"updated_at":    time.Now().UTC(),
	}).Error
}

func (s *gormStore) ListUsers(ctx context.Context) ([]UserProfile, error) {
	var users []userRecord
	if err := s.db.WithContext(ctx).
		Preload("Roles").
		Preload("Subscriptions.Category").
		Preload("Subscriptions.Tag").
		Preload("Subscriptions.Speaker").
		Order("created_at asc").
		Find(&users).Error; err != nil {
		return nil, err
	}
	items := make([]UserProfile, 0, len(users))
	for _, user := range users {
		items = append(items, mapUserRecord(user))
	}
	return items, nil
}

func (s *gormStore) ListContent(ctx context.Context, filter ContentFilter) ([]ContentItem, error) {
	query := s.db.WithContext(ctx).
		Model(&contentRecord{}).
		Select("content_items.*, COALESCE(view_counts.views_count, 0) AS views_count").
		Joins("LEFT JOIN (SELECT content_id, COUNT(*) AS views_count FROM content_views GROUP BY content_id) AS view_counts ON view_counts.content_id = content_items.id").
		Preload("Category").
		Preload("AuthorUser").
		Preload("Tags").
		Preload("MediaFile")

	if term := strings.TrimSpace(filter.Term); term != "" {
		like := "%" + strings.ToLower(term) + "%"
		query = query.Joins("LEFT JOIN categories search_categories ON search_categories.id = content_items.category_id").
			Where("lower(content_items.title) LIKE ? OR lower(coalesce(content_items.lead, '')) LIKE ? OR lower(coalesce(content_items.body, '')) LIKE ? OR lower(coalesce(content_items.author_label, '')) LIKE ? OR lower(coalesce(search_categories.title, '')) LIKE ?", like, like, like, like, like)
	}
	if filter.Category != "" && filter.Category != "Все" {
		query = query.Joins("LEFT JOIN categories filter_categories ON filter_categories.id = content_items.category_id").Where("filter_categories.title = ?", filter.Category)
	}
	if filter.ContentType != "" {
		query = query.Where("content_items.content_type = ?", contentTypeToDB(ContentType(filter.ContentType)))
	}
	if filter.Status != "" {
		query = query.Where("content_items.status = ?", contentStatusToDB(ContentStatus(filter.Status)))
	}
	if filter.AuthorID != "" {
		query = query.Where("content_items.author_user_id = ?", filter.AuthorID)
	}
	if filter.Mine {
		query = query.Where("content_items.author_user_id = ?", filter.User.ID)
	}
	if filter.Exclude != "" {
		query = query.Where("content_items.id <> ?", filter.Exclude)
	}
	if filter.OnlyEvents {
		query = query.Where("content_items.content_type = ?", contentTypeToDB(ContentTypeEvent))
	}
	if filter.OnlyMedia {
		query = query.Where("content_items.content_type IN ?", []string{contentTypeToDB(ContentTypeVideo), contentTypeToDB(ContentTypeAudio), contentTypeToDB(ContentTypeGraphic)})
	}
	if filter.SortMode == "newest" {
		query = query.Order("content_items.published_at desc NULLS LAST").Order("content_items.created_at desc")
	} else {
		query = query.Order("views_count desc").Order("content_items.published_at desc NULLS LAST")
	}
	if filter.Limit > 0 {
		query = query.Limit(filter.Limit)
	}
	var records []contentRecord
	if err := query.Find(&records).Error; err != nil {
		return nil, err
	}
	items := make([]ContentItem, 0, len(records))
	for _, record := range records {
		items = append(items, mapContentRecord(record))
	}
	return items, nil
}

func (s *gormStore) ContentByID(ctx context.Context, id string) (ContentItem, bool, error) {
	var exists int64
	if err := s.db.WithContext(ctx).Model(&contentRecord{}).Where("id = ?", id).Count(&exists).Error; err != nil {
		return ContentItem{}, false, err
	}
	if exists == 0 {
		return ContentItem{}, false, nil
	}
	if err := s.db.WithContext(ctx).Create(&contentViewRecord{ID: uuid.NewString(), ContentID: id}).Error; err != nil {
		return ContentItem{}, false, err
	}
	items, err := s.ListContent(ctx, ContentFilter{})
	if err != nil {
		return ContentItem{}, false, err
	}
	for _, item := range items {
		if item.ID == id {
			return item, true, nil
		}
	}
	return ContentItem{}, false, nil
}

func (s *gormStore) AddContent(ctx context.Context, item ContentItem) (ContentItem, error) {
	record, err := s.buildContentRecord(ctx, item, false)
	if err != nil {
		return ContentItem{}, err
	}
	if record.ID == "" {
		record.ID = uuid.NewString()
	}
	if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
		return ContentItem{}, err
	}
	if err := s.replaceContentTags(ctx, record.ID, item.Tags); err != nil {
		return ContentItem{}, err
	}
	if item.MediaURL != "" || item.FileName != "" || item.MimeType != "" || item.FileSize > 0 {
		if err := s.upsertMediaFile(ctx, record.ID, item); err != nil {
			return ContentItem{}, err
		}
	}
	_ = s.appendAuditLog(ctx, item.Author, "создал черновик", item.Title, record.ID)
	reloaded, _, err := s.reloadContent(ctx, record.ID)
	if err != nil {
		return ContentItem{}, err
	}
	return reloaded, nil
}

func (s *gormStore) PatchContent(ctx context.Context, id string, patch ContentItem) (ContentItem, bool, error) {
	var record contentRecord
	if err := s.db.WithContext(ctx).First(&record, "id = ?", id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return ContentItem{}, false, nil
	} else if err != nil {
		return ContentItem{}, false, err
	}
	updates := map[string]any{}
	if patch.Title != "" {
		updates["title"] = patch.Title
	}
	if patch.Lead != "" {
		updates["lead"] = patch.Lead
	}
	if patch.Body != "" {
		updates["body"] = patch.Body
	}
	if patch.Type != "" {
		updates["content_type"] = contentTypeToDB(patch.Type)
	}
	if patch.Category != "" {
		categoryID, err := s.lookupCategoryID(ctx, patch.Category)
		if err != nil {
			return ContentItem{}, false, err
		}
		updates["category_id"] = categoryID
	}
	if patch.Visibility != "" {
		updates["visibility"] = visibilityToDB(patch.Visibility)
	}
	if patch.Status != "" {
		updates["status"] = contentStatusToDB(patch.Status)
	}
	if patch.ModeratorComment != "" {
		updates["moderator_comment"] = patch.ModeratorComment
		updates["review_comment"] = patch.ModeratorComment
	}
	if patch.ReviewComment != "" {
		updates["review_comment"] = patch.ReviewComment
	}
	if patch.Rating > 0 {
		updates["rating_average"] = float64(patch.Rating)
		updates["rating_count"] = gorm.Expr("rating_count + 1")
	}
	if patch.MediaURL != "" || patch.MediaKind != "" || patch.MimeType != "" || patch.FileName != "" || patch.FileSize != 0 {
		if err := s.upsertMediaFile(ctx, id, patch); err != nil {
			return ContentItem{}, false, err
		}
	}
	if len(updates) > 0 {
		updates["updated_at"] = time.Now().UTC()
		if err := s.db.WithContext(ctx).Model(&contentRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
			return ContentItem{}, false, err
		}
	}
	if patch.Tags != nil {
		if err := s.replaceContentTags(ctx, id, patch.Tags); err != nil {
			return ContentItem{}, false, err
		}
	}
	item, ok, err := s.reloadContent(ctx, id)
	if err != nil || !ok {
		return item, ok, err
	}
	if patch.Status != "" || patch.ModeratorComment != "" || patch.ReviewComment != "" {
		_ = s.appendAuditLog(ctx, item.Author, "изменил статус", item.Title, id)
	}
	if patch.Rating > 0 {
		item.MyRating = patch.Rating
	}
	return item, true, nil
}

func (s *gormStore) DeleteContent(ctx context.Context, id string) (bool, error) {
	result := s.db.WithContext(ctx).Delete(&contentRecord{}, "id = ?", id)
	if result.Error != nil {
		return false, result.Error
	}
	return result.RowsAffected > 0, nil
}

func (s *gormStore) ListCategories(ctx context.Context) ([]string, error) {
	var categories []categoryRecord
	if err := s.db.WithContext(ctx).Order("title asc").Find(&categories).Error; err != nil {
		return nil, err
	}
	items := make([]string, 0, len(categories))
	for _, category := range categories {
		items = append(items, category.Title)
	}
	return items, nil
}

func (s *gormStore) ListTags(ctx context.Context) ([]string, error) {
	var tags []tagRecord
	if err := s.db.WithContext(ctx).Order("title asc").Find(&tags).Error; err != nil {
		return nil, err
	}
	items := make([]string, 0, len(tags))
	for _, tag := range tags {
		items = append(items, tag.Title)
	}
	return items, nil
}

func (s *gormStore) ListSpeakers(ctx context.Context) ([]Speaker, error) {
	var speakers []speakerRecord
	if err := s.db.WithContext(ctx).Order("display_name asc").Find(&speakers).Error; err != nil {
		return nil, err
	}
	items := make([]Speaker, 0, len(speakers))
	for _, speaker := range speakers {
		items = append(items, Speaker{ID: speaker.ID, Name: speaker.DisplayName, Role: speaker.RoleDescription, Topics: splitCSV(speaker.TopicsCSV), Materials: speaker.MaterialsCount, Subscribers: speaker.Subscribers})
	}
	return items, nil
}

func (s *gormStore) AddFile(ctx context.Context, file StoredFile) (StoredFile, error) {
	if file.ID == "" {
		file.ID = uuid.NewString()
	}
	record := fileBlobRecord{ID: file.ID, Name: file.Name, MimeType: file.MimeType, SizeBytes: file.Size, Data: file.Data}
	if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
		return StoredFile{}, err
	}
	return file, nil
}

func (s *gormStore) FileByID(ctx context.Context, id string) (StoredFile, bool, error) {
	var record fileBlobRecord
	if err := s.db.WithContext(ctx).First(&record, "id = ?", id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return StoredFile{}, false, nil
	} else if err != nil {
		return StoredFile{}, false, err
	}
	return StoredFile{ID: record.ID, Name: record.Name, MimeType: record.MimeType, Size: record.SizeBytes, Data: record.Data}, true, nil
}

func (s *gormStore) UpsertSubscription(ctx context.Context, userID, target string) ([]string, error) {
	target = strings.TrimSpace(target)
	if target == "" {
		user, ok, err := s.UserByID(ctx, userID)
		return user.Subscriptions, okToErr(ok, err)
	}
	var record subscriptionRecord
	var existing subscriptionRecord
	lookup := strings.ToLower(target)
	if categoryID, err := s.lookupCategoryIDInsensitive(ctx, lookup); err != nil {
		return nil, err
	} else if categoryID != nil {
		record = subscriptionRecord{ID: uuid.NewString(), UserID: userID, SubscriptionType: "category", CategoryID: categoryID}
		existing = subscriptionRecord{UserID: userID, SubscriptionType: "category", CategoryID: categoryID}
	} else if tagID, err := s.lookupTagIDInsensitive(ctx, lookup); err != nil {
		return nil, err
	} else if tagID != nil {
		record = subscriptionRecord{ID: uuid.NewString(), UserID: userID, SubscriptionType: "tag", TagID: tagID}
		existing = subscriptionRecord{UserID: userID, SubscriptionType: "tag", TagID: tagID}
	} else if speakerID, err := s.lookupSpeakerIDInsensitive(ctx, lookup); err != nil {
		return nil, err
	} else if speakerID != nil {
		record = subscriptionRecord{ID: uuid.NewString(), UserID: userID, SubscriptionType: "speaker", SpeakerID: speakerID}
		existing = subscriptionRecord{UserID: userID, SubscriptionType: "speaker", SpeakerID: speakerID}
	}
	if record.ID != "" {
		var count int64
		if err := s.db.WithContext(ctx).Model(&subscriptionRecord{}).Where(&existing).Count(&count).Error; err != nil {
			return nil, err
		}
		if count == 0 {
			if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
				return nil, err
			}
		}
	}
	user, ok, err := s.UserByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, nil
	}
	return user.Subscriptions, nil
}

func (s *gormStore) ListNotifications(ctx context.Context) ([]NotificationItem, error) {
	var records []notificationRecord
	if err := s.db.WithContext(ctx).Order("created_at desc").Find(&records).Error; err != nil {
		return nil, err
	}
	items := make([]NotificationItem, 0, len(records))
	for _, record := range records {
		items = append(items, NotificationItem{ID: record.ID, Title: record.Title, Description: record.Body, Read: record.IsRead, CreatedAt: record.CreatedAt.UTC().Format("2006-01-02 15:04")})
	}
	return items, nil
}

func (s *gormStore) MarkNotificationRead(ctx context.Context, id string) (*NotificationItem, error) {
	now := time.Now().UTC()
	if err := s.db.WithContext(ctx).Model(&notificationRecord{}).Where("id = ?", id).Updates(map[string]any{"is_read": true, "read_at": &now}).Error; err != nil {
		return nil, err
	}
	var record notificationRecord
	if err := s.db.WithContext(ctx).First(&record, "id = ?", id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	item := NotificationItem{ID: record.ID, Title: record.Title, Description: record.Body, Read: record.IsRead, CreatedAt: record.CreatedAt.UTC().Format("2006-01-02 15:04")}
	return &item, nil
}

func (s *gormStore) ListComments(ctx context.Context, contentID string) ([]CommentItem, error) {
	var records []commentRecord
	if err := s.db.WithContext(ctx).Preload("User").Where("content_id = ?", contentID).Order("created_at desc").Find(&records).Error; err != nil {
		return nil, err
	}
	items := make([]CommentItem, 0, len(records))
	for _, record := range records {
		items = append(items, CommentItem{ID: record.ID, ContentID: record.ContentID, Author: record.User.DisplayName, Text: record.Body, CreatedAt: record.CreatedAt.UTC().Format("2006-01-02 15:04")})
	}
	return items, nil
}

func (s *gormStore) AddComment(ctx context.Context, contentID string, user UserProfile, text string) (CommentItem, error) {
	record := commentRecord{ID: uuid.NewString(), ContentID: contentID, UserID: user.ID, Body: text, Status: "visible"}
	if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
		return CommentItem{}, err
	}
	return CommentItem{ID: record.ID, ContentID: contentID, Author: user.Name, Text: text, CreatedAt: time.Now().UTC().Format("2006-01-02 15:04")}, nil
}

func (s *gormStore) CountComments(ctx context.Context) (int, error) {
	var count int64
	if err := s.db.WithContext(ctx).Model(&commentRecord{}).Count(&count).Error; err != nil {
		return 0, err
	}
	return int(count), nil
}

func (s *gormStore) ListAudit(ctx context.Context) ([]AuditItem, error) {
	var records []actionLogRecord
	if err := s.db.WithContext(ctx).Preload("ActorUser").Order("created_at desc").Find(&records).Error; err != nil {
		return nil, err
	}
	items := make([]AuditItem, 0, len(records))
	for _, record := range records {
		target := ""
		if record.EntityID != nil {
			if item, ok, err := s.reloadContent(ctx, *record.EntityID); err == nil && ok {
				target = item.Title
			}
		}
		if target == "" {
			target = record.EntityType
		}
		actor := record.ActorUser.DisplayName
		if actor == "" {
			actor = "Система"
		}
		items = append(items, AuditItem{ID: record.ID, Actor: actor, Action: record.Action, Target: target, CreatedAt: record.CreatedAt.UTC().Format("2006-01-02 15:04")})
	}
	return items, nil
}

func (s *gormStore) reloadContent(ctx context.Context, id string) (ContentItem, bool, error) {
	items, err := s.ListContent(ctx, ContentFilter{})
	if err != nil {
		return ContentItem{}, false, err
	}
	for _, item := range items {
		if item.ID == id {
			return item, true, nil
		}
	}
	return ContentItem{}, false, nil
}

func (s *gormStore) buildContentRecord(ctx context.Context, item ContentItem, preserveID bool) (contentRecord, error) {
	categoryID, err := s.lookupCategoryID(ctx, item.Category)
	if err != nil {
		return contentRecord{}, err
	}
	authorID, err := s.lookupUserIDByName(ctx, item.Author)
	if err != nil {
		return contentRecord{}, err
	}
	record := contentRecord{
		Title:            item.Title,
		Lead:             item.Lead,
		Body:             item.Body,
		ContentType:      contentTypeToDB(item.Type),
		Status:           contentStatusToDB(item.Status),
		Visibility:       visibilityToDB(item.Visibility),
		CategoryID:       categoryID,
		AuthorUserID:     authorID,
		AuthorLabel:      item.Author,
		Duration:         item.Duration,
		ImageTone:        item.ImageTone,
		ModeratorComment: item.ModeratorComment,
		ReviewComment:    item.ReviewComment,
	}
	if preserveID {
		record.ID = item.ID
	}
	if item.PublishedAt != "" {
		ts := mustParseDate(item.PublishedAt)
		record.PublishedAt = &ts
	}
	if record.Status == "" {
		record.Status = contentStatusToDB(ContentStatusDraft)
	}
	if record.Visibility == "" {
		record.Visibility = visibilityToDB(VisibilityPublic)
	}
	return record, nil
}

func (s *gormStore) replaceContentTags(ctx context.Context, contentID string, tags []string) error {
	if err := s.db.WithContext(ctx).Where("content_id = ?", contentID).Delete(&struct {
		ContentID string `gorm:"column:content_id"`
		TagID     string `gorm:"column:tag_id"`
	}{}).Error; err != nil {
		return err
	}
	for _, title := range tags {
		tagID, err := s.lookupTagID(ctx, title)
		if err != nil {
			return err
		}
		if tagID == nil {
			continue
		}
		if err := s.db.WithContext(ctx).Table("content_tags").Create(map[string]any{"content_id": contentID, "tag_id": *tagID}).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s *gormStore) upsertMediaFile(ctx context.Context, contentID string, item ContentItem) error {
	if item.FileName == "" && item.MediaURL == "" && item.MimeType == "" && item.FileSize == 0 {
		return nil
	}
	var existing mediaFileRecord
	err := s.db.WithContext(ctx).Where("content_id = ?", contentID).First(&existing).Error
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	record := mediaFileRecord{
		ID:           existing.ID,
		ContentID:    &contentID,
		OriginalName: firstNonEmpty(item.FileName, existing.OriginalName),
		MimeType:     firstNonEmpty(item.MimeType, existing.MimeType),
		SizeBytes:    firstNonZeroInt64(item.FileSize, existing.SizeBytes),
		StorageKey:   firstNonEmpty(item.MediaURL, existing.StorageKey),
		PublicURL:    firstNonEmpty(item.MediaURL, existing.PublicURL),
	}
	if record.ID == "" {
		record.ID = uuid.NewString()
		return s.db.WithContext(ctx).Create(&record).Error
	}
	return s.db.WithContext(ctx).Model(&mediaFileRecord{}).Where("id = ?", record.ID).Updates(record).Error
}

func (s *gormStore) appendAuditLog(ctx context.Context, actorName, action, title, contentID string) error {
	actorID, err := s.lookupUserIDByName(ctx, actorName)
	if err != nil {
		return err
	}
	entityID := contentID
	return s.db.WithContext(ctx).Create(&actionLogRecord{ID: uuid.NewString(), ActorUserID: actorID, Action: action, EntityType: title, EntityID: &entityID}).Error
}

func (s *gormStore) lookupCategoryID(ctx context.Context, title string) (*string, error) {
	var record categoryRecord
	if err := s.db.WithContext(ctx).Where("title = ?", title).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) lookupCategoryIDInsensitive(ctx context.Context, title string) (*string, error) {
	var record categoryRecord
	if err := s.db.WithContext(ctx).Where("lower(title) = ?", title).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) lookupTagID(ctx context.Context, title string) (*string, error) {
	var record tagRecord
	if err := s.db.WithContext(ctx).Where("title = ?", title).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) lookupTagIDInsensitive(ctx context.Context, title string) (*string, error) {
	var record tagRecord
	if err := s.db.WithContext(ctx).Where("lower(title) = ?", title).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) lookupSpeakerIDInsensitive(ctx context.Context, title string) (*string, error) {
	var record speakerRecord
	if err := s.db.WithContext(ctx).Where("lower(display_name) = ?", title).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) lookupUserIDByName(ctx context.Context, name string) (*string, error) {
	var record userRecord
	if err := s.db.WithContext(ctx).Where("display_name = ?", name).First(&record).Error; errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	return &record.ID, nil
}

func (s *gormStore) seed(ctx context.Context) error {
	if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		for _, role := range []roleRecord{
			{ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", Code: roleCodeToDB(RoleAdministrator), Name: "Администратор", Description: "Расширенное управление системой, пользователями, ролями и настройками"},
			{ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2", Code: roleCodeToDB(RoleEditor), Name: "Редактор", Description: "Создание и редактирование материалов, участие в модерации"},
			{ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa3", Code: roleCodeToDB(RoleManager), Name: "Менеджер", Description: "Публикация, управление контентом и подписками"},
			{ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa4", Code: roleCodeToDB(RoleUser), Name: "Пользователь", Description: "Просмотр материалов, комментарии и подписки"},
		} {
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "code"}}, DoUpdates: clause.AssignmentColumns([]string{"name", "description"})}).Create(&role).Error; err != nil {
				return err
			}
		}
		for _, category := range seedCategories {
			record := categoryRecord{ID: uuid.NewSHA1(uuid.Nil, []byte("category:"+category)).String(), Title: category, Slug: slugify(category)}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"title", "slug"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		for _, tag := range seedTags {
			record := tagRecord{ID: uuid.NewSHA1(uuid.Nil, []byte("tag:"+tag)).String(), Title: tag, Slug: slugify(tag)}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"title", "slug"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		for _, speaker := range seedSpeakers {
			record := speakerRecord{ID: speaker.ID, DisplayName: speaker.Name, RoleDescription: speaker.Role, TopicsCSV: strings.Join(speaker.Topics, ","), MaterialsCount: speaker.Materials, Subscribers: speaker.Subscribers}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"display_name", "role_description", "topics_csv", "materials_count", "subscribers_count"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		var storedRoles []roleRecord
		if err := tx.Where("code IN ?", []string{roleCodeToDB(RoleAdministrator), roleCodeToDB(RoleEditor), roleCodeToDB(RoleManager), roleCodeToDB(RoleUser)}).Find(&storedRoles).Error; err != nil {
			return err
		}
		roleIDs := map[RoleCode]string{}
		for _, role := range storedRoles {
			roleIDs[roleCodeFromDB(role.Code)] = role.ID
		}
		for _, user := range seedUsers {
			email := user.Email
			hash, err := hashPassword(user.Password)
			if err != nil {
				return err
			}
			record := userRecord{ID: user.ID, Login: user.Login, PasswordHash: hash, DisplayName: user.Name, Email: &email, IsActive: true}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"login", "password_hash", "display_name", "email", "is_active"})}).Create(&record).Error; err != nil {
				return err
			}
			for _, role := range user.Roles {
				join := map[string]any{"user_id": user.ID, "role_id": roleIDs[role]}
				if err := tx.Table("user_roles").Clauses(clause.OnConflict{DoNothing: true}).Create(join).Error; err != nil {
					return err
				}
			}
		}
		for _, item := range seedContentItems {
			categoryID := uuid.NewSHA1(uuid.Nil, []byte("category:"+item.Category)).String()
			authorID, _ := userIDByLogin(item.AuthorLogin)
			publishedAt := mustParseDate(item.PublishedAt)
			record := contentRecord{ID: item.ID, Title: item.Title, Lead: item.Lead, Body: item.Body, ContentType: contentTypeToDB(item.Type), Status: contentStatusToDB(item.Status), Visibility: visibilityToDB(item.Visibility), CategoryID: &categoryID, AuthorUserID: authorID, AuthorLabel: seedUserName(item.AuthorLogin), PublishedAt: &publishedAt, Duration: item.Duration, ImageTone: item.ImageTone, ModeratorComment: item.ModeratorComment, ReviewComment: item.ReviewComment}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"title", "lead", "body", "content_type", "status", "visibility", "category_id", "author_user_id", "author_label", "published_at", "duration", "image_tone", "moderator_comment", "review_comment"})}).Create(&record).Error; err != nil {
				return err
			}
			for _, tag := range item.Tags {
				join := map[string]any{"content_id": item.ID, "tag_id": uuid.NewSHA1(uuid.Nil, []byte("tag:"+tag)).String()}
				if err := tx.Table("content_tags").Clauses(clause.OnConflict{DoNothing: true}).Create(join).Error; err != nil {
					return err
				}
			}
			var existingViews int64
			if err := tx.Model(&contentViewRecord{}).Where("content_id = ?", item.ID).Count(&existingViews).Error; err != nil {
				return err
			}
			for idx := existingViews; idx < int64(item.Views); idx++ {
				if err := tx.Create(&contentViewRecord{ID: uuid.NewString(), ContentID: item.ID, CreatedAt: publishedAt.Add(time.Duration(idx) * time.Second)}).Error; err != nil {
					return err
				}
			}
		}
		for _, notification := range seedNotifications {
			userID, _ := userIDByLogin(notification.UserLogin)
			record := notificationRecord{ID: notification.ID, UserID: derefString(userID), Title: notification.Title, Body: notification.Description, IsRead: notification.Read, CreatedAt: mustParseMinute(notification.CreatedAt)}
			if notification.Read {
				readAt := record.CreatedAt
				record.ReadAt = &readAt
			}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"user_id", "title", "body", "is_read", "created_at", "read_at"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		for _, comment := range seedComments {
			userID, _ := userIDByLogin(comment.AuthorLogin)
			record := commentRecord{ID: comment.ID, ContentID: comment.ContentID, UserID: derefString(userID), Body: comment.Text, Status: "visible", CreatedAt: mustParseMinute(comment.CreatedAt), UpdatedAt: mustParseMinute(comment.CreatedAt)}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"content_id", "user_id", "body", "status", "created_at", "updated_at"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		for _, audit := range seedAuditTrail {
			userID, _ := userIDByLogin(audit.ActorLogin)
			entityID := lookupSeedContentIDByTitle(audit.Target)
			record := actionLogRecord{ID: audit.ID, ActorUserID: userID, Action: audit.Action, EntityType: audit.Target, EntityID: entityID, CreatedAt: mustParseMinute(audit.CreatedAt)}
			if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"actor_user_id", "action", "entity_type", "entity_id", "created_at"})}).Create(&record).Error; err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		return err
	}
	for _, user := range seedUsers {
		for _, target := range user.Subscriptions {
			if _, err := s.UpsertSubscription(ctx, user.ID, target); err != nil {
				return err
			}
		}
	}
	return nil
}

func mapUserRecord(user userRecord) UserProfile {
	roles := make([]RoleCode, 0, len(user.Roles))
	for _, role := range user.Roles {
		roles = append(roles, roleCodeFromDB(role.Code))
	}
	subscriptions := make([]string, 0, len(user.Subscriptions))
	for _, subscription := range user.Subscriptions {
		switch subscription.SubscriptionType {
		case "category":
			subscriptions = append(subscriptions, subscription.Category.Title)
		case "tag":
			subscriptions = append(subscriptions, subscription.Tag.Title)
		case "speaker":
			subscriptions = append(subscriptions, subscription.Speaker.DisplayName)
		}
	}
	sort.Strings(subscriptions)
	return UserProfile{ID: user.ID, Name: user.DisplayName, Login: user.Login, PasswordHash: user.PasswordHash, Roles: roles, Subscriptions: subscriptions}
}

func mapContentRecord(record contentRecord) ContentItem {
	tags := make([]string, 0, len(record.Tags))
	for _, tag := range record.Tags {
		tags = append(tags, tag.Title)
	}
	sort.Strings(tags)
	item := ContentItem{
		ID:               record.ID,
		Title:            record.Title,
		Lead:             record.Lead,
		Body:             record.Body,
		Type:             contentTypeFromDB(record.ContentType),
		Category:         record.Category.Title,
		Tags:             tags,
		Author:           firstNonEmpty(record.AuthorUser.DisplayName, record.AuthorLabel),
		Duration:         record.Duration,
		Visibility:       visibilityFromDB(record.Visibility),
		Status:           contentStatusFromDB(record.Status),
		Views:            int(record.Views),
		ImageTone:        record.ImageTone,
		ModeratorComment: record.ModeratorComment,
		ReviewComment:    record.ReviewComment,
		RatingAverage:    record.RatingAverage,
		RatingCount:      record.RatingCount,
	}
	if record.PublishedAt != nil {
		item.PublishedAt = record.PublishedAt.UTC().Format("2006-01-02")
	}
	if record.MediaFile != nil {
		item.MediaURL = firstNonEmpty(record.MediaFile.PublicURL, record.MediaFile.StorageKey)
		item.MediaKind = inferMediaKind(record.MediaFile.MimeType, record.MediaFile.OriginalName)
		item.MimeType = record.MediaFile.MimeType
		item.FileName = record.MediaFile.OriginalName
		item.FileSize = record.MediaFile.SizeBytes
	}
	return item
}

func resolveLoginAlias(login string) string {
	login = strings.ToLower(strings.TrimSpace(login))
	aliases := map[string]string{"admin@dstu.ru": "demo_admin", "editor@dstu.ru": "demo_editor", "moderator@dstu.ru": "demo_moderator", "manager@dstu.ru": "demo_moderator", "user@dstu.ru": "demo_user"}
	if alias, ok := aliases[login]; ok {
		return alias
	}
	return login
}

func roleCodeToDB(role RoleCode) string {
	switch role {
	case RoleAdministrator:
		return "administrator"
	case RoleEditor:
		return "editor"
	case RoleManager:
		return "manager"
	default:
		return "user"
	}
}

func roleCodeFromDB(role string) RoleCode {
	switch role {
	case "administrator":
		return RoleAdministrator
	case "editor":
		return RoleEditor
	case "manager":
		return RoleManager
	default:
		return RoleUser
	}
}

func contentTypeToDB(contentType ContentType) string {
	if contentType == ContentTypeEvent {
		return "event"
	}
	return string(contentType)
}

func contentTypeFromDB(contentType string) ContentType { return ContentType(contentType) }

func contentStatusToDB(status ContentStatus) string {
	switch status {
	case ContentStatusDraft:
		return "draft"
	case ContentStatusModeration:
		return "moderation"
	case ContentStatusReview:
		return "review"
	case ContentStatusPublished:
		return "published"
	case ContentStatusReturned:
		return "returned"
	case ContentStatusArchived:
		return "archived"
	default:
		return strings.ToLower(strings.TrimSpace(string(status)))
	}
}

func contentStatusFromDB(status string) ContentStatus {
	return normalizeStatus(status)
}

func visibilityToDB(visibility Visibility) string {
	switch visibility {
	case VisibilityPublic:
		return "public"
	case VisibilityAuthenticated:
		return "authenticated"
	case VisibilityRole:
		return "role_restricted"
	default:
		return "public"
	}
}

func visibilityFromDB(value string) Visibility {
	switch value {
	case "public":
		return VisibilityPublic
	case "authenticated":
		return VisibilityAuthenticated
	case "role_restricted":
		return VisibilityRole
	default:
		return VisibilityPublic
	}
}

func splitCSV(value string) []string {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	parts := strings.Split(value, ",")
	items := make([]string, 0, len(parts))
	for _, part := range parts {
		if trimmed := strings.TrimSpace(part); trimmed != "" {
			items = append(items, trimmed)
		}
	}
	return items
}

func slugify(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	replacer := strings.NewReplacer(" ", "-", ",", "", ".", "", "«", "", "»", "")
	return replacer.Replace(value)
}

func firstNonZeroInt64(value, fallback int64) int64 {
	if value == 0 {
		return fallback
	}
	return value
}

func userIDByLogin(login string) (*string, bool) {
	for _, user := range seedUsers {
		if user.Login == login {
			return &user.ID, true
		}
	}
	return nil, false
}

func seedUserName(login string) string {
	for _, user := range seedUsers {
		if user.Login == login {
			return user.Name
		}
	}
	return login
}

func lookupSeedContentIDByTitle(title string) *string {
	for _, item := range seedContentItems {
		if item.Title == title {
			return &item.ID
		}
	}
	return nil
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func okToErr(ok bool, err error) error {
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	return nil
}
