package client

import (
	"errors"
	"fmt"
	"net/url"
	"time"

	"github.com/nirmata/kyverno/pkg/config"
	tls "github.com/nirmata/kyverno/pkg/tls"
	certificates "k8s.io/api/certificates/v1beta1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/rest"
)

// InitTLSPemPair Loads or creates PEM private key and TLS certificate for webhook server.
// Created pair is stored in cluster's secret.
// Returns struct with key/certificate pair.
func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.TlsPemPair, error) {
	logger := c.log
	certProps, err := c.GetTLSCertProps(configuration)
	if err != nil {
		return nil, err
	}
	tlsPair := c.ReadTlsPair(certProps)
	if tls.IsTLSPairShouldBeUpdated(tlsPair) {
		logger.Info("Generating new key/certificate pair for TLS")
		tlsPair, err = c.generateTLSPemPair(certProps, fqdncn)
		if err != nil {
			return nil, err
		}
		if err = c.WriteTlsPair(certProps, tlsPair); err != nil {
			return nil, fmt.Errorf("Unable to save TLS pair to the cluster: %v", err)
		}
		return tlsPair, nil
	}
	logger.Info("Using existing TLS key/certificate pair")
	return tlsPair, nil
}

//generateTlsPemPair Issues TLS certificate for webhook server using given PEM private key
// Returns signed and approved TLS certificate in PEM format
func (c *Client) generateTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) {
	privateKey, err := tls.TLSGeneratePrivateKey()
	if err != nil {
		return nil, err
	}

	certRequest, err := tls.CertificateGenerateRequest(privateKey, props, fqdncn)
	if err != nil {
		return nil, fmt.Errorf("Unable to create certificate request: %v", err)
	}

	certRequest, err = c.submitAndApproveCertificateRequest(certRequest)
	if err != nil {
		return nil, fmt.Errorf("Unable to submit and approve certificate request: %v", err)
	}

	tlsCert, err := c.fetchCertificateFromRequest(certRequest, 10)
	if err != nil {
		return nil, fmt.Errorf("Failed to configure a certificate for the Kyverno controller. A CA certificate is required to allow the Kubernetes API Server to communicate with Kyverno. You can either provide a certificate or configure your cluster to allow certificate signing. Please refer to https://github.com/nirmata/kyverno/installation.md.: %v", err)
	}

	return &tls.TlsPemPair{
		Certificate: tlsCert,
		PrivateKey:  tls.TLSPrivateKeyToPem(privateKey),
	}, nil
}

// Submits and approves certificate request, returns request which need to be fetched
func (c *Client) submitAndApproveCertificateRequest(req *certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) {
	logger := c.log.WithName("submitAndApproveCertificateRequest")
	certClient, err := c.GetCSRInterface()
	if err != nil {
		return nil, err
	}
	csrList, err := c.ListResource(CSRs, "", nil)
	if err != nil {
		return nil, fmt.Errorf("Unable to list existing certificate requests: %v", err)
	}

	for _, csr := range csrList.Items {
		if csr.GetName() == req.ObjectMeta.Name {
			err := c.DeleteResource(CSRs, "", csr.GetName(), false)
			if err != nil {
				return nil, fmt.Errorf("Unable to delete existing certificate request: %v", err)
			}
			logger.Info("Old certificate request is deleted")
			break
		}
	}

	unstrRes, err := c.CreateResource(CSRs, "", req, false)
	if err != nil {
		return nil, err
	}
	logger.Info("Certificate request created", "name", unstrRes.GetName())

	res, err := convertToCSR(unstrRes)
	if err != nil {
		return nil, err
	}
	res.Status.Conditions = append(res.Status.Conditions, certificates.CertificateSigningRequestCondition{
		Type:    certificates.CertificateApproved,
		Reason:  "NKP-Approve",
		Message: "This CSR was approved by Nirmata kyverno controller",
	})
	res, err = certClient.UpdateApproval(res)
	if err != nil {
		return nil, fmt.Errorf("Unable to approve certificate request: %v", err)
	}
	logger.Info("Certificate request is approved", "name", res.ObjectMeta.Name)

	return res, nil
}

// Fetches certificate from given request. Tries to obtain certificate for maxWaitSeconds
func (c *Client) fetchCertificateFromRequest(req *certificates.CertificateSigningRequest, maxWaitSeconds uint8) ([]byte, error) {
	// TODO: react of SIGINT and SIGTERM
	timeStart := time.Now()
	for time.Since(timeStart) < time.Duration(maxWaitSeconds)*time.Second {
		unstrR, err := c.GetResource(CSRs, "", req.ObjectMeta.Name)
		if err != nil {
			return nil, err
		}
		r, err := convertToCSR(unstrR)
		if err != nil {
			return nil, err
		}

		if r.Status.Certificate != nil {
			return r.Status.Certificate, nil
		}

		for _, condition := range r.Status.Conditions {
			if condition.Type == certificates.CertificateDenied {
				return nil, errors.New(condition.String())
			}
		}
	}
	return nil, fmt.Errorf("Cerificate fetch timeout is reached: %d seconds", maxWaitSeconds)
}

