// mc-status — a tiny self-hosted Minecraft Server List Ping → JSON service.
//
// It wraps github.com/mcstatus-io/mcutil (the same library that powers
// api.mcstatus.io) and re-emits the result in mcstatus.io's *v2* JSON shape, so
// the landing's ServerCard.astro can talk to it with zero changes — just point
// `statusApi` at this service instead of the public API.
//
// Hardening notes:
//   - The address in the request path is IGNORED. We only ever ping the single
//     allow-listed MC_STATUS_TARGET. This is deliberate: a path-controlled
//     pinger would be an open SSRF relay. The path segment is kept only so the
//     URL stays drop-in compatible with api.mcstatus.io/v2/status/java/<addr>.
//   - Results are cached in-memory for MC_STATUS_CACHE_TTL so a busy landing
//     page can't hammer the Minecraft server with a ping per visitor.
package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/mcstatus-io/mcutil/v4/status"
)

// --- mcstatus.io v2 response shape (only the fields ServerCard reads) ---

type v2Version struct {
	NameRaw   string `json:"name_raw"`
	NameClean string `json:"name_clean"`
	NameHTML  string `json:"name_html"`
	Protocol  int64  `json:"protocol"`
}

type v2Player struct {
	UUID      string `json:"uuid"`
	NameRaw   string `json:"name_raw"`
	NameClean string `json:"name_clean"`
	NameHTML  string `json:"name_html"`
}

type v2Players struct {
	Online int64      `json:"online"`
	Max    int64      `json:"max"`
	List   []v2Player `json:"list"`
}

type v2MOTD struct {
	Raw   string `json:"raw"`
	Clean string `json:"clean"`
	HTML  string `json:"html"`
}

type v2Response struct {
	Online  bool       `json:"online"`
	Host    string     `json:"host"`
	Port    uint16     `json:"port"`
	Version *v2Version `json:"version,omitempty"`
	Players *v2Players `json:"players,omitempty"`
	MOTD    *v2MOTD    `json:"motd,omitempty"`
	Icon    *string    `json:"icon"`
}

// --- tiny TTL cache (single target → single entry) ---

type cache struct {
	mu      sync.Mutex
	payload []byte
	expires time.Time
}

func deref[T any](p *T) (v T) {
	if p != nil {
		v = *p
	}
	return
}

func main() {
	target := envOr("MC_STATUS_TARGET", "minecraft:25565")
	listen := envOr("MC_STATUS_LISTEN", ":8080")
	ttl, err := time.ParseDuration(envOr("MC_STATUS_CACHE_TTL", "30s"))
	if err != nil {
		log.Fatalf("invalid MC_STATUS_CACHE_TTL: %v", err)
	}

	host, port := splitHostPort(target)
	c := &cache{}

	mux := http.NewServeMux()

	// Drop-in path: /v2/status/java/<addr> — <addr> is ignored (see file header).
	mux.HandleFunc("/v2/status/java/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Access-Control-Allow-Origin", "*")

		c.mu.Lock()
		fresh := c.payload != nil && time.Now().Before(c.expires)
		if fresh {
			payload := c.payload
			c.mu.Unlock()
			w.Write(payload)
			return
		}
		c.mu.Unlock()

		payload := buildPayload(host, port)

		c.mu.Lock()
		c.payload = payload
		c.expires = time.Now().Add(ttl)
		c.mu.Unlock()

		w.Write(payload)
	})

	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	log.Printf("mc-status listening on %s, target=%s:%d, ttl=%s", listen, host, port, ttl)
	log.Fatal(http.ListenAndServe(listen, mux))
}

// buildPayload pings the target and marshals the v2 response. On any ping
// failure it returns a minimal {"online":false} so the frontend can show an
// offline state without erroring.
func buildPayload(host string, port uint16) []byte {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	resp, err := status.Modern(ctx, host, port)
	if err != nil {
		b, _ := json.Marshal(v2Response{Online: false, Host: host, Port: port})
		return b
	}

	out := v2Response{
		Online: true,
		Host:   host,
		Port:   port,
		Icon:   resp.Favicon,
		Version: &v2Version{
			NameRaw:   resp.Version.Name.Raw,
			NameClean: resp.Version.Name.Clean,
			NameHTML:  resp.Version.Name.HTML,
			Protocol:  resp.Version.Protocol,
		},
		MOTD: &v2MOTD{
			Raw:   resp.MOTD.Raw,
			Clean: resp.MOTD.Clean,
			HTML:  resp.MOTD.HTML,
		},
		Players: &v2Players{
			Online: deref(resp.Players.Online),
			Max:    deref(resp.Players.Max),
			List:   make([]v2Player, 0, len(resp.Players.Sample)),
		},
	}

	for _, p := range resp.Players.Sample {
		out.Players.List = append(out.Players.List, v2Player{
			UUID:      p.ID,
			NameRaw:   p.Name.Raw,
			NameClean: p.Name.Clean,
			NameHTML:  p.Name.HTML,
		})
	}

	b, _ := json.Marshal(out)
	return b
}

func envOr(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

func splitHostPort(target string) (string, uint16) {
	host, portStr, found := strings.Cut(target, ":")
	if !found {
		return target, 25565
	}
	p, err := strconv.ParseUint(portStr, 10, 16)
	if err != nil {
		return host, 25565
	}
	return host, uint16(p)
}
