package loggerutils

import (
	"io"
	"io/fs"
	"os"
	"runtime"
)

type fileConvertFn func(dst io.WriteSeeker, src io.ReadSeeker) error

type stfID uint64

// sharedTempFileConverter converts files using a user-supplied function and
// writes the results to temporary files which are automatically cleaned up on
// close. If another request is made to convert the same file, the conversion
// result and temporary file are reused if they have not yet been cleaned up.
//
// A file is considered the same as another file using the os.SameFile function,
// which compares file identity (e.g. device and inode numbers on Linux) and is
// robust to file renames. Input files are assumed to be immutable; no attempt
// is made to ascertain whether the file contents have changed between requests.
//
// One file descriptor is used per source file, irrespective of the number of
// concurrent readers of the converted contents.
type sharedTempFileConverter struct {
	// The directory where temporary converted files are to be written to.
	// If set to the empty string, the default directory for temporary files
	// is used.
	TempDir string

	conv fileConvertFn
	st   chan stfcState
}

type stfcState struct {
	fl     map[stfID]sharedTempFile
	nextID stfID
}

type sharedTempFile struct {
	src  os.FileInfo // Info about the source file for path-independent identification with os.SameFile.
	fd   *os.File
	size int64
	ref  int                       // Reference count of open readers on the temporary file.
	wait []chan<- stfConvertResult // Wait list for the conversion to complete.
}

type stfConvertResult struct {
	fr  *sharedFileReader
	err error
}

func newSharedTempFileConverter(conv fileConvertFn) *sharedTempFileConverter {
	st := make(chan stfcState, 1)
	st <- stfcState{fl: make(map[stfID]sharedTempFile)}
	return &sharedTempFileConverter{conv: conv, st: st}
}

// Do returns a reader for the contents of f as converted by the c.C function.
// It is the caller's responsibility to close the returned reader.
//
// This function is safe for concurrent use by multiple goroutines.
func (c *sharedTempFileConverter) Do(f *os.File) (*sharedFileReader, error) {
	stat, err := f.Stat()
	if err != nil {
		return nil, err
	}

	st := <-c.st
	for id, tf := range st.fl {
		// os.SameFile can have false positives if one of the files was
		// deleted before the other file was created -- such as during
		// log rotations... https://github.com/golang/go/issues/36895
		// Weed out those false positives by also comparing the files'
		// ModTime, which conveniently also handles the case of true
		// positives where the file has also been modified since it was
		// first converted.
		if os.SameFile(tf.src, stat) && tf.src.ModTime().Equal(stat.ModTime()) {
			return c.openExisting(st, id, tf)
		}
	}
	return c.openNew(st, f, stat)
}

func (c *sharedTempFileConverter) openNew(st stfcState, f *os.File, stat os.FileInfo) (*sharedFileReader, error) {
	// Record that we are starting to convert this file so that any other
	// requests for the same source file while the conversion is in progress
	// can join.
	id := st.nextID
	st.nextID++
	st.fl[id] = sharedTempFile{src: stat}
	c.st <- st

	dst, size, convErr := c.convert(f)

	st = <-c.st
	flid := st.fl[id]

	if convErr != nil {
		// Conversion failed. Delete it from the state so that future
		// requests to convert the same file can try again fresh.
		delete(st.fl, id)
		c.st <- st
		for _, w := range flid.wait {
			w <- stfConvertResult{err: convErr}
		}
		return nil, convErr
	}

	flid.fd = dst
	flid.size = size
	flid.ref = len(flid.wait) + 1
	for _, w := range flid.wait {
		// Each waiter needs its own reader with an independent read pointer.
		w <- stfConvertResult{fr: flid.Reader(c, id)}
	}
	flid.wait = nil
	st.fl[id] = flid
	c.st <- st
	return flid.Reader(c, id), nil
}

func (c *sharedTempFileConverter) openExisting(st stfcState, id stfID, v sharedTempFile) (*sharedFileReader, error) {
	if v.fd != nil {
		// Already converted.
		v.ref++
		st.fl[id] = v
		c.st <- st
		return v.Reader(c, id), nil
	}
	// The file has not finished being converted.
	// Add ourselves to the wait list. "Don't call us; we'll call you."
	wait := make(chan stfConvertResult, 1)
	v.wait = append(v.wait, wait)
	st.fl[id] = v
	c.st <- st

	res := <-wait
	return res.fr, res.err
}

func (c *sharedTempFileConverter) convert(f *os.File) (converted *os.File, size int64, _ error) {
	dst, err := os.CreateTemp(c.TempDir, "dockerdtemp.*")
	if err != nil {
		return nil, 0, err
	}
	defer func() {
		_ = dst.Close()
		// Delete the temporary file immediately so that final cleanup
		// of the file on disk is deferred to the OS once we close all
		// our file descriptors (or the process dies). Assuming no early
		// returns due to errors, the file will be open by this process
		// with a read-only descriptor at this point. As we don't care
		// about being able to reuse the file name -- it's randomly
		// generated and unique -- we can safely use os.Remove on
		// Windows.
		_ = os.Remove(dst.Name())
	}()
	err = c.conv(dst, f)
	if err != nil {
		return nil, 0, err
	}
	// Close the exclusive read-write file descriptor, catching any delayed
	// write errors (and on Windows, releasing the share-locks on the file)
	if err := dst.Close(); err != nil {
		_ = os.Remove(dst.Name())
		return nil, 0, err
	}
	// Open the file again read-only (without locking the file against
	// deletion on Windows).
	converted, err = open(dst.Name())
	if err != nil {
		return nil, 0, err
	}

	// The position of the file's read pointer doesn't matter as all readers
	// will be accessing the file through its io.ReaderAt interface.
	size, err = converted.Seek(0, io.SeekEnd)
	if err != nil {
		_ = converted.Close()
		return nil, 0, err
	}
	return converted, size, nil
}

type sharedFileReader struct {
	*io.SectionReader

	c      *sharedTempFileConverter
	id     stfID
	closed bool
}

func (stf sharedTempFile) Reader(c *sharedTempFileConverter, id stfID) *sharedFileReader {
	rdr := &sharedFileReader{SectionReader: io.NewSectionReader(stf.fd, 0, stf.size), c: c, id: id}
	runtime.SetFinalizer(rdr, (*sharedFileReader).Close)
	return rdr
}

func (r *sharedFileReader) Close() error {
	if r.closed {
		return fs.ErrClosed
	}

	st := <-r.c.st
	flid, ok := st.fl[r.id]
	if !ok {
		panic("invariant violation: temp file state missing from map")
	}
	flid.ref--
	lastRef := flid.ref <= 0
	if lastRef {
		delete(st.fl, r.id)
	} else {
		st.fl[r.id] = flid
	}
	r.closed = true
	r.c.st <- st

	if lastRef {
		return flid.fd.Close()
	}
	runtime.SetFinalizer(r, nil)
	return nil
}
