package securityevents

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"
)

const (
	defaultKubernetesAPIServer = "https://kubernetes.default.svc"
	defaultServiceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token"
	defaultServiceAccountCA    = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)

var kubernetesDNSNamePattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$`)

type KubernetesSecretStoreConfig struct {
	Namespace  string
	SecretName string
	APIServer  string
	TokenFile  string
	CAFile     string
	HTTPClient *http.Client
}

// KubernetesSecretStore keeps all generated Push Bearers as keys in one
// pre-provisioned Secret. It intentionally has no permission to create or
// delete Kubernetes Secret resources.
type KubernetesSecretStore struct {
	endpoint  string
	tokenFile string
	client    *http.Client
}

func NewKubernetesSecretStore(config KubernetesSecretStoreConfig) (*KubernetesSecretStore, error) {
	if !validKubernetesDNSName(config.Namespace) || !validKubernetesDNSName(config.SecretName) {
		return nil, errors.New("Kubernetes security event Secret namespace or name is invalid")
	}
	if config.APIServer == "" {
		config.APIServer = defaultKubernetesAPIServer
	}
	if config.TokenFile == "" {
		config.TokenFile = defaultServiceAccountToken
	}
	parsed, err := url.Parse(strings.TrimRight(config.APIServer, "/"))
	if err != nil || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" ||
		(parsed.Scheme != "https" && config.HTTPClient == nil) {
		return nil, errors.New("Kubernetes API server URL is invalid")
	}
	client := config.HTTPClient
	if client == nil {
		if config.CAFile == "" {
			config.CAFile = defaultServiceAccountCA
		}
		certificate, readErr := os.ReadFile(config.CAFile)
		if readErr != nil {
			return nil, fmt.Errorf("read Kubernetes service account CA: %w", readErr)
		}
		roots := x509.NewCertPool()
		if !roots.AppendCertsFromPEM(certificate) {
			return nil, errors.New("Kubernetes service account CA is invalid")
		}
		transport := http.DefaultTransport.(*http.Transport).Clone()
		transport.Proxy = nil
		transport.DisableCompression = true
		transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots}
		client = &http.Client{Timeout: 10 * time.Second, Transport: transport,
			CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	}
	endpoint := fmt.Sprintf("%s/api/v1/namespaces/%s/secrets/%s", strings.TrimRight(config.APIServer, "/"), config.Namespace, config.SecretName)
	return &KubernetesSecretStore{endpoint: endpoint, tokenFile: config.TokenFile, client: client}, nil
}

func (s *KubernetesSecretStore) Put(ctx context.Context, reference string, value []byte) error {
	if !secretReferencePattern.MatchString(reference) {
		return errors.New("security event secret reference is invalid")
	}
	if len(value) < 32 || len(value) > 4096 {
		return errors.New("security event secret length is invalid")
	}
	return s.patch(ctx, map[string]any{reference: base64.StdEncoding.EncodeToString(value)})
}

func (s *KubernetesSecretStore) Get(ctx context.Context, reference string) ([]byte, error) {
	if !secretReferencePattern.MatchString(reference) {
		return nil, errors.New("security event secret reference is invalid")
	}
	request, err := s.request(ctx, http.MethodGet, nil)
	if err != nil {
		return nil, err
	}
	response, err := s.client.Do(request)
	if err != nil {
		return nil, fmt.Errorf("read Kubernetes security event Secret: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusNotFound {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 64*1024))
		return nil, ErrSecretNotFound
	}
	if response.StatusCode != http.StatusOK {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 64*1024))
		return nil, fmt.Errorf("Kubernetes security event Secret returned HTTP %d", response.StatusCode)
	}
	var payload struct {
		Data map[string]string `json:"data"`
	}
	if err := decodeLimitedJSON(response.Body, &payload); err != nil {
		return nil, err
	}
	encoded, ok := payload.Data[reference]
	if !ok {
		return nil, ErrSecretNotFound
	}
	value, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil || len(value) < 32 || len(value) > 4096 {
		clear(value)
		return nil, errors.New("Kubernetes security event Secret value is invalid")
	}
	return value, nil
}

func (s *KubernetesSecretStore) Delete(ctx context.Context, reference string) error {
	if !secretReferencePattern.MatchString(reference) {
		return errors.New("security event secret reference is invalid")
	}
	return s.patch(ctx, map[string]any{reference: nil})
}

func (s *KubernetesSecretStore) patch(ctx context.Context, data map[string]any) error {
	payload, err := json.Marshal(map[string]any{"data": data})
	if err != nil {
		return err
	}
	request, err := s.request(ctx, http.MethodPatch, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	request.Header.Set("Content-Type", "application/merge-patch+json")
	response, err := s.client.Do(request)
	if err != nil {
		return fmt.Errorf("update Kubernetes security event Secret: %w", err)
	}
	defer response.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 64*1024))
	if response.StatusCode == http.StatusNotFound {
		return errors.New("Kubernetes security event Secret is not provisioned")
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return fmt.Errorf("Kubernetes security event Secret returned HTTP %d", response.StatusCode)
	}
	return nil
}

func (s *KubernetesSecretStore) request(ctx context.Context, method string, body io.Reader) (*http.Request, error) {
	token, err := os.ReadFile(s.tokenFile)
	if err != nil || strings.TrimSpace(string(token)) == "" {
		clear(token)
		return nil, errors.New("Kubernetes service account token is unavailable")
	}
	request, err := http.NewRequestWithContext(ctx, method, s.endpoint, body)
	if err != nil {
		clear(token)
		return nil, err
	}
	request.Header.Set("Authorization", "Bearer "+strings.TrimSpace(string(token)))
	request.Header.Set("Accept", "application/json")
	clear(token)
	return request, nil
}

func validKubernetesDNSName(value string) bool {
	return len(value) > 0 && len(value) <= 253 && kubernetesDNSNamePattern.MatchString(value)
}
