// SPDX-License-Identifier: GPL-3.0-or-later

package logs

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"

	"github.com/netdata/netdata/go/plugins/logger"
)

const (
	maxEOF = 60
)

var (
	ErrNoMatchedFile = errors.New("no matched files")
)

// Reader is a log rotate aware Reader
// TODO: better reopen algorithm
// TODO: handle truncate
type Reader struct {
	file          *os.File
	path          string
	excludePath   string
	eofCounter    int
	continuousEOF int
	log           *logger.Logger
}

// Open a file and seek to end of the file.
// path: the shell file name pattern
// excludePath: the shell file name pattern
func Open(path string, excludePath string, log *logger.Logger) (*Reader, error) {
	var err error
	if path, err = filepath.Abs(path); err != nil {
		return nil, err
	}
	if _, err = filepath.Match(path, "/"); err != nil {
		return nil, fmt.Errorf("bad path syntax: %q", path)
	}
	if _, err = filepath.Match(excludePath, "/"); err != nil {
		return nil, fmt.Errorf("bad exclude_path syntax: %q", excludePath)
	}
	r := &Reader{
		path:        path,
		excludePath: excludePath,
		log:         log,
	}

	if err = r.open(); err != nil {
		return nil, err
	}
	return r, nil
}

// CurrentFilename get current opened file name
func (r *Reader) CurrentFilename() string {
	if r.file == nil {
		return ""
	}
	return r.file.Name()
}

func (r *Reader) open() error {
	path := r.findFile()
	if path == "" {
		r.log.Debugf("couldn't find log file, used path: '%s', exclude_path: '%s'", r.path, r.excludePath)
		return ErrNoMatchedFile
	}
	r.log.Debug("open log file: ", path)
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	stat, err := file.Stat()
	if err != nil {
		_ = file.Close()
		return err
	}
	if _, err = file.Seek(stat.Size(), io.SeekStart); err != nil {
		_ = file.Close()
		return err
	}
	r.file = file
	return nil
}

func (r *Reader) Read(p []byte) (n int, err error) {
	n, err = r.file.Read(p)
	if err != nil {
		switch {
		case err == io.EOF:
			err = r.handleEOFErr()
		case errors.Is(err, os.ErrInvalid): // r.file is nil after Close
			err = r.handleInvalidArgErr()
		}
		return
	}
	r.continuousEOF = 0
	return
}

func (r *Reader) handleEOFErr() (err error) {
	err = io.EOF
	r.eofCounter++
	r.continuousEOF++
	if r.eofCounter < maxEOF || r.continuousEOF < 2 {
		return err
	}
	if err2 := r.reopen(); err2 != nil {
		err = err2
	}
	return err
}

func (r *Reader) handleInvalidArgErr() (err error) {
	err = io.EOF
	if err2 := r.reopen(); err2 != nil {
		err = err2
	}
	return err
}

func (r *Reader) Close() (err error) {
	if r == nil || r.file == nil {
		return
	}
	r.log.Debug("close log file: ", r.file.Name())
	err = r.file.Close()
	r.file = nil
	r.continuousEOF = 0
	r.eofCounter = 0
	return
}

func (r *Reader) reopen() error {
	r.log.Debugf("reopen, look for: %s", r.path)
	_ = r.Close()
	return r.open()
}

func (r *Reader) findFile() string {
	return find(r.path, r.excludePath)
}

func find(path, exclude string) string {
	return finder{}.find(path, exclude)
}

// TODO: tests
type finder struct{}

func (f finder) find(path, exclude string) string {
	files, _ := filepath.Glob(path)
	if len(files) == 0 {
		return ""
	}

	files = f.filter(files, exclude)
	if len(files) == 0 {
		return ""
	}

	return f.findLastFile(files)
}

func (f finder) filter(files []string, exclude string) []string {
	if exclude == "" {
		return files
	}

	fs := make([]string, 0, len(files))
	for _, file := range files {
		if ok, _ := filepath.Match(exclude, file); ok {
			continue
		}
		fs = append(fs, file)
	}
	return fs
}

// TODO: the logic is probably wrong
func (f finder) findLastFile(files []string) string {
	sort.Strings(files)
	for i := len(files) - 1; i >= 0; i-- {
		stat, err := os.Stat(files[i])
		if err != nil || !stat.Mode().IsRegular() {
			continue
		}
		return files[i]
	}
	return ""
}
