package drivers

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/pkg/errors"
	v3 "github.com/rancher/rancher/pkg/generated/norman/project.cattle.io/v3"
	"github.com/rancher/rancher/pkg/pipeline/remote/model"
	"github.com/rancher/rancher/pkg/pipeline/utils"
	"github.com/rancher/rancher/pkg/ref"
	"github.com/xanzy/go-gitlab"
)

const (
	GitlabWebhookHeader = "X-Gitlab-Event"
	GitlabTokenHeader   = "X-Gitlab-Token"
	gitlabPushEvent     = "Push Hook"
	gitlabMREvent       = "Merge Request Hook"
	gitlabTagEvent      = "Tag Push Hook"

	gitlabActionOpen   = "open"
	gitlabActionUpdate = "update"

	gitlabStateOpen = "opened"
)

type GitlabDriver struct {
	PipelineLister             v3.PipelineLister
	PipelineExecutions         v3.PipelineExecutionInterface
	SourceCodeCredentials      v3.SourceCodeCredentialInterface
	SourceCodeCredentialLister v3.SourceCodeCredentialLister
}

func (g GitlabDriver) Execute(req *http.Request) (int, error) {
	var signature string
	if signature = req.Header.Get(GitlabTokenHeader); len(signature) == 0 {
		return http.StatusUnprocessableEntity, errors.New("gitlab webhook missing token")
	}
	event := req.Header.Get(GitlabWebhookHeader)
	if event != gitlabPushEvent && event != gitlabMREvent && event != gitlabTagEvent {
		return http.StatusUnprocessableEntity, fmt.Errorf("not trigger for event:%s", event)
	}

	pipelineID := req.URL.Query().Get("pipelineId")
	ns, name := ref.Parse(pipelineID)
	pipeline, err := g.PipelineLister.Get(ns, name)
	if err != nil {
		return http.StatusInternalServerError, err
	}
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return http.StatusUnprocessableEntity, err
	}

	if pipeline.Status.Token != signature {
		return http.StatusUnprocessableEntity, errors.New("gitlab webhook invalid token")
	}

	if pipeline.Status.PipelineState == "inactive" {
		return http.StatusUnavailableForLegalReasons, errors.New("pipeline is not active")
	}

	info := &model.BuildInfo{}
	if event == gitlabPushEvent {
		info, err = gitlabParsePushPayload(body)
		if err != nil {
			return http.StatusUnprocessableEntity, err
		}
	} else if event == gitlabMREvent {
		info, err = gitlabParseMergeRequestPayload(body)
		if err != nil {
			return http.StatusUnprocessableEntity, err
		}
	} else if event == gitlabTagEvent {
		info, err = gitlabParseTagPayload(body)
		if err != nil {
			return http.StatusUnprocessableEntity, err
		}
	}

	return validateAndGeneratePipelineExecution(g.PipelineExecutions, g.SourceCodeCredentials, g.SourceCodeCredentialLister, info, pipeline)
}

func gitlabParsePushPayload(raw []byte) (*model.BuildInfo, error) {
	payload := &gitlab.PushEvent{}
	if err := json.Unmarshal(raw, payload); err != nil {
		return nil, err
	}

	info := &model.BuildInfo{}
	if err := json.Unmarshal(raw, payload); err != nil {
		return nil, err
	}
	info.TriggerType = utils.TriggerTypeWebhook
	info.Event = utils.WebhookEventPush
	info.Commit = payload.After
	info.Branch = strings.TrimPrefix(payload.Ref, RefsBranchPrefix)
	info.Ref = payload.Ref
	info.HTMLLink = payload.Repository.HTTPURL
	lastCommit := payload.Commits[len(payload.Commits)-1]
	info.Message = lastCommit.Message
	info.Email = payload.UserEmail
	info.AvatarURL = payload.UserAvatar
	info.Author = payload.UserName
	info.Sender = payload.UserName

	return info, nil
}

func gitlabParseMergeRequestPayload(raw []byte) (*model.BuildInfo, error) {
	info := &model.BuildInfo{}
	payload := &gitlab.MergeEvent{}
	if err := json.Unmarshal(raw, payload); err != nil {
		return nil, err
	}

	action := payload.ObjectAttributes.Action
	oldRev := payload.ObjectAttributes.OldRev
	if action != gitlabActionOpen && action != gitlabActionUpdate {
		return nil, fmt.Errorf("no trigger for %s action", action)
	}
	if action == gitlabActionUpdate && oldRev == "" {
		return nil, fmt.Errorf("no trigger for metadata update action")
	}
	if payload.ObjectAttributes.State != gitlabStateOpen {
		return nil, fmt.Errorf("no trigger for closed merge requests")
	}

	info.TriggerType = utils.TriggerTypeWebhook
	info.Event = utils.WebhookEventPullRequest
	info.Branch = payload.ObjectAttributes.TargetBranch
	info.Ref = fmt.Sprintf("refs/merge-requests/%d/head", payload.ObjectAttributes.IID)
	info.HTMLLink = payload.ObjectAttributes.URL
	info.Title = payload.ObjectAttributes.Title
	info.Message = payload.ObjectAttributes.LastCommit.Message
	info.Commit = payload.ObjectAttributes.LastCommit.ID
	info.Author = payload.User.Name
	info.AvatarURL = payload.User.AvatarURL
	info.Email = payload.User.Email
	info.Sender = payload.User.Name

	return info, nil
}

func gitlabParseTagPayload(raw []byte) (*model.BuildInfo, error) {
	info := &model.BuildInfo{}
	payload := &gitlab.TagEvent{}
	if err := json.Unmarshal(raw, payload); err != nil {
		return nil, err
	}

	info.TriggerType = utils.TriggerTypeWebhook
	info.Event = utils.WebhookEventTag
	info.Ref = payload.Ref
	tag := strings.TrimPrefix(payload.Ref, RefsTagPrefix)
	info.Message = "tag " + tag
	info.Branch = tag
	info.Commit = payload.After
	info.Author = payload.UserName
	info.AvatarURL = payload.UserAvatar

	return info, nil
}
