// Copyright [2026] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package v1 provides the API for the webserver.
package v1

import (
	"errors"
	"io/fs"
	"net/http"
	"slices"
	"strings"

	"github.com/gorilla/mux"
	"github.com/vearutop/statigz"
	"github.com/vearutop/statigz/brotli"

	"github.com/release-argus/Argus/config/decode"
	"github.com/release-argus/Argus/internal/logx"
	"github.com/release-argus/Argus/util"
	"github.com/release-argus/Argus/util/errfmt"
	apitype "github.com/release-argus/Argus/web/api/types"
	"github.com/release-argus/Argus/web/ui"
)

// SetupRoutesAPI sets up the HTTP API routes.
func (api *API) SetupRoutesAPI() {
	// Create a subrouter for "/api/v1".
	v1Router := api.Router.PathPrefix("/api/v1").Subrouter()

	// Only if VERBOSE or DEBUG.
	if logx.Level() >= 3 {
		// Apply loggerMiddleware to only the "/api/v1" routes.
		v1Router.Use(loggerMiddleware)
	}

	// /config
	// Apply the logging middleware globally.
	//   GET, config.
	v1Router.HandleFunc("/config", api.httpConfig).Methods(http.MethodGet)
	// /status
	//   GET, runtime info.
	v1Router.HandleFunc("/status/runtime", api.httpRuntimeInfo).Methods(http.MethodGet)
	//   GET, build info.
	v1Router.HandleFunc("/version", api.httpVersion).Methods(http.MethodGet)
	//   GET, short-lived token for authenticating the "/ws" WebSocket handshake (only used when basic_auth is enabled).
	v1Router.HandleFunc("/ws-token", api.httpWebSocketToken).Methods(http.MethodGet)
	// /flags
	//   GET, flags.
	v1Router.HandleFunc("/flags", api.httpFlags).Methods(http.MethodGet)
	// /approvals
	//   GET, service order.
	v1Router.HandleFunc("/service/order", api.httpServiceOrderGet).Methods(http.MethodGet)
	//   PUT, service order (disable=order_edit).
	v1Router.HandleFunc("/service/order", api.httpServiceOrderSet).Methods(http.MethodPut)
	//   GET, service summary.
	v1Router.HandleFunc("/service/summary", api.httpServiceSummary).Methods(http.MethodGet)
	//   GET, service actions (webhooks/commands).
	v1Router.HandleFunc("/service/actions", api.httpServiceGetActions).Methods(http.MethodGet)
	//   POST, service actions (disable=service_actions).
	v1Router.HandleFunc("/service/actions", api.httpServiceRunActions).Methods(http.MethodPost)
	//   GET, service - get details on specific service.
	v1Router.HandleFunc("/service/config", api.httpServiceDetail).Methods(http.MethodGet)
	//   GET, service - get details on service defaults.
	v1Router.HandleFunc("/service/defaults", api.httpOtherServiceDetails).Methods(http.MethodGet)
	//   GET, service - refresh unsaved service (disable=[ld]v_refresh_new).
	v1Router.HandleFunc("/latest_version/refresh_uncreated", api.httpLatestVersionRefreshUncreated).Methods(http.MethodGet)
	v1Router.HandleFunc("/deployed_version/refresh_uncreated", api.httpDeployedVersionRefreshUncreated).Methods(http.MethodGet)
	//   GET, service - refresh service (disable=[ld]v_refresh).
	v1Router.HandleFunc("/latest_version/refresh", api.httpLatestVersionRefresh).Methods(http.MethodGet)
	v1Router.HandleFunc("/deployed_version/refresh", api.httpDeployedVersionRefresh).Methods(http.MethodGet)
	//   POST, service - test notify (disable=notify_test).
	v1Router.HandleFunc("/notify/test", api.httpNotifyTest).Methods(http.MethodPost)
	//   PUT, service - update details (disable=service_edit).
	v1Router.HandleFunc("/service/config", api.httpServiceEdit).Methods(http.MethodPut)
	//   PUT, service - new service (disable=service_create).
	v1Router.HandleFunc("/service/new", api.httpServiceEdit).Methods(http.MethodPut)
	//   DELETE, service - delete service (disable=service_delete).
	v1Router.HandleFunc("/service/delete", api.httpServiceDelete).Methods(http.MethodDelete)
	//   GET, service - template strings.
	v1Router.HandleFunc("/template", api.httpTemplateParse).Methods(http.MethodGet)
	// GET, counts for Heimdall.
	v1Router.HandleFunc("/counts", api.httpCounts).Methods(http.MethodGet)

	// Disable specified routes.
	api.DisableRoutes()
}

