//
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fulcio

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io"
	"net/rpc"
	"os"
	"path/filepath"

	"github.com/sigstore/cosign/v2/pkg/providers"
	"github.com/sigstore/gitsign/internal/cache"
	"github.com/sigstore/gitsign/internal/config"
	"github.com/sigstore/gitsign/internal/fulcio/fulcioroots"
	"github.com/sigstore/gitsign/internal/signerverifier"
	"github.com/sigstore/gitsign/pkg/fulcio"
	"github.com/sigstore/sigstore/pkg/oauth"
	"github.com/sigstore/sigstore/pkg/oauthflow"
	"github.com/sigstore/sigstore/pkg/signature"
	"golang.org/x/oauth2"
)

// PrivateKey defines the [crypto.PrivateKey] interface. This should be true for all PrivateKeys.
type PrivateKey interface {
	crypto.PrivateKey
	Public() crypto.PublicKey
}

type Identity struct {
	PrivateKey crypto.PrivateKey
	CertPEM    []byte
	ChainPEM   []byte
}

func NewIdentity(ctx context.Context, cfg *config.Config, in io.Reader, out io.Writer) (*Identity, error) {
	var cacheClient *cache.Client

	cachePath := os.Getenv("GITSIGN_CREDENTIAL_CACHE")
	if cachePath != "" {
		absPath, err := filepath.Abs(cachePath)
		if err != nil {
			return nil, fmt.Errorf("error resolving cache path: %w", err)
		}
		rpcClient, err := rpc.Dial("unix", absPath)
		if err != nil {
			return nil, fmt.Errorf("error creating RPC socket client: %w", err)
		}
		roots, intermediates, err := fulcioroots.NewFromConfig(ctx, cfg)
		if err != nil {
			return nil, fmt.Errorf("error loading certificate roots: %w", err)
		}
		cacheClient = &cache.Client{
			Client:        rpcClient,
			Roots:         roots,
			Intermediates: intermediates,
		}
		priv, cert, chain, err := cacheClient.GetCredentials(ctx, cfg)
		if err == nil {
			return &Identity{
				PrivateKey: priv,
				CertPEM:    cert,
				ChainPEM:   chain,
			}, nil
		}
		// Only print error on failure - if there's a problem fetching
		// from the cache just fall through to normal OIDC.
		fmt.Fprintf(out, "error getting cached creds: %v\n", err)
	}

	idf := &IdentityFactory{
		in:  in,
		out: out,
	}
	id, err := idf.NewIdentity(ctx, cfg)
	if err != nil {
		return nil, err
	}

	if cacheClient != nil {
		if err := id.CacheCert(ctx, cacheClient); err != nil {
			fmt.Fprintf(out, "error storing identity in cache: %v\n", err)
		}
	}

	return id, nil
}

// Certificate gets the identity's certificate.
func (i *Identity) Certificate() (*x509.Certificate, error) {
	p, _ := pem.Decode(i.CertPEM)
	cert, err := x509.ParseCertificate(p.Bytes)
	return cert, err
}

// CertificateChain attempts to get the identity's full certificate chain.
func (i *Identity) CertificateChain() ([]*x509.Certificate, error) {
	p, _ := pem.Decode(i.ChainPEM)
	chain, err := x509.ParseCertificates(p.Bytes)
	if err != nil {
		return nil, err
	}
	// the cert itself needs to be appended to the chain
	cert, err := i.Certificate()
	if err != nil {
		return nil, err
	}

	return append([]*x509.Certificate{cert}, chain...), nil
}

// Signer gets a crypto.Signer that uses the identity's private key.
func (i *Identity) Signer() (crypto.Signer, error) {
	sv, err := i.SignerVerifier()
	if err != nil {
		return nil, err
	}
	s, ok := sv.SignerVerifier.(crypto.Signer)
	if !ok {
		return nil, fmt.Errorf("could not use signer %T as crypto.Signer", sv)
	}
	return s, nil
}

// Delete deletes this identity from the system.
func (i *Identity) Delete() error {
	// Does nothing - keys are ephemeral
	return nil
}

// Close any manually managed memory held by the Identity.
func (i *Identity) Close() {
	// noop
}

func (i *Identity) PublicKey() (crypto.PublicKey, error) {
	pk, ok := i.PrivateKey.(PrivateKey)
	if !ok {
		return nil, fmt.Errorf("private key does not implement public key method")
	}
	return pk.Public(), nil
}

