// Ref: https://github.com/natefinch/lumberjack
package log

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"go.uber.org/multierr"
)

// FileLogger is an io.WriteCloser that writes to the specified filename.
type FileLogger struct {
	Filename string
	mu       sync.Mutex
	file     *os.File
}

// Write implements io.Writer.
func (l *FileLogger) Write(p []byte) (n int, err error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	if l.file == nil {
		if err = l.openExistingOrNew(); err != nil {
			return 0, err
		}
	}

	n, err = l.file.Write(p)
	return n, err
}

// Reopen implements io.Closer, and reopens the current logfile by simply closing it.
func (l *FileLogger) Reopen() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.close()
}

// openExistingOrNew opens the logfile if it exists. If there is no such file,
// a new file is created. Must be called when l.mu mutex is acquired.
func (l *FileLogger) openExistingOrNew() error {
	_, err := os.Stat(l.Filename)
	if os.IsNotExist(err) {
		return l.openNew()
	}
	if err != nil {
		return fmt.Errorf("error getting log file info: %s", err)
	}

	file, err := os.OpenFile(l.Filename, os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		// If we fail to open the old log file for some reason, just ignore
		// it and open a new log file.
		return l.openNew()
	}
	l.file = file
	return nil
}

// openNew opens a new log file for writing, moving any old log file out of the
// way. This methods assumes the file has already been closed.
func (l *FileLogger) openNew() error {
	err := os.MkdirAll(l.dir(), 0755)
	if err != nil {
		return fmt.Errorf("can't make directories for new logfile: %s", err)
	}

	// We use truncate here because this should only get called when we've closed
	// the renamed (by logrotate) file ourselves. If someone else creates the file
	// in the meantime, just wipe out the contents.
	f, err := os.OpenFile(l.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0644))
	if err != nil {
		return fmt.Errorf("can't open new logfile: %s", err)
	}
	l.file = f
	return nil
}

// dir returns the directory for the current filename.
func (l *FileLogger) dir() string {
	return filepath.Dir(l.Filename)
}

// close closes the file if it is open. Must be called when l.mu mutex is acquired.
func (l *FileLogger) close() error {
	if l.file == nil {
		return nil
	}
	err := l.file.Close()
	l.file = nil
	return err
}

type FileLoggers []*FileLogger

// Reopen all holding file loggers
func (fls FileLoggers) Reopen() error {
	errors := make([]error, len(fls))
	for _, fileLogger := range fls {
		if err := fileLogger.Reopen(); err != nil {
			errors = append(errors, err)
		}
	}
	if len(errors) > 0 {
		return multierr.Combine(errors...)
	} else {
		return nil
	}
}
