reads.gno

package memba_appstore_v1

import (
	"strings"

	"gno.land/p/nt/ufmt/v0"
)

const (
	DefaultPageSize = 20  // default JSON window
	MaxPageLimit    = 100 // hard cap on any JSON read window (read-DoS guard)
)

func clampLimit(limit int) int {
	if limit <= 0 {
		return DefaultPageSize
	}
	if limit > MaxPageLimit {
		return MaxPageLimit
	}
	return limit
}

// jsonEscape escapes a string for embedding in a JSON string literal — every
// listing text field is attacker-supplied, so this is the only thing between a
// malicious name/tagline and a broken/injected JSON payload on the read path.
func jsonEscape(s string) string {
	var sb strings.Builder
	for _, r := range s {
		switch r {
		case '"':
			sb.WriteString("\\\"")
		case '\\':
			sb.WriteString("\\\\")
		case '\n':
			sb.WriteString("\\n")
		case '\r':
			sb.WriteString("\\r")
		case '\t':
			sb.WriteString("\\t")
		default:
			if r < 0x20 {
				sb.WriteString(ufmt.Sprintf("\\u%04x", r))
			} else {
				sb.WriteString(string(r))
			}
		}
	}
	return sb.String()
}

// listingJSON renders one listing. `full` adds the (large) description — omitted
// from list windows so a bounded page stays small.
func listingJSON(l *Listing, full bool) string {
	descr := ""
	if full {
		descr = ufmt.Sprintf(`,"descr":"%s"`, jsonEscape(l.Descr))
	}
	return ufmt.Sprintf(
		`{"id":%d,"pkgPath":"%s","name":"%s","tagline":"%s","category":"%s","iconCID":"%s","appURL":"%s","publisher":"%s","status":"%s","flagCount":%d,"createdAt":%d%s}`,
		l.Id,
		jsonEscape(l.PkgPath),
		jsonEscape(l.Name),
		jsonEscape(l.Tagline),
		jsonEscape(l.Category),
		jsonEscape(l.IconCID),
		jsonEscape(l.AppURL),
		l.Publisher.String(),
		l.Status,
		l.FlagCount,
		l.CreatedAt,
		descr,
	)
}

// ListLiveJSON returns a bounded JSON array of the visible (live, un-hidden)
// listings: skip `offset`, take up to clamped `limit`. Iteration stops as soon as
// the window is filled, so the scan is bounded by offset+limit regardless of
// catalog size.
func ListLiveJSON(offset, limit int) string {
	if offset < 0 {
		offset = 0
	}
	limit = clampLimit(limit)

	var sb strings.Builder
	sb.WriteString("[")
	skipped := 0
	taken := 0
	first := true
	listings.Iterate("", "", func(_ string, v any) bool {
		l := v.(*Listing)
		if !isVisible(l) {
			return false
		}
		if skipped < offset {
			skipped++
			return false
		}
		if taken >= limit {
			return true // window full — stop
		}
		if !first {
			sb.WriteString(",")
		}
		first = false
		sb.WriteString(listingJSON(l, false))
		taken++
		return false
	})
	sb.WriteString("]")
	return sb.String()
}

// GetListingJSON returns a single listing (any status) with its description, or
// the JSON literal `null` if the package path is not registered.
func GetListingJSON(pkgPath string) string {
	v, ok := listings.Get(pkgPath)
	if !ok {
		return "null"
	}
	return listingJSON(v.(*Listing), true)
}