package accesslog

import (
	"bytes"
	"errors"
	"io"
)

type ReaderAtSeeker interface {
	io.ReaderAt
	io.Seeker
}

// BackScanner provides an interface to read a file backward line by line.
type BackScanner struct {
	file      ReaderAtSeeker
	size      int64
	chunkSize int
	chunkBuf  []byte

	offset int64
	chunk  []byte
	line   []byte
	err    error
}

// NewBackScanner creates a new Scanner to read the file backward.
// chunkSize determines the size of each read chunk from the end of the file.
func NewBackScanner(file ReaderAtSeeker, fileSize int64, chunkSize int) *BackScanner {
	return newBackScanner(file, fileSize, make([]byte, chunkSize))
}

func newBackScanner(file ReaderAtSeeker, fileSize int64, buf []byte) *BackScanner {
	return &BackScanner{
		file:      file,
		size:      fileSize,
		offset:    fileSize,
		chunkSize: len(buf),
		chunkBuf:  buf,
	}
}

// Scan advances the scanner to the previous line, which will then be available
// via the Bytes method. It returns false when there are no more lines.
func (s *BackScanner) Scan() bool {
	if s.err != nil {
		return false
	}

	// Read chunks until a newline is found or the file is fully read
	for {
		// Check if there's a line in the buffer
		if idx := bytes.LastIndexByte(s.chunk, '\n'); idx >= 0 {
			s.line = s.chunk[idx+1:]
			s.chunk = s.chunk[:idx]
			if len(s.line) > 0 {
				return true
			}
			continue
		}

		for {
			if s.offset <= 0 {
				// No more data to read; check remaining buffer
				if len(s.chunk) > 0 {
					s.line = s.chunk
					s.chunk = nil
					return true
				}
				return false
			}

			newOffset := max(0, s.offset-int64(s.chunkSize))
			chunkSize := s.offset - newOffset
			chunk := s.chunkBuf[:chunkSize]

			n, err := s.file.ReadAt(chunk, newOffset)
			if err != nil {
				if !errors.Is(err, io.EOF) {
					s.err = err
				}
				return false
			} else if n == 0 {
				return false
			}

			// Prepend the chunk to the buffer
			clone := append([]byte{}, chunk[:n]...)
			s.chunk = append(clone, s.chunk...)
			s.offset = newOffset

			// Check for newline in the updated buffer
			if idx := bytes.LastIndexByte(s.chunk, '\n'); idx >= 0 {
				s.line = s.chunk[idx+1:]
				s.chunk = s.chunk[:idx]
				if len(s.line) > 0 {
					return true
				}
				break
			}
		}
	}
}

// Bytes returns the most recent line generated by a call to Scan.
func (s *BackScanner) Bytes() []byte {
	return s.line
}

// Err returns the first non-EOF error encountered by the scanner.
func (s *BackScanner) Err() error {
	return s.err
}

func (s *BackScanner) Reset() error {
	_, err := s.file.Seek(0, io.SeekStart)
	if err != nil {
		return err
	}
	*s = *newBackScanner(s.file, s.size, s.chunkBuf)
	return nil
}