package proxy

import (
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/jinzhu/copier"
	"github.com/justinas/alice"
	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
	"github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
	log "github.com/sirupsen/logrus"
	"goauthentik.io/outpost/pkg/client/crypto"
	"goauthentik.io/outpost/pkg/models"
)

type providerBundle struct {
	http.Handler

	s     *Server
	proxy *OAuthProxy
	Host  string

	cert *tls.Certificate

	log *log.Entry
}

func intToPointer(i int) *int {
	return &i
}

func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *options.Options {
	externalHost, err := url.Parse(*provider.ExternalHost)
	if err != nil {
		log.WithError(err).Warning("Failed to parse URL, skipping provider")
		return nil
	}
	providerOpts := &options.Options{}
	err = copier.Copy(&providerOpts, getCommonOptions())
	if err != nil {
		log.WithError(err).Warning("Failed to copy options, skipping provider")
		return nil
	}
	providerOpts.ClientID = provider.ClientID
	providerOpts.ClientSecret = provider.ClientSecret

	providerOpts.Cookie.Secret = provider.CookieSecret
	providerOpts.Cookie.Secure = externalHost.Scheme == "https"

	providerOpts.SkipOIDCDiscovery = true
	providerOpts.OIDCIssuerURL = *provider.OidcConfiguration.Issuer
	providerOpts.LoginURL = *provider.OidcConfiguration.AuthorizationEndpoint
	providerOpts.RedeemURL = *provider.OidcConfiguration.TokenEndpoint
	providerOpts.OIDCJwksURL = *provider.OidcConfiguration.JwksURI
	providerOpts.ProfileURL = *provider.OidcConfiguration.UserinfoEndpoint

	if provider.SkipPathRegex != "" {
		skipRegexes := strings.Split(provider.SkipPathRegex, "\n")
		providerOpts.SkipAuthRegex = skipRegexes
	}

	if provider.ForwardAuthMode {
		providerOpts.UpstreamServers = []options.Upstream{
			{
				ID:         "static",
				Static:     true,
				StaticCode: intToPointer(202),
				Path:       "/",
			},
		}
	} else {
		providerOpts.UpstreamServers = []options.Upstream{
			{
				ID:                    "default",
				URI:                   provider.InternalHost,
				Path:                  "/",
				InsecureSkipTLSVerify: provider.InternalHostSslValidation,
			},
		}
	}

	if provider.Certificate != nil {
		pb.log.WithField("provider", provider.ClientID).Debug("Enabling TLS")
		cert, err := pb.s.ak.Client.Crypto.CryptoCertificatekeypairsViewCertificate(&crypto.CryptoCertificatekeypairsViewCertificateParams{
			Context: context.Background(),
			KpUUID:  *provider.Certificate,
		}, pb.s.ak.Auth)
		if err != nil {
			pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch certificate")
			return providerOpts
		}
		key, err := pb.s.ak.Client.Crypto.CryptoCertificatekeypairsViewPrivateKey(&crypto.CryptoCertificatekeypairsViewPrivateKeyParams{
			Context: context.Background(),
			KpUUID:  *provider.Certificate,
		}, pb.s.ak.Auth)
		if err != nil {
			pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch private key")
			return providerOpts
		}

		x509cert, err := tls.X509KeyPair([]byte(cert.Payload.Data), []byte(key.Payload.Data))
		if err != nil {
			pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate")
			return providerOpts
		}
		pb.cert = &x509cert
		pb.log.WithField("provider", provider.ClientID).Debug("Loaded certificates")
	}
	return providerOpts
}

func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) {
	opts := pb.prepareOpts(provider)

	chain := alice.New()

	if opts.ForceHTTPS {
		_, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress)
		if err != nil {
			log.Fatalf("FATAL: invalid HTTPS address %q: %v", opts.HTTPAddress, err)
		}
		chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort))
	}

	healthCheckPaths := []string{opts.PingPath}
	healthCheckUserAgents := []string{opts.PingUserAgent}
	if opts.GCPHealthChecks {
		healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
		healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
	}

	// To silence logging of health checks, register the health check handler before
	// the logging handler
	if opts.Logging.SilencePing {
		chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler)
	} else {
		chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents))
	}

	err := validation.Validate(opts)
	if err != nil {
		log.Printf("%s", err)
		os.Exit(1)
	}
	oauthproxy, err := NewOAuthProxy(opts, provider)
	if err != nil {
		log.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err)
		os.Exit(1)
	}

	if provider.BasicAuthEnabled {
		oauthproxy.SetBasicAuth = true
		oauthproxy.BasicAuthUserAttribute = provider.BasicAuthUserAttribute
		oauthproxy.BasicAuthPasswordAttribute = provider.BasicAuthPasswordAttribute
	}

	pb.proxy = oauthproxy
	pb.Handler = chain.Then(oauthproxy)
}
