// Copyright 2019 Sorint.lab
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
// See the License for the specific language governing permissions and
// limitations under the License.

package gateway

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"path"

	"github.com/sorintlab/agola/internal/config"
	gitsource "github.com/sorintlab/agola/internal/gitsources"
	"github.com/sorintlab/agola/internal/gitsources/agolagit"
	"github.com/sorintlab/agola/internal/runconfig"
	csapi "github.com/sorintlab/agola/internal/services/configstore/api"
	"github.com/sorintlab/agola/internal/services/gateway/common"
	rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api"
	rstypes "github.com/sorintlab/agola/internal/services/runservice/types"
	"github.com/sorintlab/agola/internal/services/types"
	"github.com/sorintlab/agola/internal/util"

	"github.com/pkg/errors"
	"go.uber.org/zap"
)

const (
	defaultSSHPort = "22"

	agolaDefaultConfigPath = ".agola/config.yml"

	// List of runs annotations
	AnnotationEventType = "event_type"
	AnnotationRunType   = "runtype"
	AnnotationProjectID = "projectid"
	AnnotationUserID    = "userid"
	// AnnotationVirtualBranch represent a "virtual branch": i.e a normal branch, a pr (with name pr-$prid), a tag (with name tag-tagname)
	AnnotationVirtualBranch = "virtual_branch"

	AnnotationCommitSHA   = "commit_sha"
	AnnotationRef         = "ref"
	AnnotationSender      = "sender"
	AnnotationMessage     = "message"
	AnnotationCommitLink  = "commit_link"
	AnnotationCompareLink = "compare_link"

	AnnotationBranch          = "branch"
	AnnotationBranchLink      = "branch_link"
	AnnotationTag             = "tag"
	AnnotationTagLink         = "tag_link"
	AnnotationPullRequestID   = "pull_request_id"
	AnnotationPullRequestLink = "pull_request_link"
)

type GroupType string

const (
	GroupTypeProject     GroupType = "project"
	GroupTypeUser        GroupType = "user"
	GroupTypeBranch      GroupType = "branch"
	GroupTypeTag         GroupType = "tag"
	GroupTypePullRequest GroupType = "pr"
)

func genAnnotationVirtualBranch(webhookData *types.WebhookData) string {
	switch webhookData.Event {
	case types.WebhookEventPush:
		return "branch-" + webhookData.Branch
	case types.WebhookEventTag:
		return "tag-" + webhookData.Tag
	case types.WebhookEventPullRequest:
		return "pr-" + webhookData.PullRequestID
	}

	panic(fmt.Errorf("invalid webhook event type: %q", webhookData.Event))
}

func genGroup(baseGroupType GroupType, baseGroupID string, webhookData *types.WebhookData) string {
	// we pathescape the branch name to handle branches with slashes and make the
	// branch a single path entry
	switch webhookData.Event {
	case types.WebhookEventPush:
		return path.Join("/", string(baseGroupType), baseGroupID, string(GroupTypeBranch), url.PathEscape(webhookData.Branch))
	case types.WebhookEventTag:
		return path.Join("/", string(baseGroupType), baseGroupID, string(GroupTypeTag), url.PathEscape(webhookData.Tag))
	case types.WebhookEventPullRequest:
		return path.Join("/", string(baseGroupType), baseGroupID, string(GroupTypePullRequest), url.PathEscape(webhookData.PullRequestID))
	}

	panic(fmt.Errorf("invalid webhook event type: %q", webhookData.Event))
}

type webhooksHandler struct {
	log               *zap.SugaredLogger
	configstoreClient *csapi.Client
	runserviceClient  *rsapi.Client
	apiExposedURL     string
}

func (h *webhooksHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	code, userErr, err := h.handleWebhook(r)
	if err != nil {
		h.log.Errorf("err: %+v", err)
		http.Error(w, userErr, code)
	}
}