//ReadRootCASecret returns the RootCA from the pre-defined secret
func (c *Client) ReadRootCASecret() (result []byte) {
	logger := c.log.WithName("ReadRootCASecret")
	certProps, err := c.GetTLSCertProps(c.clientConfig)
	if err != nil {
		logger.Error(err, "failed to get TLS Cert Properties")
		return result
	}
	sname := generateRootCASecretName(certProps)
	stlsca, err := c.GetResource(Secrets, certProps.Namespace, sname)
	if err != nil {
		return result
	}
	tlsca, err := convertToSecret(stlsca)
	if err != nil {
		logger.Error(err, "failed to convert secret", "name", sname, "namespace", certProps.Namespace)
		return result
	}

	result = tlsca.Data[rootCAKey]
	if len(result) == 0 {
		logger.Info("root CA certificate not found in secret", "name", tlsca.Name, "namespace", certProps.Namespace)
		return result
	}
	logger.V(4).Info("using CA bundle defined in secret to validate the webhook's server certificate", "name", tlsca.Name, "namespace", certProps.Namespace)
	return result
}

const selfSignedAnnotation string = "self-signed-cert"
const rootCAKey string = "rootCA.crt"

//ReadTlsPair Reads the pair of TLS certificate and key from the specified secret.
func (c *Client) ReadTlsPair(props tls.TlsCertificateProps) *tls.TlsPemPair {
	logger := c.log.WithName("ReadTlsPair")
	sname := generateTLSPairSecretName(props)
	unstrSecret, err := c.GetResource(Secrets, props.Namespace, sname)
	if err != nil {
		logger.Error(err, "Failed to get secret", "name", sname, "namespace", props.Namespace)
		return nil
	}

	// If secret contains annotation 'self-signed-cert', then it's created using helper scripts to setup self-signed certificates.
	// As the root CA used to sign the certificate is required for webhook cnofiguration, check if the corresponding secret is created
	annotations := unstrSecret.GetAnnotations()
	if _, ok := annotations[selfSignedAnnotation]; ok {
		sname := generateRootCASecretName(props)
		_, err := c.GetResource(Secrets, props.Namespace, sname)
		if err != nil {
			logger.Error(err, "Root CA secret is required while using self-signed certificates TLS pair, defaulting to generating new TLS pair", "name", sname, "namespace", props.Namespace)
			return nil
		}
	}
	secret, err := convertToSecret(unstrSecret)
	if err != nil {
		return nil
	}
	pemPair := tls.TlsPemPair{
		Certificate: secret.Data[v1.TLSCertKey],
		PrivateKey:  secret.Data[v1.TLSPrivateKeyKey],
	}
	if len(pemPair.Certificate) == 0 {
		logger.Info("TLS Certificate not found in secret", "name", sname, "namespace", props.Namespace)
		return nil
	}
	if len(pemPair.PrivateKey) == 0 {
		logger.Info("TLS PrivateKey not found in secret", "name", sname, "namespace", props.Namespace)
		return nil
	}
	return &pemPair
}

//WriteTlsPair Writes the pair of TLS certificate and key to the specified secret.
// Updates existing secret or creates new one.
func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPemPair) error {
	logger := c.log.WithName("WriteTlsPair")
	name := generateTLSPairSecretName(props)
	_, err := c.GetResource(Secrets, props.Namespace, name)
	if err != nil {
		secret := &v1.Secret{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Secret",
				APIVersion: "v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: props.Namespace,
			},
			Data: map[string][]byte{
				v1.TLSCertKey:       pemPair.Certificate,
				v1.TLSPrivateKeyKey: pemPair.PrivateKey,
			},
			Type: v1.SecretTypeTLS,
		}

		_, err := c.CreateResource(Secrets, props.Namespace, secret, false)
		if err == nil {
			logger.Info("secret created", "name", name, "namespace", props.Namespace)
		}
		return err
	}
	secret := v1.Secret{}

	if secret.Data == nil {
		secret.Data = make(map[string][]byte)
	}
	secret.Data[v1.TLSCertKey] = pemPair.Certificate
	secret.Data[v1.TLSPrivateKeyKey] = pemPair.PrivateKey

	_, err = c.UpdateResource(Secrets, props.Namespace, secret, false)
	if err != nil {
		return err
	}
	logger.Info("secret updated", "name", name, "namespace", props.Namespace)
	return nil
}

func generateTLSPairSecretName(props tls.TlsCertificateProps) string {
	return tls.GenerateInClusterServiceName(props) + ".kyverno-tls-pair"
}

func generateRootCASecretName(props tls.TlsCertificateProps) string {
	return tls.GenerateInClusterServiceName(props) + ".kyverno-tls-ca"
}

//GetTLSCertProps provides the TLS Certificate Properties
func (c *Client) GetTLSCertProps(configuration *rest.Config) (certProps tls.TlsCertificateProps, err error) {
	apiServerURL, err := url.Parse(configuration.Host)
	if err != nil {
		return certProps, err
	}
	certProps = tls.TlsCertificateProps{
		Service:       config.WebhookServiceName,
		Namespace:     config.KubePolicyNamespace,
		ApiServerHost: apiServerURL.Hostname(),
	}
	return certProps, nil
}
