package cursors

import (
	"encoding/base64"
	"errors"
	"fmt"
	"strings"
)

const (
	// cursorSeparator is a unique boundary that separates the distinct properties of
	// a cursor so we can concatenate them all into a single base64-encoded string.
	// It's important that this separator have a value that is not in the domain
	// of the individual cursor properties (i.e. it shouldn't be an app name if we're
	// sorting on app name). Tilde ("~")was selected for being URL-safe while also not being
	// in the base64 alphabet used to encode the parts.
	cursorSeparator = "~"
)

// Cursor encodes and decodes a Postgres cursor
type Cursor struct {
	SortKey       string // The name of the key that the cursor sorts on
	LastSortValue string // The last value of the sort key returned from the previous query
	LastRowID     string // The last value of the row ID from the previous query, for tiebreakers
	SortOrder     string // The sort order of the previous query (ASC or DESC)
	PrimaryKey    string // primary key, for tiebreakers
}

// String returns a string representation of a cursor for the next set of results
// of the {sortKey}~{lastSortValue}~{lastRowID}~{order} where each of the parts
// is base64 encoded using a URL safe alphabet
func (c Cursor) String() string {
	if c.LastSortValue == "" || c.LastRowID == "" {
		return ""
	}
	sortKey := base64.URLEncoding.EncodeToString([]byte(c.SortKey))
	sortOrder := base64.URLEncoding.EncodeToString([]byte(c.SortOrder))
	lastSortValue := base64.URLEncoding.EncodeToString([]byte(c.LastSortValue))
	lastRowID := base64.URLEncoding.EncodeToString([]byte(c.LastRowID))
	primaryKey := base64.URLEncoding.EncodeToString([]byte(c.PrimaryKey))
	return strings.Join([]string{sortKey, sortOrder, lastSortValue, lastRowID, primaryKey}, cursorSeparator)
}

// Parse accepts a string and attempts to deserialize it into a Cursor
func Parse(c string) (Cursor, error) {
	if c == "" {
		return Cursor{}, errors.New("Cannot parse empty cursor")
	}
	parts := strings.Split(c, cursorSeparator)
	if len(parts) != 5 {
		return Cursor{}, fmt.Errorf("Invalid cursor, could not parse: %s", c)
	}
	decodedParts := make([]string, len(parts))
	for i, part := range parts {
		decodedPart, err := base64.URLEncoding.DecodeString(part)
		if err != nil {
			return Cursor{}, err
		}
		decodedParts[i] = string(decodedPart)
	}
	return Cursor{
		SortKey:       decodedParts[0],
		SortOrder:     decodedParts[1],
		LastSortValue: decodedParts[2],
		LastRowID:     decodedParts[3],
		PrimaryKey:    decodedParts[4],
	}, nil
}
