package soundtouchweb

import (
	"context"
	"io/fs"
	"net/http"
	"time"

	"github.com/gesellix/bose-soundtouch/pkg/discovery"
	"github.com/go-chi/chi/v5"
)

// MountWeb registers the portable soundtouch-player surface on r: the embedded
// assets (/app/static/*), the control API (/api/control/*), and the SPA
// (/app/*). It is self-contained under /api/control and /app, registering
// nothing outside those subtrees (no /, no /health), so it can be mounted into
// another router (e.g. soundtouch-service) additively. The discovery service
// is reused by POST /api/control/discover for on-demand sweeps; pass the same
// instance used for startup discovery so settings (interface, timeout) stay
// consistent.
func (app *WebApp) MountWeb(r chi.Router, discoveryService *discovery.UnifiedDiscoveryService) {
	// Embedded assets, served under the /app subtree so nothing contends with a
	// host router's own /static (e.g. the Stockholm bridge's root catch-all).
	subFS, _ := fs.Sub(StaticFS, "static")
	r.Get("/app/static/*", http.StripPrefix("/app/static", http.FileServer(http.FS(subFS))).ServeHTTP)

	// Player / control API. Per #451 this is the post-merge canonical shape:
	// device-scoped actions nest under devices/{id}/, so every direct child of
	// /api/control is a literal namespace (version, ws, discover, devices,
	// providers) — no static-vs-param sibling, so routing never depends on
	// chi's static-over-param precedence.
	r.Route("/api/control", func(r chi.Router) {
		r.Get("/version", app.HandleAPIVersion)

		// App-wide event stream: device list, discovery status, per-device
		// status updates. The read/event half of the control surface (the
		// per-device socket lives at devices/{id}/ws).
		r.Get("/ws", app.HandleWebSocket)

		r.Post("/discover", func(w http.ResponseWriter, r *http.Request) {
			app.HandleAPIDiscover(w, r)

			// Trigger discovery
			//nolint:contextcheck // Context is created within goroutine
			go func() {
				ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
				defer cancel()

				app.BroadcastDiscoveryStatus("starting", app.DeviceCount())

				app.DiscoverDevices(ctx, discoveryService)

				app.BroadcastDiscoveryStatus("completed", app.DeviceCount())
				app.BroadcastDeviceList()
			}()
		})

		// One /devices subrouter holds both the list and the /{id} subtree (the
		// issue #285 single-subrouter lesson). Under /{id} every child is a
		// literal action.
		r.Route("/devices", func(r chi.Router) {
			r.Get("/", app.HandleAPIDevices)

			r.Route("/{id}", func(r chi.Router) {
				r.Get("/", app.HandleAPIDevice)
				r.Delete("/", app.HandleDeleteDevice)
				r.Post("/key/{key}", app.HandleDeviceKey)
				r.Post("/volume/{volume}", app.HandleDirectVolumeControl)
				r.Post("/power", app.HandleDevicePower)
				r.Get("/power-status", app.HandleDevicePowerStatus)
				r.Get("/recents", app.HandleDeviceRecents)
				// Low-level "play this ContentItem" primitive (not a provider).
				r.Post("/play", app.HandleDevicePlay)
				// Generic key / preset / source / bass actions.
				r.Get("/action/{action}", app.HandleAPIControl)
				r.Post("/action/{action}", app.HandleAPIControl)
				r.Get("/ws", app.HandleDeviceWebSocket)

				r.Route("/library", func(r chi.Router) {
					r.Get("/servers", app.HandleDeviceLibraryServers)
					r.Post("/servers", app.HandleAddLibraryServer)
					r.Delete("/servers/{account}", app.HandleRemoveLibraryServer)
					r.Get("/browse", app.HandleLibraryBrowse)
					r.Post("/play", app.HandlePlayLibrary)
				})

				r.Route("/zone", func(r chi.Router) {
					r.Get("/", app.HandleGetZone)
					r.Post("/add/{slaveId}", app.HandleZoneAdd)
					r.Post("/remove/{slaveId}", app.HandleZoneRemove)
					r.Post("/dissolve", app.HandleZoneDissolve)
					r.Post("/leave", app.HandleZoneLeave)
				})

				// Play a result from a content provider on this device.
				// Browsable providers (tunein, radiobrowser) take a catalog item;
				// input providers (url, tts) take the raw input.
				r.Route("/providers", func(r chi.Router) {
					r.Post("/tunein/play", app.HandlePlayTuneIn)
					r.Post("/radiobrowser/play", app.HandlePlayRadioBrowser)
					r.Post("/url/play", app.HandlePlayURL)
					// Proxied to the AfterTouch service's /api/setup/tts/speak.
					r.Post("/tts/play", app.HandleAPISpeakText)
				})
			})
		})

		// Provider browse / search (global, not device-scoped). Only browsable
		// providers (a catalog you search/navigate) appear here; input
		// providers (url, tts) exist solely as a device play above.
		r.Route("/providers", func(r chi.Router) {
			r.Route("/tunein", func(r chi.Router) {
				r.Get("/search", app.HandleTuneInSearch)
				r.Get("/search/next", app.HandleTuneInSearchNext)
				r.Get("/navigate", app.HandleTuneInNavigate)
				r.Get("/navigate/*", app.HandleTuneInNavigate)
			})

			r.Route("/radiobrowser", func(r chi.Router) {
				r.Get("/search", app.HandleRadioBrowserSearch)
			})

			r.Route("/library", func(r chi.Router) {
				r.Get("/servers", app.HandleDiscoverLibraryServers)
			})
		})
	})

	// SPA — served under /app/*. The client navigates via component state
	// rather than the URL, so these entries only ensure deep links and
	// refreshes return index.html instead of 404. Per #451 this keeps the
	// whole web UI under one /app subtree, so folding -web into -service is an
	// additive mount.
	r.Get("/app", app.serveIndex)
	r.Get("/app/devices", app.serveIndex)
	r.Get("/app/device/*", app.serveIndex)
	r.Get("/app/tunein", app.serveIndex)
	r.Get("/app/radiobrowser", app.serveIndex)
	r.Get("/app/playurl", app.serveIndex)
	r.Get("/app/tts", app.serveIndex)
	r.Get("/app/library", app.serveIndex)
}

// Mount is the standalone soundtouch-player entry point: the portable web surface
// (see MountWeb) plus the standalone-only liveness endpoint and a / redirect
// into the app. soundtouch-service does not call this — it mounts MountWeb and
// keeps its own / (landing page) and /health.
func (app *WebApp) Mount(r chi.Router, discoveryService *discovery.UnifiedDiscoveryService) {
	app.MountWeb(r, discoveryService)

	// Health / liveness (standalone only).
	r.Get("/health", app.HandleHealth)

	// Standalone convenience: the bare root jumps into the app.
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/app", http.StatusFound)
	})
}

func (app *WebApp) serveIndex(w http.ResponseWriter, _ *http.Request) {
	data, _ := StaticFS.ReadFile("static/index.html")

	w.Header().Set("Content-Type", "text/html")
	_, _ = w.Write(data)
}
