package provisioner

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"text/template"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/certificates/templates"
	"github.com/smallstep/certificates/webhook"
	"go.step.sm/linkedca"
)

var ErrWebhookDenied = errors.New("webhook server did not allow request")

type WebhookSetter interface {
	SetWebhook(string, any)
}

type WebhookController struct {
	client       *http.Client
	webhooks     []*Webhook
	certType     linkedca.Webhook_CertType
	TemplateData WebhookSetter
}

// Enrich fetches data from remote servers and adds returned data to the
// templateData
func (wc *WebhookController) Enrich(req *webhook.RequestBody) error {
	if wc == nil {
		return nil
	}
	for _, wh := range wc.webhooks {
		if wh.Kind != linkedca.Webhook_ENRICHING.String() {
			continue
		}
		if !wc.isCertTypeOK(wh) {
			continue
		}
		resp, err := wh.Do(wc.client, req, wc.TemplateData)
		if err != nil {
			return err
		}
		if !resp.Allow {
			return ErrWebhookDenied
		}
		wc.TemplateData.SetWebhook(wh.Name, resp.Data)
	}
	return nil
}

// Authorize checks that all remote servers allow the request
func (wc *WebhookController) Authorize(req *webhook.RequestBody) error {
	if wc == nil {
		return nil
	}
	for _, wh := range wc.webhooks {
		if wh.Kind != linkedca.Webhook_AUTHORIZING.String() {
			continue
		}
		if !wc.isCertTypeOK(wh) {
			continue
		}
		resp, err := wh.Do(wc.client, req, wc.TemplateData)
		if err != nil {
			return err
		}
		if !resp.Allow {
			return ErrWebhookDenied
		}
	}
	return nil
}

func (wc *WebhookController) isCertTypeOK(wh *Webhook) bool {
	if wc.certType == linkedca.Webhook_ALL {
		return true
	}
	if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" {
		return true
	}
	return wc.certType.String() == wh.CertType
}

type Webhook struct {
	ID                   string `json:"id"`
	Name                 string `json:"name"`
	URL                  string `json:"url"`
	Kind                 string `json:"kind"`
	DisableTLSClientAuth bool   `json:"disableTLSClientAuth,omitempty"`
	CertType             string `json:"certType"`
	Secret               string `json:"-"`
	BearerToken          string `json:"-"`
	BasicAuth            struct {
		Username string
		Password string
	} `json:"-"`
}

func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {
	tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL)
	if err != nil {
		return nil, err
	}
	buf := &bytes.Buffer{}
	if err := tmpl.Execute(buf, data); err != nil {
		return nil, err
	}
	url := buf.String()

	/*
		Sending the token to the webhook server is a security risk. A K8sSA
		token can be reused multiple times. The webhook can misuse it to get
		fake certificates. A webhook can misuse any other token to get its own
		certificate before responding.
		switch tmpl := data.(type) {
		case x509util.TemplateData:
			reqBody.Token = tmpl[x509util.TokenKey]
		case sshutil.TemplateData:
			reqBody.Token = tmpl[sshutil.TokenKey]
		}
	*/
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	reqBody.Timestamp = time.Now()

	reqBytes, err := json.Marshal(reqBody)
	if err != nil {
		return nil, err
	}

	retries := 1
retry:

	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBytes))
	if err != nil {
		return nil, err
	}

	secret, err := base64.StdEncoding.DecodeString(w.Secret)
	if err != nil {
		return nil, err
	}
	sig := hmac.New(sha256.New, secret).Sum(reqBytes)
	req.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig))
	req.Header.Set("X-Smallstep-Webhook-ID", w.ID)

	if w.BearerToken != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken))
	} else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" {
		req.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password)
	}

	if w.DisableTLSClientAuth {
		transport, ok := client.Transport.(*http.Transport)
		if !ok {
			return nil, errors.New("client transport is not a *http.Transport")
		}
		transport = transport.Clone()
		tlsConfig := transport.TLSClientConfig.Clone()
		tlsConfig.GetClientCertificate = nil
		tlsConfig.Certificates = nil
		transport.TLSClientConfig = tlsConfig
		client = &http.Client{
			Transport: transport,
		}
	}
	resp, err := client.Do(req)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, err
		} else if retries > 0 {
			retries--
			time.Sleep(time.Second)
			goto retry
		}
		return nil, err
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			log.Printf("Failed to close body of response from %s", w.URL)
		}
	}()
	if resp.StatusCode >= 500 && retries > 0 {
		retries--
		time.Sleep(time.Second)
		goto retry
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("Webhook server responded with %d", resp.StatusCode)
	}

	respBody := &webhook.ResponseBody{}
	if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
		return nil, err
	}

	return respBody, nil
}
