package opnsense

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"runtime"
	"time"

	"github.com/go-kit/log"
	"github.com/go-kit/log/level"
)

// MaxRetries is the maximum number of retries
// when a request to the OPNsense API fails
const MaxRetries = 3

// EndpointName is the custom type for name of an endpoint definition
type EndpointName string

// EndpointPath is the custom type for url path of an endpoint definition
type EndpointPath string

// Client is an OPNsense API client
type Client struct {
	log              log.Logger
	baseURL          string
	key              string
	secret           string
	sslInsecure      bool
	endpoints        map[EndpointName]EndpointPath
	httpClient       *http.Client
	headers          map[string]string
	gatewayLossRegex *regexp.Regexp
	gatewayRTTRegex  *regexp.Regexp
}

// NewClient creates a new OPNsense API Client
func NewClient(protocol, adress, key, secret, userAgentVersion string, sslInsecure bool, log log.Logger) (Client, error) {

	sslPool, err := x509.SystemCertPool()

	if err != nil {
		return Client{}, errors.Join(fmt.Errorf("failed to load system cert pool"), err)
	}

	gatewayLossRegex, err := regexp.Compile(`\d\.\d %`)

	if err != nil {
		return Client{}, errors.Join(fmt.Errorf("failed to build regex for gatewayLoss calculation"), err)
	}

	gatewayRTTRegex, err := regexp.Compile(`\d+\.\d+ ms`)
	if err != nil {
		return Client{}, errors.Join(fmt.Errorf("failed to build regex for gatewayRTT calculation"), err)
	}
	client := Client{
		log:              log,
		baseURL:          fmt.Sprintf("%s://%s", protocol, adress),
		key:              key,
		secret:           secret,
		gatewayLossRegex: gatewayLossRegex,
		gatewayRTTRegex:  gatewayRTTRegex,
		endpoints: map[EndpointName]EndpointPath{
			"services":           "api/core/service/search",
			"protocolStatistics": "api/diagnostics/interface/getProtocolStatistics",
			"arp":                "api/diagnostics/interface/search_arp",
			"dhcpv4":             "api/dhcpv4/leases/searchLease",
			"openVPNInstances":   "api/openvpn/instances/search",
			"interfaces":         "api/diagnostics/traffic/interface",
			"systemInfo":         "widgets/api/get.php?load=system%2Ctemperature",
			"gatewaysStatus":     "api/routes/gateway/status",
			"unboundDNSStatus":   "api/unbound/diagnostics/stats",
			"cronJobs":           "api/cron/settings/searchJobs",
		},
		headers: map[string]string{
			"Accept":          "application/json",
			"User-Agent":      fmt.Sprintf("prometheus-opnsense-exporter/%s", userAgentVersion),
			"Accept-Encoding": "gzip, deflate, br",
		},
		sslInsecure: sslInsecure,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{
					InsecureSkipVerify: sslInsecure,
					RootCAs:            sslPool,
				},
				IdleConnTimeout:       90 * time.Second,
				TLSHandshakeTimeout:   1 * time.Second,
				ExpectContinueTimeout: 1 * time.Second,
				ForceAttemptHTTP2:     true,
				MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 1,
			},
		},
	}

	return client, nil
}

// Endpoints returns a map of all the endpoints
// that are called by the client.
func (c *Client) Endpoints() map[EndpointName]EndpointPath {
	return c.endpoints
}

// do sends a request to the OPNsense API.
// The response is unmarshalled
// into the responseStruc
func (c *Client) do(method string, path EndpointPath, body io.Reader, responseStruct any) *APICallError {

	url := fmt.Sprintf("%s/%s", c.baseURL, string(path))

	req, err := http.NewRequest(method, url, body)

	if err != nil {
		return &APICallError{
			Endpoint:   string(path),
			Message:    err.Error(),
			StatusCode: 0,
		}
	}

	req.SetBasicAuth(c.key, c.secret)

	for k, v := range c.headers {
		req.Header.Add(k, v)
	}

	if method == "POST" {
		req.Header.Add("Content-Type", "application/json;charset=utf-8")
	}

	level.Debug(c.log).
		Log("msg", "fetching data", "component", "opnsense-client", "url", url, "method", method)

	// Retry the request up to MaxRetries times
	for i := 0; i < MaxRetries; i++ {
		resp, err := c.httpClient.Do(req)
		if err != nil {
			level.Error(c.log).
				Log("msg", "failed to send request; retrying",
					"component", "opnsense-client",
					"err", err.Error())
			time.Sleep(25 * time.Millisecond)
			continue
		}

		body, err := io.ReadAll(resp.Body)

		if err != nil {
			return &APICallError{
				Endpoint:   string(path),
				Message:    fmt.Sprintf("failed to read response body: %s", err.Error()),
				StatusCode: resp.StatusCode,
			}
		}

		resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			err := json.Unmarshal(body, &responseStruct)
			if err != nil {
				fmt.Println(url)
				fmt.Println(string(body))
				return &APICallError{
					Endpoint:   string(path),
					Message:    fmt.Sprintf("failed to unmarshal response body: %s", err.Error()),
					StatusCode: resp.StatusCode,
				}
			}
			return nil
		} else {
			return &APICallError{
				Endpoint:   string(path),
				Message:    string(body),
				StatusCode: resp.StatusCode,
			}
		}

	}
	return &APICallError{
		Endpoint:   string(path),
		Message:    fmt.Sprintf("max retries of %d times reached", MaxRetries),
		StatusCode: 0,
	}
}
