// 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/rsa"
	"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"
)

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 *rsa.PrivateKey. 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) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode(key)
	if block != nil {
		key = block.Bytes
	}
	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.(*rsa.PrivateKey)
	if !ok {
		return nil, errors.New("private key is invalid")
	}
	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 {
	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)
	})
	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) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
	defer cancel()
	return metadata.GetWithContext(ctx, "universe/universe-domain")
}

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