// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package webhooks

import (
	"fmt"
	"strings"
	"net/http"
	"bytes"
	"time"

	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"

	"code.vikunja.io/api/pkg/log"
	"code.vikunja.io/api/pkg/config"

	"github.com/ThreeDotsLabs/watermill/message"
)

const (
	defaultTimeout = 5 * time.Second
	ctHeader = "Content-Type"
	ctValue = "application/json"
	hmacHeader = "X-Signature"
)

type FilteringFunction func(string) (bool)
type WebhookCallFunction func(string, *message.Message) (error)


// Single configuration entry
type SingleConfEntry struct {
	Events        []string `json:"events"`
	URL             string `json:"url"`
	Secret          string `json:"secret"`  
	Timeout         int    `json:"timeout"`
}


type WebhookRuntimeConfig struct {
	FilterFunc       FilteringFunction
	ExecuteFunc      WebhookCallFunction
}



func getWebhookFilterFunc(cfg SingleConfEntry) FilteringFunction {
	return func(topic string) (is_interesting bool) {
		for _, filter := range cfg.Events {
			log.Debugf("Match pattern:'%s' agains:'%s'", filter, topic)
			if filter == "*" {
				log.Debugf(" '*' == Always match ")
				return true
			} else {
				if strings.HasPrefix(topic, filter) {
					log.Debugf("Positive match [%s] -> [%s]", filter, topic)
					return true
				}
			}
		}
		log.Debugf("No match for [%s]", topic)
		return false
	}
}

func getWebhookCallFunc(cfg SingleConfEntry) WebhookCallFunction {
	return func(topic string, msg *message.Message) (error) {
		endpointURL := cfg.URL
		hmacKey := cfg.Secret
		timeout := defaultTimeout
		if cfg.Timeout > 0 {
			timeout = time.Second * time.Duration(cfg.Timeout)
		}	

		log.Debugf("Webhook Call : %s (key=%s)", endpointURL, hmacKey)
		webhookUrl := fmt.Sprintf("%s%s", endpointURL, topic)
		
		rawData := msg.Payload

		req, err := http.NewRequest( http.MethodPost, webhookUrl,  bytes.NewBuffer(rawData))
		
		if err != nil {
			return err
		}

		client := &http.Client{
			Timeout: timeout,
		}
	
		req.Header.Set(ctHeader, ctValue)

		if len(hmacKey) > 1 {
			signature := GenerateHMAC(rawData, hmacKey)
			req.Header.Set(hmacHeader, signature)
		}

		_, err = client.Do(req)

		if err != nil {
			log.Debugf("Webhook failed : %s , +%v", webhookUrl , err)
			return err
		}

		log.Debugf("Webhook success : %s ", webhookUrl )
		return nil
	}
}



func GenerateHMAC(data []byte, key string) string {
	h := hmac.New(sha256.New, []byte(key))
	h.Write(data)
	return hex.EncodeToString(h.Sum(nil))
}



// Process config and prepare mapping
func ProcessConfig() ([]WebhookRuntimeConfig)  {
	
	var items []SingleConfEntry

	config.WebhooksConf.GetUnmarshaled(&items)

	runtime := make([]WebhookRuntimeConfig, len(items))

	log.Debugf("Webhook config items : %+v\n", items)

	for i, item := range items {
		runtime[i].FilterFunc  = getWebhookFilterFunc(item)
		runtime[i].ExecuteFunc = getWebhookCallFunc(item)
	}

	return runtime
}
