// Package pkgserver implements the package metadata service backend.
package pkgserver

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"sync"
	"time"

	"hakurei.app/internal/info"
	"hakurei.app/internal/rosa"
)

// for lazy initialisation of serveInfo
var (
	infoPayload struct {
		// Current package count.
		Count int `json:"count"`
		// Hakurei version, set at link time.
		HakureiVersion string `json:"hakurei_version"`
	}
	infoPayloadOnce sync.Once
)

// handleInfo writes constant system information.
func handleInfo(w http.ResponseWriter, _ *http.Request) {
	infoPayloadOnce.Do(func() {
		infoPayload.Count = len(rosa.Collect())
		infoPayload.HakureiVersion = info.Version()
	})
	// TODO(mae): cache entire response if no additional fields are planned
	writeAPIPayload(w, infoPayload)
}

// newStatusHandler returns a [http.HandlerFunc] that offers status files for
// viewing or download, if available.
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		m, ok := index.names[path.Base(r.URL.Path)]
		if !ok || !m.HasReport {
			http.NotFound(w, r)
			return
		}

		contentType := "text/plain; charset=utf-8"
		if disposition {
			contentType = "application/octet-stream"

			// quoting like this is unsound, but okay, because metadata is hardcoded
			contentDisposition := `attachment; filename="`
			contentDisposition += m.Name + "-"
			if m.Version != "" {
				contentDisposition += m.Version + "-"
			}
			contentDisposition += m.ids + `.log"`
			w.Header().Set("Content-Disposition", contentDisposition)
		}
		w.Header().Set("Content-Type", contentType)
		w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
		if err := func() (err error) {
			defer index.handleAccess(&err)()
			_, err = w.Write(m.status)
			return
		}(); err != nil {
			log.Println(err)
			http.Error(
				w, "cannot deliver status, contact maintainers",
				http.StatusInternalServerError,
			)
		}
	}
}

// handleGet writes a slice of metadata with specified order.
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	limit, err := strconv.Atoi(q.Get("limit"))
	if err != nil || limit > 100 || limit < 1 {
		http.Error(
			w, "limit must be an integer between 1 and 100",
			http.StatusBadRequest,
		)
		return
	}
	i, err := strconv.Atoi(q.Get("index"))
	if err != nil || i >= len(index.sorts[0]) || i < 0 {
		http.Error(
			w, "index must be an integer between 0 and "+
				strconv.Itoa(len(index.sorts[0])-1),
			http.StatusBadRequest,
		)
		return
	}
	sort, err := strconv.Atoi(q.Get("sort"))
	if err != nil || sort >= len(index.sorts) || sort < 0 {
		http.Error(
			w, "sort must be an integer between 0 and "+
				strconv.Itoa(sortOrderEnd),
			http.StatusBadRequest,
		)
		return
	}
	values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
	writeAPIPayload(w, &struct {
		Values []*metadata `json:"values"`
	}{values})
}

func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	limit, err := strconv.Atoi(q.Get("limit"))
	if err != nil || limit > 100 || limit < 1 {
		http.Error(
			w, "limit must be an integer between 1 and 100",
			http.StatusBadRequest,
		)
		return
	}
	i, err := strconv.Atoi(q.Get("index"))
	if err != nil || i >= len(index.sorts[0]) || i < 0 {
		http.Error(
			w, "index must be an integer between 0 and "+
				strconv.Itoa(len(index.sorts[0])-1),
			http.StatusBadRequest,
		)
		return
	}
	search, err := url.QueryUnescape(q.Get("search"))
	if len(search) > 100 || err != nil {
		http.Error(
			w, "search must be a string between 0 and 100 characters long",
			http.StatusBadRequest,
		)
		return
	}
	desc := q.Get("desc") == "true"
	n, res, err := index.performSearchQuery(limit, i, search, desc)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	writeAPIPayload(w, &struct {
		Count  int            `json:"count"`
		Values []searchResult `json:"values"`
	}{n, res})
}

// apiVersion is the name of the current API revision, as part of the pattern.
const apiVersion = "v1"

// registerAPI registers API handler functions.
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
	mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
	mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
	mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
	mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
	mux.HandleFunc("GET /status/", index.newStatusHandler(true))
}

// Register arranges for mux to service API requests.
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
	var index packageIndex
	index.search = make(searchCache)
	if err := index.populate(report); err != nil {
		return err
	}
	ticker := time.NewTicker(1 * time.Minute)
	go func() {
		for {
			select {
			case <-ctx.Done():
				ticker.Stop()
				return
			case <-ticker.C:
				index.search.clean()
			}
		}
	}()
	index.registerAPI(mux)
	return nil
}

// writeAPIPayload sets headers common to API responses and encodes payload as
// JSON for the response body.
func writeAPIPayload(w http.ResponseWriter, payload any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "0")

	if err := json.NewEncoder(w).Encode(payload); err != nil {
		log.Println(err)
		http.Error(
			w, "cannot encode payload, contact maintainers",
			http.StatusInternalServerError,
		)
	}
}
