package api

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

const (
	castTokenMinExpSeconds = 60
	castTokenMaxExpSeconds = 86400 // 24h
	castTokenDefaultExp    = 21600 // 6h
)

type castTokenRequest struct {
	TrackID    string `json:"trackId"`
	ExpSeconds int    `json:"expSeconds,omitempty"`
}

type castTokenResponse struct {
	Token string `json:"token"`
	Exp   int64  `json:"exp"`
	URL   string `json:"url"`
	// MIME and Title let the client build proper DIDL-Lite metadata for
	// SetAVTransportURI. Sonos rejects empty DIDL with vendor error 1023;
	// passing back the track's MIME + title here lets the client populate
	// `<res protocolInfo>` and `<dc:title>` without a follow-up round trip.
	MIME  string `json:"mime"`
	Title string `json:"title"`
}

// mimeForFormat returns the audio MIME type for a cast (Sonos/UPnP) URL.
// Wraps the canonical audioContentType lookup in media.go and overrides
// the unknown-format fallback to audio/mpeg, because Sonos rejects
// DIDL-Lite with protocolInfo=application/octet-stream (the browser
// fallback) -- most Sonos firmware probes the URL anyway and recovers
// from a small MIME mismatch.
func mimeForFormat(format string) string {
	mime := audioContentType(format)
	if mime == "application/octet-stream" {
		return "audio/mpeg"
	}
	return mime
}

// extForFormat maps the tracks.file_format column to a path-safe file
// extension. Sonos firmware gates duration probing on the URL path
// extension (Content-Type header alone is insufficient) -- without a
// recognizable extension, Sonos reports TrackDuration=0 and seeks
// trigger auto-advance because every position past 0 looks past-the-
// end. Defaults to "mp3" for unknown formats. See task #610.
func extForFormat(format string) string {
	switch strings.ToLower(strings.TrimSpace(format)) {
	case "mp3", "mpeg":
		return "mp3"
	case "flac":
		return "flac"
	case "aac":
		return "aac"
	case "m4a", "mp4":
		return "m4a"
	case "ogg", "vorbis":
		return "ogg"
	case "opus":
		return "opus"
	case "wav", "wave":
		return "wav"
	default:
		return "mp3"
	}
}

// handleCastStreamToken issues a short-lived HMAC stream token for the
// given trackId. Authenticated via the standard session cookie / bearer.
//
// The returned URL is a fully-formed stream URL (token + exp embedded
// as query params) that the client passes verbatim to a UPnP / Sonos
// device's AVTransport.SetAVTransportURI call — those devices cannot
// carry the user's session, so the signed query string is the only way
// they can fetch the bytes.
//
// expSeconds is clamped to [60, 86400]; default 21600 (6h) — long enough
// to play through any typical track without re-minting mid-playback.
//
// Part of the output-picker UPnP slice. See
// docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md.
func (h *handlers) handleCastStreamToken(w http.ResponseWriter, r *http.Request) {
	if _, ok := requireUser(w, r); !ok {
		return
	}
	var req castTokenRequest
	if !decodeBody(w, r, &req) {
		return
	}
	trackUUID, ok := parseUUID(req.TrackID)
	if !ok || !trackUUID.Valid {
		writeErr(w, apierror.BadRequest("invalid_track_id", "trackId must be a UUID"))
		return
	}
	// Track lookup for the DIDL-Lite metadata the client builds for
	// SetAVTransportURI. A missing track is a 404 — there's nothing to
	// cast in that case.
	track, err := dbq.New(h.pool).GetTrackByID(r.Context(), trackUUID)
	if err != nil {
		writeErr(w, apierror.NotFound("track"))
		return
	}
	expSec := clampExpSeconds(req.ExpSeconds)
	exp := time.Now().Add(time.Duration(expSec) * time.Second).Unix()
	token := SignStreamToken(h.streamSecret, req.TrackID, exp)

	// Behind a TLS-terminating reverse proxy, r.TLS is nil even though
	// the public-facing URL is https://. UPnP devices (Sonos especially)
	// reject SetAVTransportURI with error 714 (IllegalMimeType) when
	// they hit an http:// URL that immediately 301s to https:// — the
	// MIME probe fails to find an audio body. Honor X-Forwarded-Proto +
	// X-Forwarded-Host first so the URL we hand to the speaker reaches
	// it on the same scheme/host the client used. Falls back to
	// r.TLS-based detection for direct (no-proxy) deployments.
	scheme := "http"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS != nil {
		scheme = "https"
	}
	host := r.Host
	if h := r.Header.Get("X-Forwarded-Host"); h != "" {
		host = h
	}
	// Include the file extension in the path so Sonos's URL probe sees a
	// recognizable audio file. Without it, Sonos reports TrackDuration=0
	// and seeks past 0s land "after the end" -> early track-skip.
	url := scheme + "://" + host + streamURLWithExt(trackUUID, extForFormat(track.FileFormat)) +
		"?token=" + token + "&exp=" + strconv.FormatInt(exp, 10)

	writeJSON(w, http.StatusOK, castTokenResponse{
		Token: token,
		Exp:   exp,
		URL:   url,
		MIME:  mimeForFormat(track.FileFormat),
		Title: track.Title,
	})
}

// clampExpSeconds applies the [60, 86400] window with a 6h default for
// non-positive inputs. Extracted so it doesn't bloat the handler's
// detekt-equivalent line count and so the test can exercise edges directly.
func clampExpSeconds(v int) int {
	if v <= 0 {
		return castTokenDefaultExp
	}
	if v < castTokenMinExpSeconds {
		return castTokenMinExpSeconds
	}
	if v > castTokenMaxExpSeconds {
		return castTokenMaxExpSeconds
	}
	return v
}
