package updates

import (
	"bytes"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/ghodss/yaml"

	"github.com/safing/portbase/api"
	"github.com/safing/portbase/log"
	"github.com/safing/portbase/utils"
)

const (
	apiPathCheckForUpdates = "updates/check"
)

func registerAPIEndpoints() error {
	if err := api.RegisterEndpoint(api.Endpoint{
		Name:        "Check for Updates",
		Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.",
		Parameters: []api.Parameter{{
			Method:      http.MethodPost,
			Field:       "download",
			Value:       "",
			Description: "Force downloading and applying of all updates, regardless of auto-update settings.",
		}},
		Path:      apiPathCheckForUpdates,
		Write:     api.PermitUser,
		BelongsTo: module,
		ActionFunc: func(r *api.Request) (msg string, err error) {
			// Check if we should also download regardless of settings.
			downloadAll := r.URL.Query().Has("download")

			// Trigger update task.
			err = TriggerUpdate(true, downloadAll)
			if err != nil {
				return "", err
			}

			// Report how we triggered.
			if downloadAll {
				return "downloading all updates...", nil
			}
			return "checking for updates...", nil
		},
	}); err != nil {
		return err
	}

	if err := api.RegisterEndpoint(api.Endpoint{
		Name:        "Get Resource",
		Description: "Returns the requested resource from the udpate system",
		Path:        `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`,
		Read:        api.PermitUser,
		ReadMethod:  http.MethodGet,
		BelongsTo:   module,
		HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
			// Get identifier from URL.
			var identifier string
			if ar := api.GetAPIRequest(r); ar != nil {
				identifier = ar.URLVars["identifier"]
			}
			if identifier == "" {
				http.Error(w, "no resource speicified", http.StatusBadRequest)
				return
			}

			// Get resource.
			resource, err := registry.GetFile(identifier)
			if err != nil {
				http.Error(w, err.Error(), http.StatusNotFound)
				return
			}

			// Open file for reading.
			file, err := os.Open(resource.Path())
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			defer file.Close() //nolint:errcheck,gosec

			// Assign file to reader
			var reader io.Reader = file

			// Add version to header.
			w.Header().Set("Resource-Version", resource.Version())

			// Set Content-Type.
			contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path()))
			w.Header().Set("Content-Type", contentType)

			// Check if the content type may be returned.
			accept := r.Header.Get("Accept")
			if accept != "" {
				mimeTypes := strings.Split(accept, ",")
				// First, clean mime types.
				for i, mimeType := range mimeTypes {
					mimeType = strings.TrimSpace(mimeType)
					mimeType, _, _ = strings.Cut(mimeType, ";")
					mimeTypes[i] = mimeType
				}
				// Second, check if we may return anything.
				var acceptsAny bool
				for _, mimeType := range mimeTypes {
					switch mimeType {
					case "*", "*/*":
						acceptsAny = true
					}
				}
				// Third, check if we can convert.
				if !acceptsAny {
					var converted bool
					sourceType, _, _ := strings.Cut(contentType, ";")
				findConvertiblePair:
					for _, mimeType := range mimeTypes {
						switch {
						case sourceType == "application/yaml" && mimeType == "application/json":
							yamlData, err := io.ReadAll(reader)
							if err != nil {
								http.Error(w, err.Error(), http.StatusInternalServerError)
								return
							}
							jsonData, err := yaml.YAMLToJSON(yamlData)
							if err != nil {
								http.Error(w, err.Error(), http.StatusInternalServerError)
								return
							}
							reader = bytes.NewReader(jsonData)
							converted = true
							break findConvertiblePair
						}
					}

					// If we could not convert to acceptable format, return an error.
					if !converted {
						http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable)
						return
					}
				}
			}

			// Write file.
			w.WriteHeader(http.StatusOK)
			if r.Method != http.MethodHead {
				_, err = io.Copy(w, reader)
				if err != nil {
					log.Errorf("updates: failed to serve resource file: %s", err)
					return
				}
			}
		},
	}); err != nil {
		return err
	}

	return nil
}
