// Copyright 2023 Google LLC
//
// 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 internal

import (
	"context"
	"crypto"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
	"time"

	"cloud.google.com/go/compute/metadata"
)

const (
	// TokenTypeBearer is the auth header prefix for bearer tokens.
	TokenTypeBearer = "Bearer"

	// QuotaProjectEnvVar is the environment variable for setting the quota
	// project.
	QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
	// UniverseDomainEnvVar is the environment variable for setting the default
	// service domain for a given Cloud universe.
	UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
	projectEnvVar        = "GOOGLE_CLOUD_PROJECT"
	maxBodySize          = 1 << 20

	// DefaultUniverseDomain is the default value for universe domain.
	// Universe domain is the default service domain for a given Cloud universe.
	DefaultUniverseDomain = "googleapis.com"

	// TrustBoundaryNoOp is a constant indicating no trust boundary is enforced.
	TrustBoundaryNoOp = "0x0"

	// TrustBoundaryDataKey is the key used to store trust boundary data in a token's metadata.
	TrustBoundaryDataKey = "google.auth.trust_boundary_data"
)

type clonableTransport interface {
	Clone() *http.Transport
}

// DefaultClient returns an [http.Client] with some defaults set. If
// the current [http.DefaultTransport] is a [clonableTransport], as
// is the case for an [*http.Transport], the clone will be used.
// Otherwise the [http.DefaultTransport] is used directly.
func DefaultClient() *http.Client {
	if transport, ok := http.DefaultTransport.(clonableTransport); ok {
		return &http.Client{
			Transport: transport.Clone(),
			Timeout:   30 * time.Second,
		}
	}

	return &http.Client{
		Transport: http.DefaultTransport,
		Timeout:   30 * time.Second,
	}
}

// ParseKey converts the binary contents of a private key file
// to an crypto.Signer. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (crypto.Signer, error) {
	block, _ := pem.Decode(key)
	if block != nil {
		key = block.Bytes
	}
	var parsedKey crypto.PrivateKey
	var err error
	parsedKey, err = x509.ParsePKCS8PrivateKey(key)
	if err != nil {
		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
		if err != nil {
			return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
		}
	}
	parsed, ok := parsedKey.(crypto.Signer)
	if !ok {
		return nil, errors.New("private key is not a signer")
	}
	return parsed, nil
}

// GetQuotaProject retrieves quota project with precedence being: override,
// environment variable, creds json file.
func GetQuotaProject(b []byte, override string) string {
	if override != "" {
		return override
	}
	if env := os.Getenv(QuotaProjectEnvVar); env != "" {
		return env
	}
	if b == nil {
		return ""
	}
	var v struct {
		QuotaProject string `json:"quota_project_id"`
	}
	if err := json.Unmarshal(b, &v); err != nil {
		return ""
	}
	return v.QuotaProject
}

// GetProjectID retrieves project with precedence being: override,
// environment variable, creds json file.
func GetProjectID(b []byte, override string) string {
	if override != "" {
		return override
	}
	if env := os.Getenv(projectEnvVar); env != "" {
		return env
	}
	if b == nil {
		return ""
	}
	var v struct {
		ProjectID string `json:"project_id"` // standard service account key
		Project   string `json:"project"`    // gdch key
	}
	if err := json.Unmarshal(b, &v); err != nil {
		return ""
	}
	if v.ProjectID != "" {
		return v.ProjectID
	}
	return v.Project
}

// DoRequest executes the provided req with the client. It reads the response
// body, closes it, and returns it.
func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
	resp, err := client.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()
	body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
	if err != nil {
		return nil, nil, err
	}
	return resp, body, nil
}

// ReadAll consumes the whole reader and safely reads the content of its body
// with some overflow protection.
func ReadAll(r io.Reader) ([]byte, error) {
	return io.ReadAll(io.LimitReader(r, maxBodySize))
}

// StaticCredentialsProperty is a helper for creating static credentials
// properties.
func StaticCredentialsProperty(s string) StaticProperty {
	return StaticProperty(s)
}

// StaticProperty always returns that value of the underlying string.
type StaticProperty string

// GetProperty loads the properly value provided the given context.
func (p StaticProperty) GetProperty(context.Context) (string, error) {
	return string(p), nil
}

// ComputeUniverseDomainProvider fetches the credentials universe domain from
// the google cloud metadata service.
type ComputeUniverseDomainProvider struct {
	MetadataClient     *metadata.Client
	universeDomainOnce sync.Once
	universeDomain     string
	universeDomainErr  error
}

// GetProperty fetches the credentials universe domain from the google cloud
// metadata service.
func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
	c.universeDomainOnce.Do(func() {
		c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
	})
	if c.universeDomainErr != nil {
		return "", c.universeDomainErr
	}
	return c.universeDomain, nil
}

// httpGetMetadataUniverseDomain is a package var for unit test substitution.
var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
	defer cancel()
	return client.GetWithContext(ctx, "universe/universe-domain")
}

func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
	universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
	if err == nil {
		return universeDomain, nil
	}
	if _, ok := err.(metadata.NotDefinedError); ok {
		// http.StatusNotFound (404)
		return DefaultUniverseDomain, nil
	}
	return "", err
}

// FormatIAMServiceAccountResource sets a service account name in an IAM resource
// name.
func FormatIAMServiceAccountResource(name string) string {
	return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}

// TrustBoundaryData represents the trust boundary data associated with a token.
// It contains information about the regions or environments where the token is valid.
type TrustBoundaryData struct {
	// Locations is the list of locations that the token is allowed to be used in.
	Locations []string
	// EncodedLocations represents the locations in an encoded format.
	EncodedLocations string
}

// NewTrustBoundaryData returns a new TrustBoundaryData with the specified locations and encoded locations.
func NewTrustBoundaryData(locations []string, encodedLocations string) *TrustBoundaryData {
	// Ensure consistency by treating a nil slice as an empty slice.
	if locations == nil {
		locations = []string{}
	}
	locationsCopy := make([]string, len(locations))
	copy(locationsCopy, locations)
	return &TrustBoundaryData{
		Locations:        locationsCopy,
		EncodedLocations: encodedLocations,
	}
}

// NewNoOpTrustBoundaryData returns a new TrustBoundaryData with no restrictions.
func NewNoOpTrustBoundaryData() *TrustBoundaryData {
	return &TrustBoundaryData{
		Locations:        []string{},
		EncodedLocations: TrustBoundaryNoOp,
	}
}

// TrustBoundaryHeader returns the value for the x-allowed-locations header and a bool
// indicating if the header should be set. The return values are structured to
// handle three distinct states required by the backend:
// 1. Header not set: (value="", present=false) -> data is empty.
// 2. Header set to an empty string: (value="", present=true) -> data is a no-op.
// 3. Header set to a value: (value="...", present=true) -> data has locations.
func (t TrustBoundaryData) TrustBoundaryHeader() (value string, present bool) {
	if t.EncodedLocations == "" {
		// If the data is empty, the header should not be present.
		return "", false
	}

	// If data is not empty, the header should always be present.
	present = true
	value = ""
	if t.EncodedLocations != TrustBoundaryNoOp {
		value = t.EncodedLocations
	}
	// For a no-op, the backend requires an empty string.
	return value, present
}
