package dao

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/render"
	"github.com/openfaas/faas-cli/proxy"
	"github.com/openfaas/faas/gateway/requests"
	"github.com/rs/zerolog/log"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/yaml"
)

const (
	oFaasGatewayEnv  = "OPENFAAS_GATEWAY"
	oFaasJWTTokenEnv = "OPENFAAS_JWT_TOKEN"
	oFaasTLSInsecure = "OPENFAAS_TLS_INSECURE"
)

var (
	_ Accessor  = (*OpenFaas)(nil)
	_ Nuker     = (*OpenFaas)(nil)
	_ Describer = (*OpenFaas)(nil)
)

// OpenFaas represents a faas gateway connection.
type OpenFaas struct {
	NonResource
}

// IsOpenFaasEnabled returns true if a gateway url is set in the environment.
func IsOpenFaasEnabled() bool {
	return os.Getenv(oFaasGatewayEnv) != ""
}

func getOpenFAASFlags() (string, string, bool) {
	gw, token := os.Getenv(oFaasGatewayEnv), os.Getenv(oFaasJWTTokenEnv)
	tlsInsecure := false
	if os.Getenv(oFaasTLSInsecure) == "true" {
		tlsInsecure = true
	}

	return gw, token, tlsInsecure
}

// Get returns a function by name.
func (f *OpenFaas) Get(ctx context.Context, path string) (runtime.Object, error) {
	ns, n := client.Namespaced(path)

	oo, err := f.List(ctx, ns)
	if err != nil {
		return nil, err
	}

	var found runtime.Object
	for _, o := range oo {
		r, ok := o.(render.OpenFaasRes)
		if !ok {
			continue
		}
		if r.Function.Name == n {
			found = o
			break
		}
	}

	if found == nil {
		return nil, fmt.Errorf("unable to locate function %q", path)
	}

	return found, nil
}

// List returns a collection of functions.
func (f *OpenFaas) List(_ context.Context, ns string) ([]runtime.Object, error) {
	if !IsOpenFaasEnabled() {
		return nil, errors.New("OpenFAAS is not enabled on this cluster")
	}

	gw, token, tls := getOpenFAASFlags()
	ff, err := proxy.ListFunctionsToken(gw, tls, token, ns)
	if err != nil {
		return nil, err
	}

	oo := make([]runtime.Object, 0, len(ff))
	for _, f := range ff {
		oo = append(oo, render.OpenFaasRes{Function: f})
	}

	return oo, nil
}

// Delete removes a function.
func (f *OpenFaas) Delete(path string, _, _ bool) error {
	gw, token, tls := getOpenFAASFlags()
	ns, n := client.Namespaced(path)

	// BOZO!! openfaas spews to stdout. Not good for us...
	return deleteFunctionToken(gw, n, tls, token, ns)
}

// ToYAML dumps a function to yaml.
func (f *OpenFaas) ToYAML(path string) (string, error) {
	return f.Describe(path)
}

// Describe describes a function.
func (f *OpenFaas) Describe(path string) (string, error) {
	o, err := f.Get(context.Background(), path)
	if err != nil {
		return "", err
	}

	fn, ok := o.(render.OpenFaasRes)
	if !ok {
		return "", fmt.Errorf("expecting OpenFaasRes but got %T", o)
	}

	raw, err := json.Marshal(fn)
	if err != nil {
		return "", err
	}

	bytes, err := yaml.JSONToYAML(raw)
	if err != nil {
		return "", err
	}

	return string(bytes), nil
}

// BOZO!! Meow! openfaas fn prints to stdout have to dup ;(
func deleteFunctionToken(gateway string, functionName string, tlsInsecure bool, token string, namespace string) error {
	defaultCommandTimeout := 60 * time.Second

	gateway = strings.TrimRight(gateway, "/")
	delReq := requests.DeleteFunctionRequest{FunctionName: functionName}
	reqBytes, _ := json.Marshal(&delReq)
	reader := bytes.NewReader(reqBytes)

	c := proxy.MakeHTTPClient(&defaultCommandTimeout, tlsInsecure)

	deleteEndpoint, err := createSystemEndpoint(gateway, namespace)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("DELETE", deleteEndpoint, reader)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	if len(token) > 0 {
		proxy.SetToken(req, token)
	} else {
		proxy.SetAuth(req, gateway)
	}

	delRes, delErr := c.Do(req)
	if delErr != nil {
		return delErr
	}

	if delRes.Body != nil {
		defer func() {
			if err := delRes.Body.Close(); err != nil {
				log.Error().Err(err).Msgf("closing delete-gtw body")
			}
		}()
	}

	switch delRes.StatusCode {
	case http.StatusOK, http.StatusCreated, http.StatusAccepted:
		return nil
	case http.StatusNotFound:
		return fmt.Errorf("no function named %s found", functionName)
	case http.StatusUnauthorized:
		return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server")
	default:
		bytesOut, err := ioutil.ReadAll(delRes.Body)
		if err != nil {
			return err
		}
		return fmt.Errorf("server returned unexpected status code %d %s", delRes.StatusCode, string(bytesOut))
	}
}

func createSystemEndpoint(gateway, namespace string) (string, error) {
	const systemPath = "/system/functions"

	gatewayURL, err := url.Parse(gateway)
	if err != nil {
		return "", fmt.Errorf("invalid gateway URL: %s", err.Error())
	}
	gatewayURL.Path = path.Join(gatewayURL.Path, systemPath)
	if len(namespace) > 0 {
		q := gatewayURL.Query()
		q.Set("namespace", namespace)
		gatewayURL.RawQuery = q.Encode()
	}
	return gatewayURL.String(), nil
}