func (i *Identity) SignerVerifier() (*signerverifier.CertSignerVerifier, error) {
	sv, err := signature.LoadSignerVerifier(i.PrivateKey, crypto.SHA256)
	if err != nil {
		return nil, fmt.Errorf("error creating SignerVerifier: %w", err)
	}

	return &signerverifier.CertSignerVerifier{
		SignerVerifier: sv,
		Cert:           i.CertPEM,
		Chain:          i.ChainPEM,
	}, nil
}

func (i *Identity) CacheCert(ctx context.Context, cacheClient *cache.Client) error {
	return cacheClient.StoreCert(ctx, i.PrivateKey, i.CertPEM, i.ChainPEM)
}

// IdentityFactory holds reusable values for configuring how identities are created.
// Values set here are not expected to change per-request.
type IdentityFactory struct {
	in  io.Reader
	out io.Writer
}

func NewIdentityFactory(in io.Reader, out io.Writer) *IdentityFactory {
	return &IdentityFactory{
		in:  in,
		out: out,
	}
}

func (f *IdentityFactory) NewIdentity(ctx context.Context, cfg *config.Config) (*Identity, error) {
	clientID := cfg.ClientID

	clientSecret, err := cfg.ClientSecret()

	if err != nil {
		return nil, err
	}

	// Autoclose only works if we don't go through the identity selection page
	// (otherwise it'll show a countdown timer that doesn't work)
	if cfg.ConnectorID == "" {
		cfg.Autoclose = false
	}
	html, err := oauth.GetInteractiveSuccessHTML(cfg.Autoclose, cfg.AutocloseTimeout)
	if err != nil {
		fmt.Println("error getting interactive success html, using static default", err)
		html = oauth.InteractiveSuccessHTML
	}
	defaultFlow := &oauthflow.InteractiveIDTokenGetter{
		HTMLPage: html,
		Input:    f.in,
		Output:   f.out,
	}
	if cfg.ConnectorID != "" {
		defaultFlow.ExtraAuthURLParams = []oauth2.AuthCodeOption{oauthflow.ConnectorIDOpt(cfg.ConnectorID)}
	}
	var authFlow oauthflow.TokenGetter = defaultFlow

	// If enabled, try OIDC token providers to get a token. unless the token provider is "interactive" (in which case always do default interactive flow).
	var provider providers.Interface
	if cfg.TokenProvider == "" && providers.Enabled(ctx) {
		// If no token provider is set, look for any available provider to use.
		provider = defaultFlowProvider{}
	} else if cfg.TokenProvider != "" && cfg.TokenProvider != "interactive" {
		fmt.Fprintln(f.out, "using token provider", cfg.TokenProvider)

		// If a token provider is explicitly set always use it, unless it's "interactive",
		// which means always use the default interactive flow.
		p, err := providers.ProvideFrom(ctx, cfg.TokenProvider)
		if err != nil {
			return nil, fmt.Errorf("error getting token provider %q: %w", cfg.TokenProvider, err)
		}
		provider = p
	}
	if provider != nil {
		idToken, err := provider.Provide(ctx, clientID)
		if err != nil {
			fmt.Fprintln(f.out, "error getting id token:", err)
			return nil, fmt.Errorf("error getting id token: %w", err)
		}
		authFlow = &oauthflow.StaticTokenGetter{RawToken: idToken}
	}

	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, fmt.Errorf("generating private key: %w", err)
	}

	client, err := fulcio.NewClient(cfg.Fulcio,
		fulcio.OIDCOptions{
			Issuer:       cfg.Issuer,
			ClientID:     clientID,
			ClientSecret: clientSecret,
			RedirectURL:  cfg.RedirectURL,
			TokenGetter:  authFlow,
		})
	if err != nil {
		return nil, fmt.Errorf("error creating Fulcio client: %w", err)
	}

	cert, err := client.GetCert(priv)
	if err != nil {
		fmt.Fprintln(f.out, "error getting signer:", err)
		return nil, err
	}

	return &Identity{
		PrivateKey: priv,
		CertPEM:    cert.CertPEM,
		ChainPEM:   cert.ChainPEM,
	}, nil
}

type defaultFlowProvider struct{}

func (defaultFlowProvider) Enabled(ctx context.Context) bool {
	return providers.Enabled(ctx)
}

func (defaultFlowProvider) Provide(ctx context.Context, audience string) (string, error) {
	return providers.Provide(ctx, audience)
}
