package splunk

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"net/http"
	"time"

	"github.com/pkg/errors"
	"github.com/projectdiscovery/nuclei/v3/pkg/output"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
	"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
	"github.com/projectdiscovery/retryablehttp-go"
	"github.com/projectdiscovery/useragent"
)

// Options contains necessary options required for splunk communication
type Options struct {
	// Host is the hostname and port of the splunk instance
	Host string `yaml:"host" validate:"required"`
	Port int    `yaml:"port" validate:"gte=0,lte=65535"`
	// SSL (optional) enables ssl for splunk connection
	SSL bool `yaml:"ssl"`
	// SSLVerification (optional) disables SSL verification for splunk
	SSLVerification bool `yaml:"ssl-verification"`
	// Token for HEC instance
	Token     string `yaml:"token"  validate:"required"`
	IndexName string `yaml:"index-name"  validate:"required"`

	HttpClient  *retryablehttp.Client `yaml:"-"`
	ExecutionId string                `yaml:"-"`
}

type data struct {
	Event *output.ResultEvent `json:"event"`
}

// Exporter type for splunk
type Exporter struct {
	url            string
	authentication string
	splunk         *http.Client
}

// New creates and returns a new exporter for splunk
func New(option *Options) (*Exporter, error) {
	var ei *Exporter

	dialers := protocolstate.GetDialersWithId(option.ExecutionId)
	if dialers == nil {
		return nil, fmt.Errorf("dialers not initialized for %s", option.ExecutionId)
	}

	var client *http.Client
	if option.HttpClient != nil {
		client = option.HttpClient.HTTPClient
	} else {
		client = &http.Client{
			Timeout: 5 * time.Second,
			Transport: &http.Transport{
				MaxIdleConns:        10,
				MaxIdleConnsPerHost: 10,
				DialContext:         dialers.Fastdialer.Dial,
				DialTLSContext:      dialers.Fastdialer.DialTLS,
				TLSClientConfig:     &tls.Config{InsecureSkipVerify: option.SSLVerification},
			},
		}
	}

	// preparing url for splunk
	scheme := "http://"
	if option.SSL {
		scheme = "https://"
	}

	// Authentication header for HEC
	authentication := "Splunk " + option.Token

	// add HEC endpoint, index, source, sourcetype
	addr := option.Host
	if option.Port > 0 {
		addr = net.JoinHostPort(addr, fmt.Sprint(option.Port))
	}
	base_url := fmt.Sprintf("%s%s", scheme, addr)
	sourcetype := "nuclei:splunk-hec:exporter:json"
	url := fmt.Sprintf("%s/services/collector/event?index=%s&sourcetype=%s&source=%s", base_url, option.IndexName, sourcetype, base_url)

	ei = &Exporter{
		url:            url,
		authentication: authentication,
		splunk:         client,
	}
	return ei, nil
}

// Export exports a passed result event to Splunk
func (exporter *Exporter) Export(event *output.ResultEvent) error {
	// creating a request
	req, err := http.NewRequest(http.MethodPost, exporter.url, nil)
	if err != nil {
		return errors.Wrap(err, "could not make request")
	}
	if len(exporter.authentication) > 0 {
		req.Header.Add("Authorization", exporter.authentication)
	}
	userAgent := useragent.PickRandom()
	req.Header.Set("User-Agent", userAgent.Raw)
	req.Header.Add("Content-Type", "application/json")

	d := data{Event: event}
	b, err := json.Marshal(&d)
	if err != nil {
		return err
	}
	req.Body = io.NopCloser(bytes.NewReader(b))

	res, err := exporter.splunk.Do(req)
	if err != nil {
		return err
	}

	b, err = io.ReadAll(res.Body)
	if err != nil {
		return errors.New(err.Error() + "error thrown by splunk " + string(b))
	}
	if res.StatusCode >= http.StatusMultipleChoices {
		return errors.New("splunk responded with an error: " + string(b))
	}
	return nil
}

// Close closes the exporter after operation
func (exporter *Exporter) Close() error {
	return nil
}
