package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils"

import (
	"io"
	"os"
	"time"

	"github.com/docker/docker/daemon/logger"
	"github.com/docker/docker/pkg/filenotify"
	"github.com/fsnotify/fsnotify"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

var errRetry = errors.New("retry")
var errDone = errors.New("done")

type follow struct {
	file                      *os.File
	dec                       Decoder
	fileWatcher               filenotify.FileWatcher
	logWatcher                *logger.LogWatcher
	notifyRotate, notifyEvict chan interface{}
	oldSize                   int64
	retries                   int
}

func (fl *follow) handleRotate() error {
	name := fl.file.Name()

	fl.file.Close()
	fl.fileWatcher.Remove(name)

	// retry when the file doesn't exist
	var err error
	for retries := 0; retries <= 5; retries++ {
		f, err := open(name)
		if err == nil || !os.IsNotExist(err) {
			fl.file = f
			break
		}
	}
	if err != nil {
		return err
	}
	if err := fl.fileWatcher.Add(name); err != nil {
		return err
	}
	fl.dec.Reset(fl.file)
	return nil
}

func (fl *follow) handleMustClose(evictErr error) {
	fl.file.Close()
	fl.dec.Close()
	fl.logWatcher.Err <- errors.Wrap(evictErr, "log reader evicted due to errors")
	logrus.WithField("file", fl.file.Name()).Error("Log reader notified that it must re-open log file, some log data may not be streamed to the client.")
}

func (fl *follow) waitRead() error {
	select {
	case e := <-fl.notifyEvict:
		if e != nil {
			err := e.(error)
			fl.handleMustClose(err)
		}
		return errDone
	case e := <-fl.fileWatcher.Events():
		switch e.Op {
		case fsnotify.Write:
			fl.dec.Reset(fl.file)
			return nil
		case fsnotify.Rename, fsnotify.Remove:
			select {
			case <-fl.notifyRotate:
			case <-fl.logWatcher.WatchProducerGone():
				return errDone
			case <-fl.logWatcher.WatchConsumerGone():
				return errDone
			}
			if err := fl.handleRotate(); err != nil {
				return err
			}
			return nil
		}
		return errRetry
	case err := <-fl.fileWatcher.Errors():
		logrus.Debugf("logger got error watching file: %v", err)
		// Something happened, let's try and stay alive and create a new watcher
		if fl.retries <= 5 {
			fl.fileWatcher.Close()
			fl.fileWatcher, err = watchFile(fl.file.Name())
			if err != nil {
				return err
			}
			fl.retries++
			return errRetry
		}
		return err
	case <-fl.logWatcher.WatchProducerGone():
		return errDone
	case <-fl.logWatcher.WatchConsumerGone():
		return errDone
	}
}

func (fl *follow) handleDecodeErr(err error) error {
	if !errors.Is(err, io.EOF) {
		return err
	}

	// Handle special case (#39235): max-file=1 and file was truncated
	st, stErr := fl.file.Stat()
	if stErr == nil {
		size := st.Size()
		defer func() { fl.oldSize = size }()
		if size < fl.oldSize { // truncated
			fl.file.Seek(0, 0)
			fl.dec.Reset(fl.file)
			return nil
		}
	} else {
		logrus.WithError(stErr).Warn("logger: stat error")
	}

	for {
		err := fl.waitRead()
		if err == nil {
			break
		}
		if err == errRetry {
			continue
		}
		return err
	}
	return nil
}

func (fl *follow) mainLoop(since, until time.Time) {
	for {
		select {
		case err := <-fl.notifyEvict:
			if err != nil {
				fl.handleMustClose(err.(error))
			}
			return
		default:
		}
		msg, err := fl.dec.Decode()
		if err != nil {
			if err := fl.handleDecodeErr(err); err != nil {
				if err == errDone {
					return
				}
				// we got an unrecoverable error, so return
				fl.logWatcher.Err <- err
				return
			}
			// ready to try again
			continue
		}

		fl.retries = 0 // reset retries since we've succeeded
		if !since.IsZero() && msg.Timestamp.Before(since) {
			continue
		}
		if !until.IsZero() && msg.Timestamp.After(until) {
			return
		}
		// send the message, unless the consumer is gone
		select {
		case e := <-fl.notifyEvict:
			if e != nil {
				err := e.(error)
				logrus.WithError(err).Debug("Reader evicted while sending log message")
				fl.logWatcher.Err <- err
			}
			return
		case fl.logWatcher.Msg <- msg:
		case <-fl.logWatcher.WatchConsumerGone():
			return
		}
	}
}

func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate, notifyEvict chan interface{}, dec Decoder, since, until time.Time) {
	dec.Reset(f)

	name := f.Name()
	fileWatcher, err := watchFile(name)
	if err != nil {
		logWatcher.Err <- err
		return
	}
	defer func() {
		f.Close()
		dec.Close()
		fileWatcher.Close()
	}()

	fl := &follow{
		file:         f,
		oldSize:      -1,
		logWatcher:   logWatcher,
		fileWatcher:  fileWatcher,
		notifyRotate: notifyRotate,
		notifyEvict:  notifyEvict,
		dec:          dec,
	}
	fl.mainLoop(since, until)
}
