package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"text/template"

	cfsslconfig "github.com/cloudflare/cfssl/config"
	"github.com/emicklei/go-restful"
	"gopkg.in/yaml.v2"
	"m.cluseau.fr/go/httperr"

	"novit.tech/direktil/pkg/localconfig"

	"novit.tech/direktil/local-server/pkg/mime"
)

func registerWS(rest *restful.Container) {
	// public-level APIs
	{
		ws := &restful.WebService{}
		ws.
			Path("/public").
			Produces(mime.JSON).
			Consumes(mime.JSON).
			Route(ws.POST("/unlock-store").To(wsUnlockStore).
				Reads(NamedPassphrase{}).
				Writes("").
				Doc("Try to unlock the store")).
			Route(ws.GET("/store.tar").To(wsStoreDownload).
				Produces(mime.TAR).
				Param(ws.QueryParameter("token", "the download token")).
				Doc("Fetch the encrypted store")).
			Route(ws.POST("/store.tar").To(wsStoreUpload).
				Consumes(mime.TAR).
				Doc("Upload an existing store")).
			Route(ws.GET("/downloads/{token}").To(wsDownload)).
			Route(ws.GET("/downloads/{token}/{asset}").To(wsDownloadAsset).
				Param(ws.PathParameter("token", "the download token")).
				Param(ws.PathParameter("asset", "the requested asset")).
				Doc("Fetch an asset via a download token")).
			Route(ws.GET("/download-set").To(wsDownloadSet)).
			Route(ws.GET("/download-set/{kind}/{name}/{asset}").To(wsDownloadSetAsset)).
			Route(ws.POST("/parse-kube-csr").To(wsParseKubeCSR).
				Reads(KubeCSRParseReq{}).
				Writes(KubeCSRParseResp{}).
				Doc("Parse a Kubernetes CSR PEM and return the subject fields"))

		rest.Add(ws)
	}

	// Admin-level APIs
	ws := (&restful.WebService{}).
		Filter(requireSecStore).
		Filter(adminAuth).
		Param(restful.HeaderParameter("Authorization", "Admin bearer token").Required(true)).
		Produces(mime.JSON)

	// - store management
	ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
		Consumes(mime.JSON).Reads(NamedPassphrase{}).
		Doc("Add an unlock key to the store"))
	ws.Route(ws.POST("/store/delete-key").To(wsStoreDelKey).
		Consumes(mime.JSON).Reads("").
		Doc("Remove an unlock key to the store (by its name)"))

	// - downloads
	ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
		Consumes(mime.JSON).Reads(DownloadSpec{}).
		Produces(mime.JSON).
		Doc("Create a download token for the given download"))
	ws.Route(ws.POST("/sign-download-set").To(wsSignDownloadSet).
		Consumes(mime.JSON).Reads(DownloadSetReq{}).
		Produces(mime.JSON).
		Doc("Sign a download set"))

	// - configs API
	ws.Route(ws.GET("/config").To(wsFetchConfig).
		Produces(mime.JSON, mime.YAML).
		Doc("Fetch the current configuration"))
	ws.Route(ws.POST("/configs").To(wsUploadConfig).
		Consumes(mime.YAML, mime.JSON).Param(ws.BodyParameter("config", "The new full configuration")).
		Produces(mime.JSON).Writes(true).
		Doc("Upload a new current configuration, archiving the previous one"))

	// - clusters API
	ws.Route(ws.GET("/clusters").To(wsListClusters).
		Doc("List clusters"))

	ws.Route(ws.GET("/hosts-from-template").To(wsHostsFromTemplateList).
		Doc("List host template instances"))
	ws.Route(ws.POST("/hosts-from-template/{name}").To(wsHostsFromTemplateSet).
		Reads(HostFromTemplate{}).
		Doc("Create or update a host template instance"))
	ws.Route(ws.DELETE("/hosts-from-template/{name}").To(wsHostsFromTemplateDelete).
		Reads(HostFromTemplate{}).
		Doc("Delete a host template instance"))

	const (
		GET  = http.MethodGet
		PUT  = http.MethodPut
		POST = http.MethodPost
	)

	cluster := func(method, subPath string) *restful.RouteBuilder {
		return ws.Method(method).Path("/clusters/{cluster-name}" + subPath).
			Param(ws.PathParameter("cluster-name", "name of the cluster"))
	}

	for _, builder := range []*restful.RouteBuilder{
		cluster(GET, "").To(wsCluster).
			Doc("Get cluster details"),

		cluster(GET, "/addons").To(wsClusterAddons).
			Produces(mime.YAML).
			Doc("Get cluster addons").
			Returns(http.StatusOK, "OK", nil).
			Returns(http.StatusNotFound, "The cluster does not exists or does not have addons defined", nil),

		cluster(GET, "/tokens").To(wsClusterTokens).
			Doc("List cluster's tokens"),
		cluster(GET, "/tokens/{token-name}").To(wsClusterToken).
			Doc("Get cluster's token"),

		cluster(GET, "/passwords").To(wsClusterPasswords).
			Doc("List cluster's passwords"),
		cluster(GET, "/passwords/{password-name}").To(wsClusterPassword).
			Doc("Get cluster's password"),
		cluster(PUT, "/passwords/{password-name}").To(wsClusterSetPassword).
			Doc("Set cluster's password"),

		cluster(GET, "/CAs").To(wsClusterCAs).
			Doc("Get cluster CAs"),
		cluster(GET, "/CAs/{ca-name}/certificate").To(wsClusterCACert).
			Produces(mime.CACERT).
			Doc("Get cluster CA's certificate"),
		cluster(PUT, "/CAs/{ca-name}/certificate").To(wsClusterCACertPut).
			Consumes(mime.CACERT).
			Doc("Set cluster CA's certificate(s)"),
		cluster(GET, "/CAs/{ca-name}/signed").To(wsClusterSignedCert).
			Produces(mime.CERT).
			Param(ws.QueryParameter("name", "signed reference name").Required(true)).
			Doc("Get cluster's certificate signed by the CA"),

		cluster(GET, "/ssh/user-ca").To(wsClusterSSHUserCAPubKey).
			Produces(mime.OCTET).
			Doc("User CA public key for this cluster"),
		cluster(POST, "/ssh/user-ca/sign").To(wsClusterSSHUserCASign).
			Produces(mime.OCTET).
			Doc("Sign a user's SSH public key for this cluster"),
		cluster(POST, "/kube/sign").To(wsClusterKubeCASign).
			Produces(mime.OCTET).
			Doc("Sign a user's public key for this cluster's Kubernetes API server"),
	} {
		ws.Route(builder)
	}

	ws.Route(ws.GET("/hosts").To(wsListHosts).
		Doc("List hosts"))

	rest.Add(ws)

	// Hosts API
	ws = (&restful.WebService{}).
		Filter(requireSecStore).
		Filter(adminAuth).
		Path("/hosts/{host-name}").
		Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))

	(&wsHost{
		hostDoc: "given host",
		getHost: func(req *restful.Request) (string, error) {
			return req.PathParameter("host-name"), nil
		},
	}).register(ws, func(rb *restful.RouteBuilder) {
		rb.Param(ws.PathParameter("host-name", "host's name"))
	})

	rest.Add(ws)

	// Hosts by token API
	ws = (&restful.WebService{}).
		Filter(requireSecStore).
		Path("/hosts-by-token/{host-token}").
		Param(ws.PathParameter("host-token", "host's download token"))

	(&wsHost{
		hostDoc: "token's host",
		getHost: func(req *restful.Request) (host string, err error) {
			reqToken := req.PathParameter("host-token")

			data, err := hostDownloadTokens.Data()
			if err != nil {
				return
			}

			for h, token := range data {
				if token == reqToken {
					host = h
					return
				}
			}

			return
		},
	}).register(ws, func(rb *restful.RouteBuilder) {
		rb.Notes("In this case, the host is detected from the token")
	})

	rest.Add(ws)
}

