package bitbucket

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

	"gopkg.in/go-playground/webhooks.v3"
)

// Webhook instance contains all methods needed to process events
type Webhook struct {
	provider   webhooks.Provider
	uuid       string
	eventFuncs map[Event]webhooks.ProcessPayloadFunc
}

// Config defines the configuration to create a new Bitbucket Webhook instance
type Config struct {
	UUID string
}

// Event defines a Bitbucket hook event type
type Event string

// Bitbucket hook types
const (
	RepoPushEvent                  Event = "repo:push"
	RepoForkEvent                  Event = "repo:fork"
	RepoUpdatedEvent               Event = "repo:updated"
	RepoCommitCommentCreatedEvent  Event = "repo:commit_comment_created"
	RepoCommitStatusCreatedEvent   Event = "repo:commit_status_created"
	RepoCommitStatusUpdatedEvent   Event = "repo:commit_status_updated"
	IssueCreatedEvent              Event = "issue:created"
	IssueUpdatedEvent              Event = "issue:updated"
	IssueCommentCreatedEvent       Event = "issue:comment_created"
	PullRequestCreatedEvent        Event = "pullrequest:created"
	PullRequestUpdatedEvent        Event = "pullrequest:updated"
	PullRequestApprovedEvent       Event = "pullrequest:approved"
	PullRequestUnapprovedEvent     Event = "pullrequest:unapproved"
	PullRequestMergedEvent         Event = "pullrequest:fulfilled"
	PullRequestDeclinedEvent       Event = "pullrequest:rejected"
	PullRequestCommentCreatedEvent Event = "pullrequest:comment_created"
	PullRequestCommentUpdatedEvent Event = "pullrequest:comment_updated"
	PullRequestCommentDeletedEvent Event = "pull_request:comment_deleted"
)

// New creates and returns a WebHook instance denoted by the Provider type
func New(config *Config) *Webhook {
	return &Webhook{
		provider:   webhooks.Bitbucket,
		uuid:       config.UUID,
		eventFuncs: map[Event]webhooks.ProcessPayloadFunc{},
	}
}

// Provider returns the current hooks provider ID
func (hook Webhook) Provider() webhooks.Provider {
	return hook.provider
}

// RegisterEvents registers the function to call when the specified event(s) are encountered
func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) {

	for _, event := range events {
		hook.eventFuncs[event] = fn
	}
}

// ParsePayload parses and verifies the payload and fires off the mapped function, if it exists.
func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) {
	webhooks.DefaultLog.Info("Parsing Payload...")

	uuid := r.Header.Get("X-Hook-UUID")
	if uuid == "" {
		webhooks.DefaultLog.Error("Missing X-Hook-UUID Header")
		http.Error(w, "400 Bad Request - Missing X-Hook-UUID Header", http.StatusBadRequest)
		return
	}
	webhooks.DefaultLog.Debug(fmt.Sprintf("X-Hook-UUID:%s", uuid))

	if uuid != hook.uuid {
		webhooks.DefaultLog.Error(fmt.Sprintf("X-Hook-UUID does not match configured uuid of %s", hook.uuid))
		http.Error(w, "403 Forbidden - X-Hook-UUID does not match", http.StatusForbidden)
		return
	}

	event := r.Header.Get("X-Event-Key")
	if event == "" {
		webhooks.DefaultLog.Error("Missing X-Event-Key Header")
		http.Error(w, "400 Bad Request - Missing X-Event-Key Header", http.StatusBadRequest)
		return
	}
	webhooks.DefaultLog.Debug(fmt.Sprintf("X-Event-Key:%s", event))

	bitbucketEvent := Event(event)

	fn, ok := hook.eventFuncs[bitbucketEvent]
	// if no event registered
	if !ok {
		webhooks.DefaultLog.Info(fmt.Sprintf("Webhook Event %s not registered, it is recommended to setup only events in bitbucket that will be registered in the webhook to avoid unnecessary traffic and reduce potential attack vectors.", event))
		return
	}

	payload, err := ioutil.ReadAll(r.Body)
	if err != nil || len(payload) == 0 {
		webhooks.DefaultLog.Error("Issue reading Payload")
		http.Error(w, "Issue reading Payload", http.StatusInternalServerError)
		return
	}
	webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload)))
	hd := webhooks.Header(r.Header)

	switch bitbucketEvent {
	case RepoPushEvent:
		var pl RepoPushPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case RepoForkEvent:
		var pl RepoForkPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case RepoUpdatedEvent:
		var pl RepoUpdatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case RepoCommitCommentCreatedEvent:
		var pl RepoCommitCommentCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case RepoCommitStatusCreatedEvent:
		var pl RepoCommitStatusCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case RepoCommitStatusUpdatedEvent:
		var pl RepoCommitStatusUpdatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case IssueCreatedEvent:
		var pl IssueCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case IssueUpdatedEvent:
		var pl IssueUpdatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case IssueCommentCreatedEvent:
		var pl IssueCommentCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestCreatedEvent:
		var pl PullRequestCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestUpdatedEvent:
		var pl PullRequestUpdatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestApprovedEvent:
		var pl PullRequestApprovedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestUnapprovedEvent:
		var pl PullRequestUnapprovedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestMergedEvent:
		var pl PullRequestMergedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestDeclinedEvent:
		var pl PullRequestDeclinedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestCommentCreatedEvent:
		var pl PullRequestCommentCreatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestCommentUpdatedEvent:
		var pl PullRequestCommentUpdatedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	case PullRequestCommentDeletedEvent:
		var pl PullRequestCommentDeletedPayload
		json.Unmarshal([]byte(payload), &pl)
		hook.runProcessPayloadFunc(fn, pl, hd)
	}
}

func (hook Webhook) runProcessPayloadFunc(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) {
	go func(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) {
		fn(results, header)
	}(fn, results, header)
}
