// Package hnblogs provides access to the blogs.hn dataset.
//
// The dataset is vendored into this repository as blogs.json and compiled into
// the package with go:embed, so nothing here touches the network at runtime and
// the results are stable for a given version of the module. Refresh the
// vendored copy with "make update-data" and commit the result.
package hnblogs

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"math/rand"
	"sync"
)

// BlogsURL is the upstream source of blogs.json. It is not fetched at runtime;
// it documents where "make update-data" pulls the vendored copy from.
const BlogsURL = "https://raw.githubusercontent.com/surprisetalk/blogs.hn/main/blogs.json"

// blogsJSON is the vendored dataset, refreshed by "make update-data".
//
//go:embed blogs.json
var blogsJSON []byte

var (
	blogs     []Blog
	loadError error
	once      sync.Once
)

// Blog represents a single blog entry.
type Blog struct {
	URL   string `json:"url"`
	Title string `json:"title"`
	About string `json:"about"`
	Now   string `json:"now"`
	Feed  string `json:"feed"`
	Desc  string `json:"desc"`
}

// FetchBlogs returns the embedded list of blogs, decoding it on first call and
// memoizing the result for subsequent calls.
//
// Despite the name it performs no I/O: the data is compiled into the binary, so
// the only error it can return is a malformed embedded blogs.json, which would
// mean the committed dataset is broken.
func FetchBlogs() ([]Blog, error) {
	once.Do(func() {
		var decoded []Blog
		if err := json.Unmarshal(blogsJSON, &decoded); err != nil {
			loadError = fmt.Errorf("failed to decode embedded blogs JSON: %w", err)
			return
		}

		blogs = decoded
	})

	return blogs, loadError
}

// GetBlogs returns the memoized list of blogs.
func GetBlogs() ([]Blog, error) {
	return FetchBlogs()
}

// RandomBlog returns a random blog from the list of blogs.
func RandomBlog() (Blog, error) {
	blogs, err := GetBlogs()
	if err != nil {
		return Blog{}, err
	}

	return blogs[rand.Intn(len(blogs))], nil
}

// RandomBlogs returns n random blogs from the list of blogs.
func RandomBlogs(n int) ([]Blog, error) {
	blogs, err := GetBlogs()
	if err != nil {
		return nil, err
	}

	if n <= 0 || n > len(blogs) {
		return nil, fmt.Errorf("invalid number of blogs requested")
	}

	selected := make([]Blog, n)
	for i := range selected {
		selected[i] = blogs[rand.Intn(len(blogs))]
	}

	return selected, nil
}

// NthBlog returns the nth blog from the list of blogs.
func NthBlog(n int) (Blog, error) {
	blogs, err := GetBlogs()
	if err != nil {
		return Blog{}, err
	}

	if n < 0 || n >= len(blogs) {
		return Blog{}, fmt.Errorf("index out of range")
	}

	return blogs[n], nil
}
