package subsonic

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"strings"
)

// APIVersion is the Subsonic REST version we advertise. 1.16.1 is the last
// "classic" level; OpenSubsonic extensions are surfaced via the openSubsonic
// flag and the type/serverVersion fields.
const (
	APIVersion = "1.16.1"
	ServerType = "minstrel"
	XMLNS      = "http://subsonic.org/restapi"
)

// ServerVersion identifies this build. Overwritten by cmd/minstrel at startup.
var ServerVersion = "dev"

// Envelope holds the attributes present on every Subsonic response. Concrete
// response types embed it and add endpoint-specific fields with json+xml tags.
type Envelope struct {
	XMLName       xml.Name `json:"-" xml:"subsonic-response"`
	XMLNS         string   `json:"-" xml:"xmlns,attr"`
	Status        string   `json:"status" xml:"status,attr"`
	Version       string   `json:"version" xml:"version,attr"`
	Type          string   `json:"type" xml:"type,attr"`
	ServerVersion string   `json:"serverVersion" xml:"serverVersion,attr"`
	OpenSubsonic  bool     `json:"openSubsonic" xml:"openSubsonic,attr"`
	Error         *Error   `json:"error,omitempty" xml:"error,omitempty"`
}

// Error is the body of a failed response.
type Error struct {
	XMLName xml.Name `json:"-" xml:"error"`
	Code    int      `json:"code" xml:"code,attr"`
	Message string   `json:"message" xml:"message,attr"`
}

// NewEnvelope returns a populated header for the given status ("ok"|"failed").
func NewEnvelope(status string) Envelope {
	return Envelope{
		XMLNS:         XMLNS,
		Status:        status,
		Version:       APIVersion,
		Type:          ServerType,
		ServerVersion: ServerVersion,
		OpenSubsonic:  true,
	}
}

// Write renders body in the format requested by the ?f= query parameter.
// body must embed Envelope. Unknown formats fall back to JSON.
func Write(w http.ResponseWriter, r *http.Request, body any) {
	switch strings.ToLower(r.URL.Query().Get("f")) {
	case "xml":
		writeXML(w, body)
	case "jsonp":
		writeJSONP(w, body, r.URL.Query().Get("callback"))
	default:
		writeJSON(w, body)
	}
}

// WriteFail emits an envelope-only failed response with the given error code.
func WriteFail(w http.ResponseWriter, r *http.Request, code int, message string) {
	env := NewEnvelope("failed")
	env.Error = &Error{Code: code, Message: message}
	Write(w, r, env)
}

func writeJSON(w http.ResponseWriter, body any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]any{"subsonic-response": body})
}

func writeJSONP(w http.ResponseWriter, body any, callback string) {
	if callback == "" {
		// Per spec the client must supply callback; fall back to plain JSON
		// rather than producing invalid JavaScript.
		writeJSON(w, body)
		return
	}
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	_, _ = fmt.Fprintf(w, "%s(", callback)
	_ = json.NewEncoder(w).Encode(map[string]any{"subsonic-response": body})
	_, _ = io.WriteString(w, ")")
}

func writeXML(w http.ResponseWriter, body any) {
	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
	_, _ = io.WriteString(w, xml.Header)
	_ = xml.NewEncoder(w).Encode(body)
}
