/*
Copyright the Velero contributors.

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 downloadrequest

import (
	"compress/gzip"
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/pkg/errors"
	kbclient "sigs.k8s.io/controller-runtime/pkg/client"

	veleroV1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
	"github.com/vmware-tanzu/velero/pkg/builder"
)

// ErrNotFound is exported for external packages to check for when a file is
// not found
var ErrNotFound = errors.New("file not found")
var ErrDownloadRequestDownloadURLTimeout = errors.New("download request download url timeout, check velero server logs for errors. backup storage location may not be available")

func Stream(
	ctx context.Context,
	kbClient kbclient.Client,
	namespace, name string,
	kind veleroV1api.DownloadTargetKind,
	w io.Writer,
	timeout time.Duration,
	insecureSkipTLSVerify bool,
	caCertFile string,
) error {
	return StreamWithBSLCACert(ctx, kbClient, namespace, name, kind, w, timeout, insecureSkipTLSVerify, caCertFile, "")
}

// StreamWithBSLCACert is like Stream but accepts an additional bslCACert parameter
// that contains the cacert from the BackupStorageLocation config
func StreamWithBSLCACert(
	ctx context.Context,
	kbClient kbclient.Client,
	namespace, name string,
	kind veleroV1api.DownloadTargetKind,
	w io.Writer,
	timeout time.Duration,
	insecureSkipTLSVerify bool,
	caCertFile string,
	bslCACert string,
) error {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	downloadURL, err := getDownloadURL(ctx, kbClient, namespace, name, kind)
	if err != nil {
		return err
	}

	if err := download(ctx, downloadURL, kind, w, insecureSkipTLSVerify, caCertFile, bslCACert); err != nil {
		return err
	}

	return nil
}

func getDownloadURL(
	ctx context.Context,
	kbClient kbclient.Client,
	namespace, name string,
	kind veleroV1api.DownloadTargetKind,
) (string, error) {
	uuid, err := uuid.NewRandom()
	if err != nil {
		return "", err
	}

	reqName := fmt.Sprintf("%s-%s", name, uuid.String())
	created := builder.ForDownloadRequest(namespace, reqName).Target(kind, name).Result()

	if err := kbClient.Create(ctx, created, &kbclient.CreateOptions{}); err != nil {
		return "", errors.WithStack(err)
	}

	for {
		select {
		case <-ctx.Done():
			return "", ErrDownloadRequestDownloadURLTimeout

		case <-time.After(25 * time.Millisecond):
			updated := &veleroV1api.DownloadRequest{}
			if err := kbClient.Get(ctx, kbclient.ObjectKey{Name: created.Name, Namespace: namespace}, updated); err != nil {
				return "", errors.WithStack(err)
			}

			if updated.Status.DownloadURL != "" {
				return updated.Status.DownloadURL, nil
			}
		}
	}
}

func download(
	ctx context.Context,
	downloadURL string,
	kind veleroV1api.DownloadTargetKind,
	w io.Writer,
	insecureSkipTLSVerify bool,
	caCertFile string,
	caCertByteString string,
) error {
	var caPool *x509.CertPool
	var err error

	// Initialize caPool once
	caPool, err = x509.SystemCertPool()
	if err != nil {
		caPool = x509.NewCertPool()
	}

	// Try to load CA cert from file first
	if len(caCertFile) > 0 {
		caCert, err := os.ReadFile(caCertFile)
		if err != nil {
			// If caCertFile fails and BSL cert is available, fall back to it
			if len(caCertByteString) > 0 {
				fmt.Fprintf(os.Stderr, "Warning: Failed to open CA certificate file %s: %v. Using CA certificate from backup storage location instead.\n", caCertFile, err)
				caPool.AppendCertsFromPEM([]byte(caCertByteString))
			} else {
				// If no BSL cert available, return the original error
				return errors.Wrapf(err, "couldn't open cacert")
			}
		} else {
			caPool.AppendCertsFromPEM(caCert)
		}
	} else if len(caCertByteString) > 0 {
		// If no caCertFile specified, use BSL cert if available
		caPool.AppendCertsFromPEM([]byte(caCertByteString))
	}

	defaultTransport := http.DefaultTransport.(*http.Transport)
	// same settings as the default transport
	// aside from TLSClientConfig
	httpClient := new(http.Client)
	httpClient.Transport = &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: insecureSkipTLSVerify, //nolint:gosec // This parameter is useful for some scenarios.
			RootCAs:            caPool,
		},
		DialContext:           defaultTransport.DialContext,
		ForceAttemptHTTP2:     defaultTransport.ForceAttemptHTTP2,
		MaxIdleConns:          defaultTransport.MaxIdleConns,
		Proxy:                 defaultTransport.Proxy,
		TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
		ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
	if err != nil {
		return err
	}

	resp, err := httpClient.Do(httpReq)
	if err != nil {
		if urlErr, ok := err.(*url.Error); ok {
			if _, ok := urlErr.Err.(x509.UnknownAuthorityError); ok {
				return fmt.Errorf("%s\n\nThe --insecure-skip-tls-verify flag can also be used to accept any TLS certificate for the download, but it is susceptible to man-in-the-middle attacks", err.Error())
			}
		}
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return errors.Wrapf(err, "request failed: unable to decode response body")
		}

		if resp.StatusCode == http.StatusNotFound {
			return ErrNotFound
		}

		return errors.Errorf("request failed: %v", string(body))
	}

	reader := resp.Body
	if kind != veleroV1api.DownloadTargetKindBackupContents {
		// need to decompress logs
		gzipReader, err := gzip.NewReader(resp.Body)
		if err != nil {
			return err
		}
		defer gzipReader.Close()
		reader = gzipReader
	}

	_, err = io.Copy(w, reader)
	return err
}
