package skills

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

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

// CreateSkillVersion records one exact immutable ContentVersion projection.
// Replaying the same Skill/content-version pair is idempotent; no broader
// history or workspace scan is performed.
func (s *Store) CreateSkillVersion(ctx context.Context, version SkillVersion) (SkillVersion, error) {
	if err := s.ensureDB(); err != nil {
		return SkillVersion{}, err
	}
	version.ID = strings.TrimSpace(version.ID)
	version.WorkspaceID = strings.TrimSpace(version.WorkspaceID)
	version.SkillID = strings.TrimSpace(version.SkillID)
	version.ContentID = strings.TrimSpace(version.ContentID)
	version.ContentVersionID = strings.TrimSpace(version.ContentVersionID)
	version.CreatedBy = strings.TrimSpace(version.CreatedBy)
	if version.ID == "" || version.WorkspaceID == "" || version.SkillID == "" || version.ContentID == "" ||
		version.ContentVersionID == "" || version.CreatedBy == "" || version.FileCount < 0 || version.SizeBytes < 0 || version.SkillCount < 0 || version.CreatedAt.IsZero() {
		return SkillVersion{}, fmt.Errorf("skill version projection is invalid")
	}
	result := s.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&version)
	if result.Error != nil {
		return SkillVersion{}, fmt.Errorf("create skill version: %w", result.Error)
	}
	var stored SkillVersion
	if err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND skill_id = ? AND content_id = ? AND content_version_id = ?", version.WorkspaceID, version.SkillID, version.ContentID, version.ContentVersionID).
		First(&stored).Error; err != nil {
		return SkillVersion{}, fmt.Errorf("load skill version: %w", err)
	}
	if stored.ContentID != version.ContentID || stored.RootDigest != version.RootDigest || stored.FileCount != version.FileCount ||
		stored.SizeBytes != version.SizeBytes || stored.SkillCount != version.SkillCount {
		return SkillVersion{}, ErrSkillVersionConflict
	}
	return stored, nil
}

// GetSkillVersion loads one exact immutable Skill version.
func (s *Store) GetSkillVersion(ctx context.Context, workspaceID, skillID, versionID string) (SkillVersion, error) {
	if err := s.ensureDB(); err != nil {
		return SkillVersion{}, err
	}
	var version SkillVersion
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND skill_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(skillID), strings.TrimSpace(versionID)).
		First(&version).Error
	return version, err
}

// FindSkillVersionByContentVersion resolves the product projection for one
// exact immutable ContentVersion. It is used by mutation retries to determine
// whether the product transaction already committed.
func (s *Store) FindSkillVersionByContentVersion(ctx context.Context, workspaceID, skillID, contentID, contentVersionID string) (SkillVersion, bool, error) {
	if err := s.ensureDB(); err != nil {
		return SkillVersion{}, false, err
	}
	var version SkillVersion
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND skill_id = ? AND content_id = ? AND content_version_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(skillID), strings.TrimSpace(contentID), strings.TrimSpace(contentVersionID)).
		First(&version).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return SkillVersion{}, false, nil
	}
	return version, err == nil, err
}

// AdvanceSkillContentVersion atomically moves one exact Skill projection from
// its expected immutable version to the newly published version. A concurrent
// mutation cannot silently overwrite the winner.
func (s *Store) AdvanceSkillContentVersion(ctx context.Context, workspaceID, skillID, expectedContentID, expectedVersionID, contentID, versionID string, skillCount int, updatedBy string, updatedAt time.Time) (Skill, error) {
	if err := s.ensureDB(); err != nil {
		return Skill{}, err
	}
	result := s.db.WithContext(ctx).Model(&Skill{}).
		Where("workspace_id = ? AND id = ? AND deleted_at IS NULL AND content_id = ? AND active_content_version_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(skillID), strings.TrimSpace(expectedContentID), strings.TrimSpace(expectedVersionID)).
		Updates(map[string]any{
			"content_id": strings.TrimSpace(contentID), "active_content_version_id": strings.TrimSpace(versionID),
			"skill_count": skillCount, "updated_by": strings.TrimSpace(updatedBy), "updated_at": updatedAt,
		})
	if result.Error != nil {
		return Skill{}, fmt.Errorf("advance skill content version: %w", result.Error)
	}
	if result.RowsAffected != 1 {
		return Skill{}, ErrSkillVersionConflict
	}
	return s.GetSkill(ctx, workspaceID, skillID)
}

// ListSkillVersions returns at most one lookahead row beyond a bounded page for
// one explicit Skill. The lookahead lets callers compute HasMore without a
// second query or a count over the full history.
func (s *Store) ListSkillVersions(ctx context.Context, workspaceID, skillID string, limit, offset int) ([]SkillVersion, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > 100 || offset < 0 || offset > 10_000 {
		return nil, fmt.Errorf("skill version page is out of bounds")
	}
	var versions []SkillVersion
	if err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND skill_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(skillID)).
		Order("created_at DESC").Order("id DESC").Limit(limit + 1).Offset(offset).Find(&versions).Error; err != nil {
		return nil, fmt.Errorf("list skill versions: %w", err)
	}
	return versions, nil
}
