package conf

import (
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/base64"
	"errors"
	"fmt"
	"math/big"
	"net"
	"net/url"
	"time"
)

// SAMLConfiguration holds configuration for native SAML support.
type SAMLConfiguration struct {
	Enabled                  bool          `json:"enabled"`
	PrivateKey               string        `json:"-" split_words:"true"`
	AllowEncryptedAssertions bool          `json:"allow_encrypted_assertions" split_words:"true"`
	RelayStateValidityPeriod time.Duration `json:"relay_state_validity_period" split_words:"true"`

	RSAPrivateKey *rsa.PrivateKey   `json:"-"`
	RSAPublicKey  *rsa.PublicKey    `json:"-"`
	Certificate   *x509.Certificate `json:"-"`

	ExternalURL string `json:"external_url,omitempty" split_words:"true"`

	RateLimitAssertion float64 `default:"15" split_words:"true"`
}

func (c *SAMLConfiguration) Validate() error {
	if c.Enabled {
		bytes, err := base64.StdEncoding.DecodeString(c.PrivateKey)
		if err != nil {
			return errors.New("SAML private key not in standard Base64 format")
		}

		privateKey, err := x509.ParsePKCS1PrivateKey(bytes)
		if err != nil {
			return errors.New("SAML private key not in PKCS#1 format")
		}

		err = privateKey.Validate()
		if err != nil {
			return errors.New("SAML private key is not valid")
		}

		if privateKey.E != 0x10001 {
			return errors.New("SAML private key should use the 65537 (0x10001) RSA public exponent")
		}

		if privateKey.N.BitLen() < 2048 {
			return errors.New("SAML private key must be at least RSA 2048")
		}

		if c.RelayStateValidityPeriod < 0 {
			return errors.New("SAML RelayState validity period should be a positive duration")
		}

		if c.ExternalURL != "" {
			_, err := url.ParseRequestURI(c.ExternalURL)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// PopulateFields fills the configuration details based off the provided
// parameters.
func (c *SAMLConfiguration) PopulateFields(externalURL string) error {
	// errors are intentionally ignored since they should have been handled
	// within #Validate()
	bytes, _ := base64.StdEncoding.DecodeString(c.PrivateKey)
	privateKey, _ := x509.ParsePKCS1PrivateKey(bytes)

	c.RSAPrivateKey = privateKey
	c.RSAPublicKey = privateKey.Public().(*rsa.PublicKey)

	parsedURL, err := url.ParseRequestURI(externalURL)
	if err != nil {
		return fmt.Errorf("saml: unable to parse external URL for SAML, check API_EXTERNAL_URL: %w", err)
	}

	host := ""
	host, _, err = net.SplitHostPort(parsedURL.Host)
	if err != nil {
		host = parsedURL.Host
	}

	// SAML does not care much about the contents of the certificate, it
	// only uses it as a vessel for the public key; therefore we set these
	// fixed values.
	// Please avoid modifying or adding new values to this template as they
	// will change the exposed SAML certificate, requiring users of
	// GoTrue to re-establish a connection between their Identity Provider
	// and their running GoTrue instances.
	certTemplate := &x509.Certificate{
		SerialNumber: big.NewInt(0),
		IsCA:         false,
		DNSNames: []string{
			"_samlsp." + host,
		},
		KeyUsage:  x509.KeyUsageDigitalSignature,
		NotBefore: time.UnixMilli(0).UTC(),
		NotAfter:  time.UnixMilli(0).UTC().AddDate(200, 0, 0),
		Subject: pkix.Name{
			CommonName: "SAML 2.0 Certificate for " + host,
		},
	}

	if c.AllowEncryptedAssertions {
		certTemplate.KeyUsage = certTemplate.KeyUsage | x509.KeyUsageDataEncipherment
	}

	certDer, err := x509.CreateCertificate(nil, certTemplate, certTemplate, c.RSAPublicKey, c.RSAPrivateKey)
	if err != nil {
		return err
	}

	cert, err := x509.ParseCertificate(certDer)
	if err != nil {
		return err
	}

	c.Certificate = cert

	if c.RelayStateValidityPeriod == 0 {
		c.RelayStateValidityPeriod = 2 * time.Minute
	}

	return nil
}
