package utils

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"

	"github.com/safing/portbase/utils/renameio"
)

// AtomicFileOptions holds additional options for manipulating
// the behavior of CreateAtomic and friends.
type AtomicFileOptions struct {
	// Mode is the file mode for the new file. If
	// 0, the file mode will be set to 0600.
	Mode os.FileMode

	// TempDir is the path to the temp-directory
	// that should be used. If empty, it defaults
	// to the system temp.
	TempDir string
}

// CreateAtomic creates or overwrites a file at dest atomically using
// data from r. Atomic means that even in case of a power outage,
// dest will never be a zero-length file. It will always either contain
// the previous data (or not exist) or the new data but never anything
// in between.
func CreateAtomic(dest string, r io.Reader, opts *AtomicFileOptions) error {
	if opts == nil {
		opts = &AtomicFileOptions{}
	}

	tmpFile, err := renameio.TempFile(opts.TempDir, dest)
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	defer tmpFile.Cleanup() //nolint:errcheck

	if opts.Mode != 0 {
		if err := tmpFile.Chmod(opts.Mode); err != nil {
			return fmt.Errorf("failed to update mode bits of temp file: %w", err)
		}
	}

	if _, err := io.Copy(tmpFile, r); err != nil {
		return fmt.Errorf("failed to copy source file: %w", err)
	}

	if err := tmpFile.CloseAtomicallyReplace(); err != nil {
		return fmt.Errorf("failed to rename temp file to %q", dest)
	}

	return nil
}

// CopyFileAtomic is like CreateAtomic but copies content from
// src to dest. If opts.Mode is 0 CopyFileAtomic tries to set
// the file mode of src to dest.
func CopyFileAtomic(dest string, src string, opts *AtomicFileOptions) error {
	if opts == nil {
		opts = &AtomicFileOptions{}
	}

	if opts.Mode == 0 {
		stat, err := os.Stat(src)
		if err != nil {
			return err
		}
		opts.Mode = stat.Mode()
	}

	f, err := os.Open(src)
	if err != nil {
		return err
	}
	defer func() {
		_ = f.Close()
	}()

	return CreateAtomic(dest, f, opts)
}

// ReplaceFileAtomic replaces the file at dest with the content from src.
// If dest exists it's file mode copied and used for the replacement. If
// not, dest will get the same file mode as src. See CopyFileAtomic and
// CreateAtomic for more information.
func ReplaceFileAtomic(dest string, src string, opts *AtomicFileOptions) error {
	if opts == nil {
		opts = &AtomicFileOptions{}
	}

	if opts.Mode == 0 {
		stat, err := os.Stat(dest)
		if err == nil {
			opts.Mode = stat.Mode()
		} else if !errors.Is(err, fs.ErrNotExist) {
			return err
		}
	}

	return CopyFileAtomic(dest, src, opts)
}
