// Package opentdb fills the casino's trivia bank from the Open Trivia Database.
//
// The questions are *prefetched* into a local table, not fetched per question,
// and that is a deliberate call rather than an optimisation. A trivia ladder
// asks a question every fifteen seconds with money on the clock: a per-question
// fetch would put somebody else's latency, rate limit and downtime inside a
// timed round the player is being scored against. Pull the bank in the
// background, and a round becomes a local read that either works or doesn't.
//
// OpenTDB allows one request every five seconds per IP and caps a batch at 50,
// so the refill is a slow, polite drip, run in the background and never in the
// path of anything a player is waiting for.
package opentdb

import (
	"context"
	"encoding/json"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"pete/internal/games/trivia"
	"pete/internal/safehttp"
)

// endpoint is the API. It is the only host this package ever talks to, and it
// goes through safehttp like every other outbound fetch in Pete.
const endpoint = "https://opentdb.com/api.php"

// Batch is the most OpenTDB will hand over in one request.
const Batch = 50

// Politeness is the gap the API asks for between requests. Going faster earns a
// response_code 5 and nothing else.
const Politeness = 6 * time.Second

// fetchTimeout bounds a single request. The refill runs in the background, so a
// slow answer costs nothing but its own goroutine — but it must still end.
const fetchTimeout = 20 * time.Second

// maxBody caps what we will read from the API, hostile or merely broken.
const maxBody = 1 << 20

// apiResponse is OpenTDB's envelope. ResponseCode is the part that matters:
// zero is the only one that means "here are your questions".
type apiResponse struct {
	ResponseCode int `json:"response_code"`
	Results      []struct {
		Category  string   `json:"category"`
		Type      string   `json:"type"`
		Question  string   `json:"question"`
		Correct   string   `json:"correct_answer"`
		Incorrect []string `json:"incorrect_answers"`
	} `json:"results"`
}

// responseErr turns a non-zero code into something a log line can explain.
func responseErr(code int) error {
	switch code {
	case 1:
		return fmt.Errorf("opentdb: no results for that query")
	case 2:
		return fmt.Errorf("opentdb: the query was invalid")
	case 3, 4:
		return fmt.Errorf("opentdb: session token expired or exhausted")
	case 5:
		return fmt.Errorf("opentdb: rate limited — slow down")
	default:
		return fmt.Errorf("opentdb: response code %d", code)
	}
}

// Client fetches questions.
type Client struct {
	http *http.Client
}

func New() *Client {
	return &Client{http: safehttp.NewClient(fetchTimeout)}
}

// Fetch pulls up to n multiple-choice questions of one difficulty.
//
// Only "multiple" questions are asked for: the ladder is four buttons, and a
// true/false question on the same felt would be a coin flip dressed up as a
// question — and a coin flip the player is being paid a difficulty multiple for.
func (c *Client) Fetch(ctx context.Context, difficulty string, n int) ([]trivia.Question, error) {
	if n <= 0 || n > Batch {
		n = Batch
	}
	q := url.Values{
		"amount":     {fmt.Sprint(n)},
		"difficulty": {difficulty},
		"type":       {"multiple"},
	}
	raw := endpoint + "?" + q.Encode()
	if err := safehttp.ValidateURL(raw); err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, raw, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "pete-games/1.0 (+https://games.parodia.dev)")

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("opentdb: http %d", resp.StatusCode)
	}

	body, err := io.ReadAll(safehttp.LimitedBody(resp.Body, maxBody))
	if err != nil {
		return nil, err
	}

	var out apiResponse
	if err := json.Unmarshal(body, &out); err != nil {
		return nil, fmt.Errorf("opentdb: %w", err)
	}
	if out.ResponseCode != 0 {
		return nil, responseErr(out.ResponseCode)
	}

	qs := make([]trivia.Question, 0, len(out.Results))
	for _, r := range out.Results {
		// The API hands back HTML entities ("Who wrote &quot;Dune&quot;?"), which
		// would otherwise be drawn literally onto a button.
		text := clean(r.Question)
		correct := clean(r.Correct)
		if text == "" || correct == "" || len(r.Incorrect) != 3 {
			continue // a malformed question is one we simply don't take
		}

		// Correct: 0 here is a convention, not a tell. The engine reshuffles every
		// question against the game's own seed as it builds the ladder, so where
		// the right answer sits in the bank never reaches a player.
		answers := make([]string, 0, 4)
		answers = append(answers, correct)
		dupe := false
		for _, w := range r.Incorrect {
			a := clean(w)
			// A wrong answer that reads the same as the right one — usually two
			// spellings that collapse once the entities are decoded — is a question
			// with two identical buttons on it, and the shuffle can only call one of
			// them correct. A player who clicked the right words and was told they
			// were wrong has lost the whole ladder to our typography. Drop it.
			if a == "" || a == correct {
				dupe = true
				break
			}
			answers = append(answers, a)
		}
		if dupe || len(answers) != 4 {
			continue
		}
		qs = append(qs, trivia.Question{
			Category: clean(r.Category),
			Text:     text,
			Answers:  answers,
			Correct:  0,
		})
	}
	return qs, nil
}

// clean turns an API string into something you can put on a button: entities
// decoded, whitespace tidied.
func clean(s string) string {
	return strings.TrimSpace(html.UnescapeString(s))
}