func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
	if !secStore.Unlocked() {
		wsError(resp, ErrStoreLocked)
		return
	}
	chain.ProcessFilter(req, resp)
}

func wsReadConfig(resp *restful.Response) *localconfig.Config {
	cfg, err := readConfig()
	if err != nil {
		log.Print("failed to read config: ", err)
		resp.WriteErrorString(http.StatusServiceUnavailable, "failed to read config")
		return nil
	}

	return cfg
}

func wsNotFound(resp *restful.Response) {
	wsError(resp, ErrNotFound)
}

func wsBadRequest(resp *restful.Response, err string) {
	httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter)
}

func wsError(resp *restful.Response, err error) {
	log.Output(2, fmt.Sprint("request failed: ", err))

	switch err := err.(type) {
	case httperr.Error:
		err.WriteJSON(resp.ResponseWriter)

	default:
		httperr.Internal(err).WriteJSON(resp.ResponseWriter)
	}
}

func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value any) {
	tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr)
	if err != nil {
		wsError(resp, err)
		return
	}

	err = tmpl.Execute(resp, value)
	if err != nil {
		wsError(resp, err)
		return
	}
}

func init() {
	restful.RegisterEntityAccessor(mime.YAML, yamlEntityAccessor{})
}

type yamlEntityAccessor struct{}

var _ restful.EntityReaderWriter = yamlEntityAccessor{}

func (_ yamlEntityAccessor) Read(req *restful.Request, v any) error {
	decoder := yaml.NewDecoder(req.Request.Body)
	return decoder.Decode(v)
}
func (_ yamlEntityAccessor) Write(resp *restful.Response, status int, v any) error {
	if v == nil {
		resp.WriteHeader(status)
		// do not write a nil representation
		return nil
	}
	resp.Header().Set("Content-Type", mime.YAML)
	resp.WriteHeader(status)
	return yaml.NewEncoder(resp.ResponseWriter).Encode(v)
}
