package fulcio

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"fmt"
	"io"
	"net/url"

	"github.com/containers/image/v5/internal/useragent"
	"github.com/containers/image/v5/signature/sigstore/internal"
	"github.com/sigstore/fulcio/pkg/api"
	"github.com/sigstore/sigstore/pkg/oauth"
	"github.com/sigstore/sigstore/pkg/oauthflow"
	sigstoreSignature "github.com/sigstore/sigstore/pkg/signature"
	"github.com/sirupsen/logrus"
	"golang.org/x/oauth2"
)

// setupSignerWithFulcio updates s with a certificate generated by fulcioURL based on oidcIDToken
func setupSignerWithFulcio(s *internal.SigstoreSigner, fulcioURL *url.URL, oidcIDToken *oauthflow.OIDCIDToken) error {
	// ECDSA-P256 is the only interoperable algorithm per
	// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes .
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return fmt.Errorf("generating short-term private key: %w", err)
	}
	keyAlgorithm := "ecdsa"
	// SHA-256 is opencontainers/go-digest.Canonical, thus the algorithm to use here as well per
	// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#hashing-algorithms
	signer, err := sigstoreSignature.LoadECDSASigner(privateKey, crypto.SHA256)
	if err != nil {
		return fmt.Errorf("initializing short-term private key: %w", err)
	}
	s.PrivateKey = signer

	logrus.Debugf("Requesting a certificate from Fulcio at %s", fulcioURL.Redacted())
	fulcioClient := api.NewClient(fulcioURL, api.WithUserAgent(useragent.DefaultUserAgent))
	// Sign the email address as part of the request
	h := sha256.Sum256([]byte(oidcIDToken.Subject))
	keyOwnershipProof, err := ecdsa.SignASN1(rand.Reader, privateKey, h[:])
	if err != nil {
		return fmt.Errorf("Error signing key ownership proof: %w", err)
	}
	publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
	if err != nil {
		return fmt.Errorf("converting public key to ASN.1: %w", err)
	}
	// Note that unlike most OAuth2 uses, this passes the ID token, not an access token.
	// This is only secure if every Fulcio server has an individual client ID value
	// = fulcioOIDCClientID, distinct from other Fulcio servers,
	// that is embedded into the ID token’s "aud" field.
	resp, err := fulcioClient.SigningCert(api.CertificateRequest{
		PublicKey: api.Key{
			Content:   publicKeyBytes,
			Algorithm: keyAlgorithm,
		},
		SignedEmailAddress: keyOwnershipProof,
	}, oidcIDToken.RawString)
	if err != nil {
		return fmt.Errorf("obtaining certificate from Fulcio: %w", err)
	}
	s.FulcioGeneratedCertificate = resp.CertPEM
	s.FulcioGeneratedCertificateChain = resp.ChainPEM
	// Cosign goes through an unmarshal/marshal roundtrip for Fulcio-generated certificates, let’s not do that.
	s.SigningKeyOrCert = resp.CertPEM
	return nil
}

// WithFulcioAndPreexistingOIDCIDToken sets up signing to use a short-lived key and a Fulcio-issued certificate
// based on a caller-provided OIDC ID token.
func WithFulcioAndPreexistingOIDCIDToken(fulcioURL *url.URL, oidcIDToken string) internal.Option {
	return func(s *internal.SigstoreSigner) error {
		if s.PrivateKey != nil {
			return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
		}

		// This adds dependencies even just to parse the token. We could possibly reimplement that, and split this variant
		// into a subpackage without the OIDC dependencies… but really, is this going to be used in significantly different situations
		// than the two interactive OIDC authentication workflows?
		//
		// Are there any widely used tools to manually obtain an ID token? Why would there be?
		// For long-term usage, users provisioning a static OIDC credential might just as well provision an already-generated certificate
		// or something like that.
		logrus.Debugf("Using a statically-provided OIDC token")
		staticTokenGetter := oauthflow.StaticTokenGetter{RawToken: oidcIDToken}
		oidcIDToken, err := staticTokenGetter.GetIDToken(nil, oauth2.Config{})
		if err != nil {
			return fmt.Errorf("parsing OIDC token: %w", err)
		}

		return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
	}
}

// WithFulcioAndDeviceAuthorizationGrantOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate
// based on an OIDC ID token obtained using a device authorization grant (RFC 8628).
//
// interactiveOutput must be directly accessible to a human user in real time (i.e. not be just a log file).
func WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string,
	interactiveOutput io.Writer) internal.Option {
	return func(s *internal.SigstoreSigner) error {
		if s.PrivateKey != nil {
			return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
		}

		logrus.Debugf("Starting OIDC device flow for issuer %s", oidcIssuerURL.Redacted())
		tokenGetter := oauthflow.NewDeviceFlowTokenGetterForIssuer(oidcIssuerURL.String())
		tokenGetter.MessagePrinter = func(s string) {
			fmt.Fprintln(interactiveOutput, s)
		}
		oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter)
		if err != nil {
			return fmt.Errorf("Error authenticating with OIDC: %w", err)
		}

		return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
	}
}

// WithFulcioAndInterativeOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate
// based on an interactively-obtained OIDC ID token.
// The token is obtained
//   - directly using a browser, listening on localhost, automatically opening a browser to the OIDC issuer,
//     to be redirected on localhost. (I.e. the current environment must allow launching a browser that connect back to the current process;
//     either or both may be impossible in a container or a remote VM).
//   - or by instructing the user to manually open a browser, obtain the OIDC code, and interactively input it as text.
//
// interactiveInput and interactiveOutput must both be directly operable by a human user in real time (i.e. not be just a log file).
func WithFulcioAndInteractiveOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string,
	interactiveInput io.Reader, interactiveOutput io.Writer) internal.Option {
	return func(s *internal.SigstoreSigner) error {
		if s.PrivateKey != nil {
			return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures")
		}

		logrus.Debugf("Starting interactive OIDC authentication for issuer %s", oidcIssuerURL.Redacted())
		// This is intended to match oauthflow.DefaultIDTokenGetter, overriding only input/output
		tokenGetter := &oauthflow.InteractiveIDTokenGetter{
			HTMLPage: oauth.InteractiveSuccessHTML,
			Input:    interactiveInput,
			Output:   interactiveOutput,
		}
		oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter)
		if err != nil {
			return fmt.Errorf("Error authenticating with OIDC: %w", err)
		}

		return setupSignerWithFulcio(s, fulcioURL, oidcIDToken)
	}
}
