//go:build search
// +build search

package client

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/spf13/cobra"
	"gopkg.in/yaml.v3"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/api/constants"
)

const (
	StatusOnline  = "online"
	StatusOffline = "offline"
	StatusUnknown = "unknown"
)

func NewServerStatusCommand() *cobra.Command {
	serverInfoCmd := &cobra.Command{
		Use:   "status",
		Short: "Information about the server configuration and build information",
		Long:  `Information about the server configuration and build information`,
		Args:  cobra.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			searchConfig, err := GetSearchConfigFromFlags(cmd, NewSearchService())
			if err != nil {
				return err
			}

			return GetServerStatus(searchConfig)
		},
	}

	serverInfoCmd.PersistentFlags().String(URLFlag, "",
		"Specify zot server URL if config-name is not mentioned")
	serverInfoCmd.PersistentFlags().StringP(ConfigFlag, "c", "",
		"Specify the registry configuration to use for connection")
	serverInfoCmd.PersistentFlags().StringP(UserFlag, "u", "",
		`User Credentials of zot server in "username:password" format`)
	serverInfoCmd.Flags().StringP(OutputFormatFlag, "f", "text", "Specify the output format [text|json|yaml]")

	return serverInfoCmd
}

func GetServerStatus(config SearchConfig) error {
	ctx := context.Background()
	username, password := getUsernameAndPassword(config.User)

	checkAPISupportEndpoint, err := combineServerAndEndpointURL(config.ServURL, constants.RoutePrefix+"/")
	if err != nil {
		return err
	}

	_, err = makeGETRequest(ctx, checkAPISupportEndpoint, username, password, config.VerifyTLS, config.Debug,
		nil, config.ResultWriter)
	if err != nil {
		serverInfo := ServerInfo{}

		switch {
		case errors.Is(err, zerr.ErrUnauthorizedAccess):
			serverInfo.Status = StatusUnknown
			serverInfo.ErrorMsg = "unauthorised access, " + getCredentialsSuggestion(username)
		case errors.Is(err, zerr.ErrBadHTTPStatusCode), errors.Is(err, zerr.ErrURLNotFound):
			serverInfo.Status = StatusOffline
			serverInfo.ErrorMsg = fmt.Sprintf("%s: request at %s failed", zerr.ErrAPINotSupported.Error(),
				checkAPISupportEndpoint)
		default:
			serverInfo.Status = StatusOffline
			serverInfo.ErrorMsg = err.Error()
		}

		return PrintServerInfo(serverInfo, config)
	}

	mgmtEndpoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s",
		constants.RoutePrefix, constants.ExtMgmt))
	if err != nil {
		return err
	}

	serverInfo := ServerInfo{}

	_, err = makeGETRequest(ctx, mgmtEndpoint, username, password, config.VerifyTLS, config.Debug,
		&serverInfo, config.ResultWriter)

	switch {
	case err == nil:
		serverInfo.Status = StatusOnline
	case errors.Is(err, zerr.ErrURLNotFound):
		serverInfo.Status = StatusOnline
		serverInfo.ErrorMsg = fmt.Sprintf("%s%s endpoint is not available", constants.RoutePrefix, constants.ExtMgmt)
	case errors.Is(err, zerr.ErrUnauthorizedAccess):
		serverInfo.Status = StatusOnline
		serverInfo.ErrorMsg = "unauthorised access, " + getCredentialsSuggestion(username)
	case errors.Is(err, zerr.ErrBadHTTPStatusCode):
		serverInfo.Status = StatusOnline
		serverInfo.ErrorMsg = fmt.Sprintf("%s: request at %s failed", zerr.ErrAPINotSupported.Error(),
			checkAPISupportEndpoint)
	default:
		serverInfo.Status = StatusOffline
		serverInfo.ErrorMsg = err.Error()
	}

	return PrintServerInfo(serverInfo, config)
}

func getCredentialsSuggestion(username string) string {
	if username == "" {
		return "endpoint requires valid user credentials (add the flag '--user [user]:[password]')"
	}

	return "given credentials are invalid"
}

func PrintServerInfo(serverInfo ServerInfo, config SearchConfig) error {
	outputResult, err := serverInfo.ToStringFormat(config.OutputFormat)
	if err != nil {
		return err
	}

	fmt.Fprintln(config.ResultWriter, outputResult)

	return nil
}

type ServerInfo struct {
	Status          string `json:"status,omitempty"          mapstructure:"status"`
	ErrorMsg        string `json:"error,omitempty"           mapstructure:"error"`
	DistSpecVersion string `json:"distSpecVersion,omitempty" mapstructure:"distSpecVersion"`
	Commit          string `json:"commit,omitempty"          mapstructure:"commit"`
	BinaryType      string `json:"binaryType,omitempty"      mapstructure:"binaryType"`
	ReleaseTag      string `json:"releaseTag,omitempty"      mapstructure:"releaseTag"`
}

func (si *ServerInfo) ToStringFormat(format string) (string, error) {
	switch format {
	case "text", "":
		return si.ToText()
	case "json":
		return si.ToJSON()
	case "yaml", "yml":
		return si.ToYAML()
	default:
		return "", zerr.ErrFormatNotSupported
	}
}

func (si *ServerInfo) ToText() (string, error) {
	flagsList := strings.Split(strings.Trim(si.BinaryType, "-"), "-")
	flags := strings.Join(flagsList, ", ")

	var output string

	if si.ErrorMsg != "" {
		serverStatus := fmt.Sprintf("Server Status: %s\n"+
			"Error: %s", si.Status, si.ErrorMsg)

		output = serverStatus
	} else {
		serverStatus := "Server Status: " + si.Status
		serverInfo := fmt.Sprintf("Server Version: %s\n"+
			"Dist Spec Version: %s\n"+
			"Built with: %s",
			si.ReleaseTag, si.DistSpecVersion, flags,
		)

		output = serverStatus + "\n" + serverInfo
	}

	return output, nil
}

func (si *ServerInfo) ToJSON() (string, error) {
	blob, err := json.MarshalIndent(*si, "", "    ")

	return string(blob), err
}

func (si *ServerInfo) ToYAML() (string, error) {
	body, err := yaml.Marshal(*si)

	return string(body), err
}
