package ioz

import (
	"context"
	"crypto/ed25519"
	"encoding/base64"
	"fmt"
	"io"
	"log/slog"
	"os"
	"sync"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/cryptoz"
	"google.golang.org/grpc/mem"
)

// DiscardData discards up to 8KiB of data from r.
// Function can be used to drain HTTP client response body.
// See https://pkg.go.dev/net/http#Response.
func DiscardData(r io.Reader) error {
	_, err := io.Copy(io.Discard, io.LimitReader(r, 8*1024))
	if err != nil {
		return fmt.Errorf("failed to read data: %w", err)
	}
	return nil
}

func LoadSHA256Base64Secret(log *slog.Logger, filename string) ([]byte, error) {
	return LoadBase64Secret(log, filename, cryptoz.HMACSHA256MinKeySize, cryptoz.HMACSHA256MaxKeySize)
}

func LoadSHA3_512Base64Secret(log *slog.Logger, filename string) ([]byte, error) {
	return LoadBase64Secret(log, filename, cryptoz.HMACSHA3_512MinKeySize, cryptoz.HMACSHA3_512MaxKeySize)
}

func LoadEd25519Base64PublicKey(log *slog.Logger, filename string) ([]byte, error) {
	return LoadBase64Secret(log, filename, ed25519.PublicKeySize, ed25519.PublicKeySize)
}

func LoadBase64Secret(log *slog.Logger, filename string, minLen, maxLen int) ([]byte, error) {
	encodedAuthSecret, err := os.ReadFile(filename) //nolint: gosec
	if err != nil {
		return nil, fmt.Errorf("read file: %w", err)
	}
	decodedAuthSecret := make([]byte, base64.StdEncoding.DecodedLen(len(encodedAuthSecret)))

	n, err := base64.StdEncoding.Decode(decodedAuthSecret, encodedAuthSecret)
	if err != nil {
		return nil, fmt.Errorf("decoding: %w", err)
	}

	if n < minLen || n > maxLen {
		// TODO make this an error in 18.0, remove logging
		// TODO https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/issues/617
		log.Warn(fmt.Sprintf("secret in file %s is %d bytes long; allowed length is from %d to %d bytes", filename, n, minLen, maxLen))
		//return nil, fmt.Errorf("secret is %d bytes long; allowed length is from %d to %d bytes", n, minLen, maxLen)
	}

	return decodedAuthSecret[:n], nil
}

// ReadAllFunc fully reads the reader and passes the data to the callback.
// Callback must not retain the buffer or any portion of it.
func ReadAllFunc(r io.Reader, f func([]byte) error) error {
	bs, err := mem.ReadAll(r, mem.DefaultBufferPool())
	if err != nil {
		bs.Free()
		return fmt.Errorf("read all: %w", err)
	}
	data := bs.MaterializeToBuffer(mem.DefaultBufferPool())
	bs.Free()
	defer data.Free()
	return f(data.ReadOnlyData())
}

func ReadSecretAES256Key(path string) ([]byte, error) {
	k, err := os.ReadFile(path) //nolint: gosec
	if err != nil {
		return nil, fmt.Errorf("unable to read key file: %w", err)
	}

	if len(k) != cryptoz.AES256KeyLength {
		return nil, fmt.Errorf("secret is %d bytes but want %d", len(k), cryptoz.AES256KeyLength)
	}

	return k, nil
}

type CancelingReadCloser struct {
	io.ReadCloser
	Cancel context.CancelFunc
}

func (c *CancelingReadCloser) Close() error {
	// First close the pipe, then cancel the context.
	// Doing it the other way around can result in another goroutine calling CloseWithError() with the
	// cancellation error (if ReadCloser is a io.Pipe() used across goroutines).
	err := c.ReadCloser.Close()
	c.Cancel()
	return err
}

type OnceReadCloser struct {
	io.ReadCloser
	once     sync.Once
	closeErr error
}

func (oc *OnceReadCloser) Close() error {
	oc.once.Do(oc.close)
	return oc.closeErr
}

func (oc *OnceReadCloser) close() {
	oc.closeErr = oc.ReadCloser.Close()
}
