package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	commErrors "github.com/pingcap/test-infra/common/errors"
	commR "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/common/model/rms/request"
	sdkCore "github.com/pingcap/test-infra/sdk/core"
	_ "github.com/pingcap/test-infra/sdk/resource/impl/k8s"
	"github.com/pingcap/test-infra/services/sdkserver/config"
	"github.com/pingcap/test-infra/services/sdkserver/core"
	"github.com/pingcap/test-infra/services/sdkserver/resources"
)

func NewRootResource(root RootSource) (core.Resource, error) {
	_, err := sdkCore.BuildContext()
	if err != nil {
		panic(err)
	}
	switch {
	case root.RootSourceHandle != "": // root resource is provided
	case root.RootSourceTestbedName != "": // get root resource from rms
		testbedSpec, err := getTestbedSpec(root.RootSourceTestbedName)
		if err != nil {
			return nil, err
		}
		root.RootSourceHandle = string(testbedSpec)
	default: // get root resource from file
		file, err := ioutil.ReadFile(root.RootSourceHandleFile)
		if err != nil {
			return nil, err
		}
		root.RootSourceHandle = string(file)
	}
	switch root.RootSourceType {
	case RootSourceTypeTestbed:
		return resources.NewTestBed(root.RootSourceHandle), nil
	case RootSourceTypeTestResource:
		h := commR.ResourceHandle{}
		err := json.Unmarshal([]byte(root.RootSourceHandle), &h)
		if err != nil {
			return nil, err
		}
		return resources.MakeResource(h)
	default:
		return nil, fmt.Errorf("unknow resource type")
	}
}

func getTestbedSpec(testbedName string) (spec []byte, err error) {
	req, err := http.NewRequest(http.MethodGet,
		config.DefaultConfig.RMSURL+fmt.Sprintf("/api/v1/testbeds/%s", testbedName), nil)
	if err != nil {
		return nil, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return ioutil.ReadAll(resp.Body)
}

type Handler struct {
	rootSource core.Resource
}

func (h Handler) Get(ctx echo.Context) error {
	path := ctx.Param("*")
	current, err := resolve(h.rootSource, path)
	if err != nil {
		return err
	}
	if current == nil {
		return core.NotFound
	}
	if strings.Contains(ctx.Request().UserAgent(), "Mozilla") {
		err = BetterHTML(ctx, current)
		if err == nil {
			return nil
		}
	}

	err = current.GetContent(ctx.Response())
	if err != nil {
		return err
	}
	return nil
}

func (h Handler) TestbedComplete(ctx echo.Context) error {
	if _, ok := h.rootSource.(resources.TestBed); !ok {
		return ctx.JSON(http.StatusInternalServerError, commErrors.NewBadRequest("root resource isn't a TestBed"))
	}

	testbed := h.rootSource.(resources.TestBed)
	urlMap, err := testbed.Archive()
	if err != nil { // archive process is partially/entirely failed, log error but still calls RMS's testbe complete API
		ctx.Logger().Error(err)
	}

	completionType := ctx.QueryParam("type")
	authToken := ctx.Request().Header.Get(echo.HeaderAuthorization)
	httpStatus, rmsCompleteResp, err := callRMSTestbedComplete(config.DefaultConfig.RMSURL, authToken, testbed.Name, completionType, urlMap)
	if err != nil {
		ctx.Logger().Error(err)
		return ctx.JSON(http.StatusInternalServerError, commErrors.NewInternalError(err))
	}

	// simplypass through rms response
	ctx.Response().Status = httpStatus
	ctx.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	_, _ = ctx.Response().Writer.Write(rmsCompleteResp)
	return nil
}

func resolve(root core.Resource, path string) (core.Resource, error) {
	names := strings.Split(path, "/")
	current := root
	var err error
	var idx = 0
	for _, n := range names {
		idx++
		if n == "" {
			continue
		}
		if current == nil {
			return nil, core.NotFound
		}
		current, err = current.GetSubResource(n)
		if err != nil {
			return nil, err
		}
	}
	return current, nil

}

func BetterHTML(ctx echo.Context, current core.Resource) error {
	if isDir, err := core.IsDir(current); err == nil && isDir {
		path := ctx.Param("*")
		var prefix = ""
		if path != "" && !strings.HasSuffix(path, "/") {
			idx := strings.LastIndexByte(path, '/')
			prefix = path[idx+1:] + "/"
		}
		s, err := dirToHTML(current, prefix)
		if err == nil {
			err := ctx.HTML(http.StatusOK, s)
			if err == nil {
				return nil
			}
		}
	}
	return current.GetContent(ctx.Response())
}

func dirToHTML(r core.Resource, prefix string) (string, error) {
	content, err := core.GetContent(r)
	if err != nil {
		return "", err
	}
	t := core.DirectoryContent{}
	err = json.Unmarshal(content, &t)
	if err != nil {
		return "", err
	}
	m := struct {
		Dir    core.DirectoryContent
		Prefix string
	}{
		Dir:    t,
		Prefix: prefix,
	}
	dirTemplate := `<html>
    <head></head>
    <body>
        <h>{{.Dir.FileType}}</h>
        <hr/>
        {{range .Dir.Subresource}}
        <a href={{$.Prefix}}{{.Name}}/>{{.Name}}</a>
		<br/>
        {{end}}
    </body>
</html>`
	parse, err := template.New("dir").Parse(dirTemplate)
	if err != nil {
		return "", err
	}
	buf := &bytes.Buffer{}
	err = parse.Execute(buf, m)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

func callRMSTestbedComplete(rmsURL string, authToken string, testbedName string, completionType string, clinicURLs map[string]string) (statusCode int, data []byte, err error) {
	testbedCompleteReq := request.OnTestbedCompletionRequest{
		ClinicURLs: clinicURLs,
	}
	reqData, err := json.Marshal(testbedCompleteReq)
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}

	req, err := http.NewRequest(http.MethodPut,
		fmt.Sprintf("%s/api/v1/testbeds/%s/complete?type=%s", rmsURL, testbedName, completionType), bytes.NewReader(reqData))
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	req.Header.Set(echo.HeaderAuthorization, authToken)

	client := http.Client{Timeout: 10 * time.Minute}
	resp, err := client.Do(req)
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}
	defer resp.Body.Close()

	data, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}
	return http.StatusOK, data, err
}