// DisableRoutes disables HTTP API routes marked as disabled in the config.
func (api *API) DisableRoutes() {
	// Trim suffix to ensure no trailing slash and prevent '//api/v1/...' routes.
	webRoutePrefix := strings.TrimSuffix(api.Config.Settings.WebRoutePrefix(), "/")
	routes := map[string]*struct {
		name         string
		method       string
		otherMethods map[string]func(w http.ResponseWriter, r *http.Request)
		disabled     bool
	}{
		webRoutePrefix + "/api/v1/service/order":                      {name: "order_edit", method: http.MethodPut},
		webRoutePrefix + "/api/v1/service/new":                        {name: "service_create", method: http.MethodPut},
		webRoutePrefix + "/api/v1/service/config":                     {name: "service_update", method: http.MethodPut},
		webRoutePrefix + "/api/v1/service/delete":                     {name: "service_delete", method: http.MethodDelete},
		webRoutePrefix + "/api/v1/notify/test":                        {name: "notify_test", method: http.MethodPost},
		webRoutePrefix + "/api/v1/latest_version/refresh":             {name: "lv_refresh", method: http.MethodGet},
		webRoutePrefix + "/api/v1/deployed_version/refresh":           {name: "dv_refresh", method: http.MethodGet},
		webRoutePrefix + "/api/v1/latest_version/refresh_uncreated":   {name: "lv_refresh_new", method: http.MethodGet},
		webRoutePrefix + "/api/v1/deployed_version/refresh_uncreated": {name: "dv_refresh_new", method: http.MethodGet},
		webRoutePrefix + "/api/v1/service/actions":                    {name: "service_actions", method: http.MethodPost},
	}
	for _, r := range routes {
		r.disabled = slices.Contains(api.Config.Settings.Web.DisabledRoutes, r.name)
	}

	_ = api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
		// Return an error if the route does not define a path.
		routePath, _ := route.GetPathTemplate()

		// Ignore routes not defined above or disabled.
		r := routes[routePath]
		if r == nil || !r.disabled {
			return nil
		}

		handler := route.GetHandler()

		// Set the new handler for the route.
		disabledMethod := r.method
		route.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			disabledMethod := disabledMethod
			if r.Method == disabledMethod {
				failRequest(
					&w,
					errors.New("route disabled"),
					http.StatusNotFound,
				)
				return
			}

			// Call the original handler for other methods.
			handler.(http.HandlerFunc)(w, r) // Cast the handler to http.HandlerFunc before calling it.
		})

		return nil
	})
}

// SetupWebSocket fills the handler for the "/ws" route.
// The route must be outside the basic-auth subrouter because Safari/WebKit
// doesn't forward cached credentials on WebSocket handshakes.
// When Basic Auth is enabled the client instead passes a short-lived token
// as a query parameter.
func (api *API) SetupWebSocket(hub *Hub, wsRoute *mux.Route) {
	wsRoute.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if api.wsTokens != nil && !api.wsTokens.Validate(r.URL.Query().Get("token")) {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		w.Header().Set("Connection", "keep-alive")
		defer r.Body.Close()
		ServeWs(hub, w, r)
	})
}

// SetupRoutesNodeJS sets up the HTTP routes to the Node.js files.
func (api *API) SetupRoutesNodeJS() {
	nodeRoutes := []string{
		"/approvals",
		"/config",
		"/flags",
		"/status",
	}
	// Serve the Node.js files.
	for _, route := range nodeRoutes {
		prefix := strings.TrimRight(api.RoutePrefix, "/") + route
		api.Router.Handle(
			route,
			http.StripPrefix(
				prefix,
				statigz.FileServer(ui.GetFS().(fs.ReadDirFS), brotli.AddEncoding),
			),
		)
	}

	// Favicon override.
	api.SetupRoutesFavicon()

	// Catch-all for JS, CSS, etc...
	api.Router.PathPrefix("/").Handler(
		http.StripPrefix(
			api.RoutePrefix,
			statigz.FileServer(ui.GetFS().(fs.ReadDirFS), brotli.AddEncoding),
		),
	)
}

// SetupRoutesFavicon adds any favicon route overrides.
func (api *API) SetupRoutesFavicon() {
	if api.Config.Settings.Web.Favicon == nil {
		return
	}

	if api.Config.Settings.Web.Favicon.SVG != "" {
		api.Router.HandleFunc("/favicon.svg", func(w http.ResponseWriter, r *http.Request) {
			http.Redirect(
				w, r,
				api.Config.Settings.Web.Favicon.SVG,
				http.StatusPermanentRedirect,
			)
		})
	}
	if api.Config.Settings.Web.Favicon.PNG != "" {
		api.Router.HandleFunc("/apple-touch-icon.png", func(w http.ResponseWriter, r *http.Request) {
			http.Redirect(
				w, r,
				api.Config.Settings.Web.Favicon.PNG,
				http.StatusPermanentRedirect,
			)
		})
	}
}

// httpVersion serves Argus version JSON over HTTP.
func (api *API) httpVersion(w http.ResponseWriter, r *http.Request) {
	logFrom := logx.LogFrom{Primary: "httpVersion", Secondary: getIP(r)}

	api.writeJSON(
		w,
		apitype.VersionAPI{
			Version:   util.Version,
			BuildDate: util.BuildDate,
			GoVersion: util.GoVersion,
		},
		logFrom,
	)
}

// marshalFailRequestBody serialises failRequest responses (overridable for tests).
var marshalFailRequestBody = func(v map[string]string) ([]byte, error) {
	return decode.Marshal("json", v)
}

// failRequest returns a JSON response containing a message and status code.
func failRequest(w *http.ResponseWriter, err error, statusCode int) {
	// Write the response.
	(*w).WriteHeader(statusCode)
	resp := map[string]string{
		"message": errfmt.FormatError(err),
	}
	jsonResp, marshalErr := marshalFailRequestBody(resp)
	if marshalErr != nil {
		logx.Error(marshalErr, logx.LogFrom{Primary: "failRequest"}, true)
		jsonResp = []byte(`{"message":"failed to encode error response"}`)
	}
	if _, writeErr := (*w).Write(jsonResp); writeErr != nil {
		logx.Error(writeErr, logx.LogFrom{Primary: "failRequest"}, true)
	}
}