func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
	ctx := r.Context()

	projectID := r.URL.Query().Get("projectid")
	userID := r.URL.Query().Get("userid")
	if projectID == "" && userID == "" {
		return http.StatusBadRequest, "", errors.Errorf("bad webhook url %q. Missing projectid or userid", r.URL)
	}

	isUserBuild := false
	if projectID == "" {
		isUserBuild = true
	}

	defer r.Body.Close()

	var webhookData *types.WebhookData
	var sshPrivKey string
	var cloneURL string
	var skipSSHHostKeyCheck bool
	var runType types.RunType
	variables := map[string]string{}

	var gitSource gitsource.GitSource
	if !isUserBuild {
		project, _, err := h.configstoreClient.GetProject(ctx, projectID)
		if err != nil {
			return http.StatusBadRequest, "", errors.Wrapf(err, "failed to get project %s", projectID)
		}
		h.log.Infof("project: %s", util.Dump(project))

		user, _, err := h.configstoreClient.GetUserByLinkedAccount(ctx, project.LinkedAccountID)
		if err != nil {
			return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get user by linked account %q", project.LinkedAccountID)
		}
		la := user.LinkedAccounts[project.LinkedAccountID]
		h.log.Infof("la: %s", util.Dump(la))
		if la == nil {
			return http.StatusInternalServerError, "", errors.Errorf("linked account %q in user %q doesn't exist", project.LinkedAccountID, user.UserName)
		}
		rs, _, err := h.configstoreClient.GetRemoteSource(ctx, la.RemoteSourceID)
		if err != nil {
			return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get remote source %q", la.RemoteSourceID)
		}

		gitSource, err = common.GetGitSource(rs, la)
		if err != nil {
			return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to create gitea client")
		}

		sshPrivKey = project.SSHPrivateKey
		skipSSHHostKeyCheck = project.SkipSSHHostKeyCheck
		runType = types.RunTypeProject
		webhookData, err = gitSource.ParseWebhook(r)
		if err != nil {
			return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook")
		}
		// skip nil webhook data
		// TODO(sgotti) report the reason of the skip
		if webhookData == nil {
			h.log.Infof("skipping webhook")
			return 0, "", nil
		}

		webhookData.ProjectID = projectID

		cloneURL = webhookData.SSHURL

		// get project variables
		pvars, _, err := h.configstoreClient.GetProjectVariables(ctx, project.ID, true)
		if err != nil {
			return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get project variables")
		}
		h.log.Infof("pvars: %v", util.Dump(pvars))

		// remove overriden variables
		pvars = common.FilterOverridenVariables(pvars)
		h.log.Infof("pvars: %v", util.Dump(pvars))

		// get project secrets
		secrets, _, err := h.configstoreClient.GetProjectSecrets(ctx, project.ID, true)
		if err != nil {
			return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get project secrets")
		}
		h.log.Infof("secrets: %v", util.Dump(secrets))
		for _, pvar := range pvars {
			// find the value match
			var varval types.VariableValue
			for _, varval = range pvar.Values {
				h.log.Infof("varval: %v", util.Dump(varval))
				match := types.MatchWhen(varval.When, webhookData.Branch, webhookData.Tag, webhookData.Ref)
				if !match {
					continue
				}
				// get the secret value referenced by the variable, it must be a secret at the same level or a lower level
				secret := common.GetVarValueMatchingSecret(varval, pvar.Parent.Path, secrets)
				h.log.Infof("secret: %v", util.Dump(secret))
				if secret != nil {
					varValue, ok := secret.Data[varval.SecretVar]
					if ok {
						variables[pvar.Name] = varValue
					}
				}
				break
			}
		}
		h.log.Infof("variables: %v", util.Dump(variables))

	} else {
		gitSource = agolagit.New(h.apiExposedURL + "/repos")
		var err error
		webhookData, err = gitSource.ParseWebhook(r)
		if err != nil {
			return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook")
		}
		// skip nil webhook data
		// TODO(sgotti) report the reason of the skip
		if webhookData == nil {
			h.log.Infof("skipping webhook")
			return 0, "", nil
		}

		user, _, err := h.configstoreClient.GetUser(ctx, userID)
		if err != nil {
			return http.StatusBadRequest, "", errors.Wrapf(err, "failed to get user with id %q", userID)
		}
		h.log.Debugf("user: %s", util.Dump(user))
		userID = user.ID

		cloneURL = fmt.Sprintf("%s/%s", h.apiExposedURL+"/repos", webhookData.Repo.Path)
		runType = types.RunTypeUser
	}

	h.log.Infof("webhookData: %s", util.Dump(webhookData))

	var data []byte
	err := util.ExponentialBackoff(util.FetchFileBackoff, func() (bool, error) {
		var err error
		data, err = gitSource.GetFile(webhookData.Repo.Path, webhookData.CommitSHA, agolaDefaultConfigPath)
		if err == nil {
			return true, nil
		}
		h.log.Errorf("get file err: %v", err)
		return false, nil
	})
	if err != nil {
		return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to fetch config file")
	}
	h.log.Debug("data: %s", data)

	gitURL, err := util.ParseGitURL(cloneURL)
	if err != nil {
		return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to parse clone url")
	}
	gitHost := gitURL.Hostname()
	gitPort := gitURL.Port()
	if gitPort == "" {
		gitPort = defaultSSHPort
	}

	// this env vars ovverrides other env vars
	env := map[string]string{
		"CI":                   "true",
		"AGOLA_SSHPRIVKEY":     sshPrivKey,
		"AGOLA_REPOSITORY_URL": cloneURL,
		"AGOLA_GIT_HOST":       gitHost,
		"AGOLA_GIT_PORT":       gitPort,
		"AGOLA_GIT_REF":        webhookData.Ref,
		"AGOLA_GIT_COMMITSHA":  webhookData.CommitSHA,
	}

	if skipSSHHostKeyCheck {
		env["AGOLA_SKIPSSHHOSTKEYCHECK"] = "1"
	}

	annotations := map[string]string{
		AnnotationRunType:       string(runType),
		AnnotationEventType:     string(webhookData.Event),
		AnnotationVirtualBranch: genAnnotationVirtualBranch(webhookData),
		AnnotationCommitSHA:     webhookData.CommitSHA,
		AnnotationRef:           webhookData.Ref,
		AnnotationSender:        webhookData.Sender,
		AnnotationMessage:       webhookData.Message,
		AnnotationCommitLink:    webhookData.CommitLink,
		AnnotationCompareLink:   webhookData.CompareLink,
	}

	if !isUserBuild {
		annotations[AnnotationProjectID] = webhookData.ProjectID
	} else {
		annotations[AnnotationUserID] = userID
	}

	if webhookData.Event == types.WebhookEventPush {
		annotations[AnnotationBranch] = webhookData.Branch
		annotations[AnnotationBranchLink] = webhookData.BranchLink
	}
	if webhookData.Event == types.WebhookEventTag {
		annotations[AnnotationTag] = webhookData.Tag
		annotations[AnnotationTagLink] = webhookData.TagLink
	}
	if webhookData.Event == types.WebhookEventPullRequest {
		annotations[AnnotationPullRequestID] = webhookData.PullRequestID
		annotations[AnnotationPullRequestLink] = webhookData.PullRequestLink
	}

	var group string
	if !isUserBuild {
		group = genGroup(GroupTypeProject, webhookData.ProjectID, webhookData)
	} else {
		group = genGroup(GroupTypeUser, userID, webhookData)
	}

	if err := h.createRuns(ctx, data, group, annotations, env, variables, webhookData); err != nil {
		return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to create run")
	}
	//if err := gitSource.CreateStatus(webhookData.Repo.Owner, webhookData.Repo.Name, webhookData.CommitSHA, gitsource.CommitStatusPending, "localhost:8080", "build %s", "agola"); err != nil {
	//	h.log.Errorf("failed to update commit status: %v", err)
	//}

	return 0, "", nil
}

