// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: MPL-2.0

package proxy

import (
	"crypto/ed25519"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"strings"
)

// clientTlsConfig creates a TLS configuration to connect to a worker proxy, or
// returns a cached config
//
// Supported options: WithWorkerHost. If provided will use that host (minus
// port) for the SNI header. Otherwise, it will use the first worker host
// provided in the session authorization data.
func (p *ClientProxy) clientTlsConfig(opt ...Option) (*tls.Config, error) {
	if p.clientTlsConf != nil {
		return p.clientTlsConf, nil
	}

	const op = "proxy.clientTlsConfig"
	if p.sessionAuthzData == nil {
		return nil, fmt.Errorf("%s: nil session authorization data", op)
	}
	if len(p.sessionAuthzData.WorkerInfo) == 0 {
		return nil, fmt.Errorf("%s: no worker info", op)
	}

	opts, err := getOpts(opt...)
	if err != nil {
		return nil, fmt.Errorf("%s: error getting options: %w", op, err)
	}

	parsedCert, err := x509.ParseCertificate(p.sessionAuthzData.Certificate)
	if err != nil {
		return nil, fmt.Errorf("%s: unable to decode mTLS certificate: %w", op, err)
	}

	workerHostRaw := opts.WithWorkerHost
	if workerHostRaw == "" {
		workerHostRaw = p.sessionAuthzData.WorkerInfo[0].Address
	}
	workerHost, _, err := net.SplitHostPort(workerHostRaw)
	if err != nil {
		if strings.Contains(err.Error(), "missing port") {
			workerHost = workerHostRaw
		} else {
			return nil, fmt.Errorf("%s: error splitting worker host/port: %w", op, err)
		}
	}

	certPool := x509.NewCertPool()
	certPool.AddCert(parsedCert)

	tlsConf := &tls.Config{
		Certificates: []tls.Certificate{
			{
				Certificate: [][]byte{p.sessionAuthzData.Certificate},
				PrivateKey:  ed25519.PrivateKey(p.sessionAuthzData.PrivateKey),
				Leaf:        parsedCert,
			},
		},
		ServerName: workerHost,
		MinVersion: tls.VersionTLS13,
		NextProtos: []string{"http/1.1", p.sessionAuthzData.SessionId},

		// This is set this way so we can make use of VerifyConnection, which we
		// set on this TLS config below. We are not skipping verification!
		InsecureSkipVerify: true,
	}

	// We disable normal DNS SAN behavior as we don't rely on DNS or IP
	// addresses for security and want to avoid issues with including localhost
	// etc.
	verifyOpts := x509.VerifyOptions{
		DNSName: p.sessionAuthzData.SessionId,
		Roots:   certPool,
		KeyUsages: []x509.ExtKeyUsage{
			x509.ExtKeyUsageClientAuth,
			x509.ExtKeyUsageServerAuth,
		},
	}
	tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
		// Go will not run this without at least one peer certificate, but
		// doesn't hurt to check
		if len(cs.PeerCertificates) == 0 {
			return fmt.Errorf("%s: no peer certificates provided", op)
		}
		_, err := cs.PeerCertificates[0].Verify(verifyOpts)
		return err
	}

	p.clientTlsConf = tlsConf
	return tlsConf, nil
}
