package approval

import (
	"context"
	"fmt"
	"strings"
)

const maxUnprojectedPageSize = 100

// UnprojectedOptions selects one stable id-ordered page whose durable
// presentation watermark is behind the authoritative Approval version.
type UnprojectedOptions struct {
	SubjectType string
	AfterID     string
	Limit       int
}

type UnprojectedPage struct {
	Instances  []Instance
	NextCursor string
	HasMore    bool
}

// ListUnprojected returns every unprojected version, regardless
// of age or terminal state. MarkProjected advances the durable
// watermark only after idempotent presentation succeeds.
func (store *Store) ListUnprojected(
	ctx context.Context,
	options UnprojectedOptions,
) (UnprojectedPage, error) {
	if err := store.Validate(); err != nil {
		return UnprojectedPage{}, err
	}
	subjectType := strings.TrimSpace(options.SubjectType)
	afterID := strings.TrimSpace(options.AfterID)
	if subjectType == "" {
		return UnprojectedPage{}, fmt.Errorf("unprojected Approval subject type is required")
	}
	limit := options.Limit
	if limit <= 0 {
		limit = 25
	}
	if limit > maxUnprojectedPageSize {
		limit = maxUnprojectedPageSize
	}
	rows, err := store.db.Query(ctx, instanceSelectSQL+`
 WHERE subject_type=$1
   AND projected_version < version
   AND id>$2
 ORDER BY id ASC
 LIMIT $3
`, subjectType, afterID, limit+1)
	if err != nil {
		return UnprojectedPage{}, fmt.Errorf("list unprojected Approval subjects: %w", err)
	}
	defer rows.Close()

	instances := make([]Instance, 0, limit+1)
	for rows.Next() {
		instance, err := scanInstance(rows)
		if err != nil {
			return UnprojectedPage{}, fmt.Errorf("scan unprojected Approval subject: %w", err)
		}
		instances = append(instances, instance)
	}
	if err := rows.Err(); err != nil {
		return UnprojectedPage{}, fmt.Errorf("iterate unprojected Approval subjects: %w", err)
	}
	hasMore := len(instances) > limit
	if hasMore {
		instances = instances[:limit]
	}
	nextCursor := afterID
	if len(instances) != 0 {
		nextCursor = instances[len(instances)-1].ID
	}
	return UnprojectedPage{Instances: instances, NextCursor: nextCursor, HasMore: hasMore}, nil
}

// MarkProjected records that the visible projection has converged
// through version. A concurrent newer Approval version remains unprojected.
func (store *Store) MarkProjected(
	ctx context.Context,
	workspaceID, approvalID string,
	version int64,
) error {
	if err := store.Validate(); err != nil {
		return err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	approvalID = strings.TrimSpace(approvalID)
	if workspaceID == "" || approvalID == "" || version <= 0 {
		return fmt.Errorf("Approval projection identity and version are required")
	}
	result, err := store.db.Exec(ctx, `
UPDATE approval_instances
SET projected_version=GREATEST(projected_version,$3)
WHERE workspace_id=$1 AND id=$2 AND version >= $3
`, workspaceID, approvalID, version)
	if err != nil {
		return fmt.Errorf("advance Approval projected version: %w", err)
	}
	if result.RowsAffected() != 1 {
		instance, loadErr := store.Load(ctx, workspaceID, approvalID)
		if loadErr != nil {
			return loadErr
		}
		if instance.Version < version {
			return fmt.Errorf("Approval projected version is not authoritative")
		}
		if instance.ProjectedVersion >= version {
			return nil
		}
		return fmt.Errorf("Approval projected version did not advance")
	}
	return nil
}