func (h *webhooksHandler) createRuns(ctx context.Context, configData []byte, group string, annotations, staticEnv, variables map[string]string, webhookData *types.WebhookData) error {
	setupErrors := []string{}

	config, err := config.ParseConfig([]byte(configData))
	if err != nil {
		log.Errorf("failed to parse config: %+v", err)

		// create a run (per config file) with a generic error since we cannot parse
		// it and know how many pipelines are defined
		setupErrors = append(setupErrors, err.Error())
		createRunReq := &rsapi.RunCreateRequest{
			RunConfigTasks:    nil,
			Group:             group,
			SetupErrors:       setupErrors,
			Name:              rstypes.RunGenericSetupErrorName,
			StaticEnvironment: staticEnv,
			Annotations:       annotations,
		}

		if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil {
			log.Errorf("failed to create run: %+v", err)
			return err
		}
		return nil
	}
	//h.log.Debugf("config: %v", util.Dump(config))

	//h.log.Debugf("pipeline: %s", createRunOpts.PipelineName)
	for _, pipeline := range config.Pipelines {
		rcts := runconfig.GenRunConfigTasks(util.DefaultUUIDGenerator{}, config, pipeline.Name, variables, webhookData.Branch, webhookData.Tag, webhookData.Ref)

		h.log.Debugf("rcts: %s", util.Dump(rcts))
		h.log.Infof("group: %s", group)
		createRunReq := &rsapi.RunCreateRequest{
			RunConfigTasks:    rcts,
			Group:             group,
			SetupErrors:       setupErrors,
			Name:              pipeline.Name,
			StaticEnvironment: staticEnv,
			Annotations:       annotations,
		}

		if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil {
			log.Errorf("failed to create run: %+v", err)
			return err
		}
	}

	return nil
}