// Copyright 2020-2025 Buf Technologies, Inc.
//
// 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 tmp provides temporary files and directories.
//
// Usage of this package requires eng approval - ask before using.
package tmp

import (
	"context"
	"errors"
	"io"
	"os"
	"path/filepath"

	"github.com/bufbuild/buf/private/pkg/uuidutil"
)

// File is a temporary file or directory.
//
// It must be closed when done.
type File interface {
	io.Closer

	Path() string
}

// NewFile returns a new temporary file with the data copied from the Reader.
//
// It must be closed when done. This deletes this file.
// This file will be automatically closed on context cancellation.
//
// Usage of this function requires eng approval - ask before using.
func NewFile(ctx context.Context, reader io.Reader) (File, error) {
	id, err := uuidutil.New()
	if err != nil {
		return nil, err
	}
	file, err := os.CreateTemp("", id.String())
	if err != nil {
		return nil, err
	}
	path := file.Name()
	// just in case
	absPath, err := filepath.Abs(filepath.Clean(path))
	if err != nil {
		return nil, err
	}
	closer := func() error { return os.Remove(absPath) }
	go func() {
		<-ctx.Done()
		_ = closer()
	}()
	_, err = io.Copy(file, reader)
	err = errors.Join(err, file.Close())
	if err != nil {
		err = errors.Join(err, closer())
		return nil, err
	}
	return newFile(closerFunc(closer), absPath), nil
}

// NewDir returns a new temporary directory.
//
// It must be closed when done. This deletes this directory and all its contents.
// This directory will be automatically closed on context cancellation.
//
// Usage of this function requires eng approval - ask before using.
func NewDir(ctx context.Context, options ...DirOption) (File, error) {
	dirOptions := newDirOptions()
	for _, option := range options {
		option(dirOptions)
	}
	id, err := uuidutil.New()
	if err != nil {
		return nil, err
	}
	path, err := os.MkdirTemp(dirOptions.basePath, id.String())
	if err != nil {
		return nil, err
	}
	// just in case
	absPath, err := filepath.Abs(filepath.Clean(path))
	if err != nil {
		return nil, err
	}
	closer := func() error { return os.RemoveAll(absPath) }
	go func() {
		<-ctx.Done()
		_ = closer()
	}()
	return newFile(closerFunc(closer), absPath), nil
}

// DirOption is an option for NewDir.
type DirOption func(*dirOptions)

// DirWithBasePath returns a new DirOption that sets the base path to create
// the temporary directory in.
//
// The default is to use os.TempDir().
func DirWithBasePath(basePath string) DirOption {
	return func(dirOptions *dirOptions) {
		dirOptions.basePath = basePath
	}
}

type file struct {
	io.Closer

	path string
}

func newFile(closer io.Closer, path string) *file {
	return &file{
		Closer: closer,
		path:   path,
	}
}

func (f *file) Path() string {
	return f.path
}

type closerFunc func() error

func (c closerFunc) Close() error {
	return c()
}

type dirOptions struct {
	basePath string
}

func newDirOptions() *dirOptions {
	return &dirOptions{}
}
