package sshlog

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/sirupsen/logrus"
)

const (
	logTimeFormat        = "2006-01-02T15-04-05.000"
	megabyte             = 1024 * 1024
	defaultFileSizeLimit = 100 * megabyte
)

// Logger will buffer and write events to disk
type Logger struct {
	sync.Mutex
	filename      string
	file          *os.File
	writeBuffer   *bufio.Writer
	logger        *logrus.Logger
	flushInterval time.Duration
	maxFileSize   int64
	done          chan struct{}
	once          sync.Once
}

// NewLogger creates a Logger instance. A buffer is created that needs to be
// drained and closed when the caller is finished, so instances should call
// Close when finished with this Logger instance. Writes will be flushed to disk
// every second (fsync). filename is the name of the logfile to be created. The
// logger variable is a logrus that will log all i/o, filesystem error etc, that
// that shouldn't end execution of the logger, but are useful to report to the
// caller.
func NewLogger(filename string, logger *logrus.Logger, flushInterval time.Duration, maxFileSize int64) (*Logger, error) {
	if logger == nil {
		return nil, errors.New("logger can't be nil")
	}
	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
	if err != nil {
		return nil, err
	}
	l := &Logger{filename: filename,
		file:          f,
		writeBuffer:   bufio.NewWriter(f),
		logger:        logger,
		flushInterval: flushInterval,
		maxFileSize:   maxFileSize,
		done:          make(chan struct{}),
	}

	go l.writer()
	return l, nil
}

// Writes to a log buffer. Implements the io.Writer interface.
func (l *Logger) Write(p []byte) (n int, err error) {
	l.Lock()
	defer l.Unlock()
	return l.writeBuffer.Write(p)
}

// Close drains anything left in the buffer and cleans up any resources still
// in use.
func (l *Logger) Close() error {
	l.once.Do(func() {
		close(l.done)
	})
	if err := l.write(); err != nil {
		return err
	}
	return l.file.Close()
}

// writer is the run loop that handles draining the write buffer and syncing
// data to disk.
func (l *Logger) writer() {
	ticker := time.NewTicker(l.flushInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			if err := l.write(); err != nil {
				l.logger.Errorln(err)
			}
		case <-l.done:
			return
		}
	}
}

// write does the actual system write calls to disk and does a rotation if the
// file size limit has been reached. Since the rotation happens at the end,
// the rotation is a soft limit (aka the file can be bigger than the max limit
// because of the final buffer flush)
func (l *Logger) write() error {
	l.Lock()
	defer l.Unlock()

	if l.writeBuffer.Buffered() <= 0 {
		return nil
	}

	if err := l.writeBuffer.Flush(); err != nil {
		return err
	}

	if err := l.file.Sync(); err != nil {
		return err
	}

	if l.shouldRotate() {
		return l.rotate()
	}
	return nil
}

// shouldRotate checks to see if the current file should be rotated to a new
// logfile.
func (l *Logger) shouldRotate() bool {
	info, err := l.file.Stat()
	if err != nil {
		return false
	}

	return info.Size() >= l.maxFileSize
}

// rotate creates a new logfile with the existing filename and renames the
// existing file with a current timestamp.
func (l *Logger) rotate() error {
	if err := l.file.Close(); err != nil {
		return err
	}

	// move the existing file
	newname := rotationName(l.filename)
	if err := os.Rename(l.filename, newname); err != nil {
		return fmt.Errorf("can't rename log file: %s", err)
	}

	f, err := os.OpenFile(l.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
	if err != nil {
		return fmt.Errorf("failed to open new logfile %s", err)
	}
	l.file = f
	l.writeBuffer = bufio.NewWriter(f)
	return nil
}

// rotationName creates a new filename from the given name, inserting a timestamp
// between the filename and the extension.
func rotationName(name string) string {
	dir := filepath.Dir(name)
	filename := filepath.Base(name)
	ext := filepath.Ext(filename)
	prefix := filename[:len(filename)-len(ext)]
	t := time.Now()
	timestamp := t.Format(logTimeFormat)
	return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
}
