////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation                                             //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file.                                                              //
////////////////////////////////////////////////////////////////////////////////

//go:build js && wasm

package logging

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"github.com/armon/circbuf"
	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"gitlab.com/elixxir/xxdk-wasm/utils"
	"gitlab.com/elixxir/xxdk-wasm/worker"
	"io"
	"strconv"
	"sync/atomic"
	"syscall/js"
	"time"
)

const (
	// DefaultInitThreshold is the log threshold used for the initial log before
	// any logging options is set.
	DefaultInitThreshold = jww.LevelTrace

	// logListenerChanSize is the size of the listener channel that stores log
	// messages before they are written.
	logListenerChanSize = 1500
)

// List of tags that can be used when sending a message or registering a handler
// to receive a message.
const (
	NewLogFileTag worker.Tag = "NewLogFile"
	WriteLogTag   worker.Tag = "WriteLog"
	GetFileTag    worker.Tag = "GetFile"
	GetFileExtTag worker.Tag = "GetFileExt"
	SizeTag       worker.Tag = "Size"
)

// logger is the global that all jwalterweatherman logging is sent to.
var logger *Logger

// Logger manages the recording of jwalterweatherman logs. It can write logs to
// a local, in-memory buffer or to an external worker.
type Logger struct {
	threshold      jww.Threshold
	maxLogFileSize int
	logListenerID  uint64

	listenChan  chan []byte
	mode        atomic.Uint32
	processQuit chan struct{}

	cb *circbuf.Buffer
	wm *worker.Manager
}

// InitLogger initializes the logger. Include this in the init function in main.
func InitLogger() *Logger {
	logger = NewLogger()
	return logger
}

// GetLogger returns the Logger object, used to manager where logging is
// recorded.
func GetLogger() *Logger {
	return logger
}

// NewLogger creates a new Logger that begins storing the first
// DefaultInitThreshold log entries. If either the log file or log worker is
// enabled, then these logs are redirected to the set destination. If the
// channel fills up with no log recorder enabled, then the listener is disabled.
func NewLogger() *Logger {
	lf := newLogger()

	// Add the log listener
	lf.logListenerID = AddLogListener(lf.Listen)

	jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+
		"at threshold %s that can store %d entries",
		lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan))

	return lf
}

// newLogger initialises a Logger without adding it as a log listener.
func newLogger() *Logger {
	lf := &Logger{
		threshold:   DefaultInitThreshold,
		listenChan:  make(chan []byte, logListenerChanSize),
		mode:        atomic.Uint32{},
		processQuit: make(chan struct{}),
	}
	lf.setMode(initMode)

	return lf
}

// LogToFile starts logging to a local, in-memory log file.
func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error {
	err := l.prepare(threshold, maxLogFileSize, fileMode)
	if err != nil {
		return err
	}

	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
	if err != nil {
		return err
	}
	l.cb = b

	sendLog := func(p []byte) {
		if n, err2 := l.cb.Write(p); err2 != nil {
			jww.ERROR.Printf(
				"[LOG] Error writing log to circular buffer: %+v", err2)
		} else if n != len(p) {
			jww.ERROR.Printf(
				"[LOG] Wrote %d bytes when %d bytes expected", n, len(p))
		}
	}
	go l.processLog(workerMode, sendLog, l.processQuit)

	return nil
}

// LogToFileWorker starts a new worker that begins listening for logs and
// writing them to file. This function blocks until the worker has started.
func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int,
	wasmJsPath, workerName string) error {
	err := l.prepare(threshold, maxLogFileSize, workerMode)
	if err != nil {
		return err
	}

	// Create new worker manager, which will start the worker and wait until
	// communication has been established
	wm, err := worker.NewManager(wasmJsPath, workerName, false)
	if err != nil {
		return err
	}
	l.wm = wm

	// Register the callback used by the Javascript to request the log file.
	// This prevents an error print when GetFileExtTag is not registered.
	l.wm.RegisterCallback(GetFileExtTag, func([]byte) {
		jww.DEBUG.Print("[LOG] Received file requested from external " +
			"Javascript. Ignoring file.")
	})

	data, err := json.Marshal(l.maxLogFileSize)
	if err != nil {
		return err
	}

	// Send message to initialize the log file listener
	errChan := make(chan error)
	l.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
		if len(data) > 0 {
			errChan <- errors.New(string(data))
		} else {
			errChan <- nil
		}
	})

	// Wait for worker to respond
	select {
	case err = <-errChan:
		if err != nil {
			return err
		}
	case <-time.After(worker.ResponseTimeout):
		return errors.Errorf("timed out after %s waiting for new log "+
			"file in worker to initialize", worker.ResponseTimeout)
	}

	jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName)

	sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) }
	go l.processLog(workerMode, sendLog, l.processQuit)

	return nil
}

// processLog processes the log messages sent to the listener channel and sends
// them to the appropriate recorder.
func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) {
	jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m)

	for {
		select {
		case <-quit:
			jww.INFO.Printf("[LOG] Stopping log file processing thread.")
			return
		case p := <-l.listenChan:
			go sendLog(p)
		}
	}
}

// prepare sets the threshold, maxLogFileSize, and mode of the logger and
// prints a log message indicating this information.
func (l *Logger) prepare(
	threshold jww.Threshold, maxLogFileSize int, m mode) error {
	if m := l.getMode(); m != initMode {
		return errors.Errorf("log already set to %s", m)
	} else if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
		return errors.Errorf("log level of %d is invalid", threshold)
	}

	l.threshold = threshold
	l.maxLogFileSize = maxLogFileSize
	l.setMode(m)

	msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+
		"with level %s", m, l.MaxSize(), l.Threshold())
	switch l.Threshold() {
	case jww.LevelTrace:
		fallthrough
	case jww.LevelDebug:
		fallthrough
	case jww.LevelInfo:
		jww.INFO.Print(msg)
	case jww.LevelWarn:
		jww.WARN.Print(msg)
	case jww.LevelError:
		jww.ERROR.Print(msg)
	case jww.LevelCritical:
		jww.CRITICAL.Print(msg)
	case jww.LevelFatal:
		jww.FATAL.Print(msg)
	}

	return nil
}

// StopLogging stops the logging of log messages and disables the log listener.
// If the log worker is running, it is terminated. Once logging is stopped, it
// cannot be resumed the log file cannot be recovered.
func (l *Logger) StopLogging() {
	jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID)
	RemoveLogListener(l.logListenerID)

	switch l.getMode() {
	case workerMode:
		go l.wm.Terminate()
		jww.DEBUG.Printf("[LOG] Terminated log worker.")
	case fileMode:
		jww.DEBUG.Printf("[LOG] Reset circular buffer.")
		l.cb.Reset()
	}

	select {
	case l.processQuit <- struct{}{}:
		jww.DEBUG.Printf("[LOG] Sent quit channel to log process.")
	default:
		jww.DEBUG.Printf("[LOG] Failed to stop log processes.")
	}
}

// GetFile returns the entire log file.
//
// If the log file is listening locally, it returns it from the local buffer. If
// it is listening from the worker, it blocks until the file is returned.
func (l *Logger) GetFile() []byte {
	switch l.getMode() {
	case fileMode:
		return l.cb.Bytes()
	case workerMode:
		fileChan := make(chan []byte)
		l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })

		select {
		case file := <-fileChan:
			return file
		case <-time.After(worker.ResponseTimeout):
			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
				"file from worker", worker.ResponseTimeout)
			return nil
		}
	default:
		return nil
	}
}

// Threshold returns the log level threshold used in the file.
func (l *Logger) Threshold() jww.Threshold {
	return l.threshold
}

// MaxSize returns the max size, in bytes, that the log file is allowed to be.
func (l *Logger) MaxSize() int {
	return l.maxLogFileSize
}

// Size returns the current size, in bytes, written to the log file.
//
// If the log file is listening locally, it returns it from the local buffer. If
// it is listening from the worker, it blocks until the size is returned.
func (l *Logger) Size() int {
	switch l.getMode() {
	case fileMode:
		return int(l.cb.Size())
	case workerMode:
		sizeChan := make(chan []byte)
		l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })

		select {
		case data := <-sizeChan:
			return int(jww.Threshold(binary.LittleEndian.Uint64(data)))
		case <-time.After(worker.ResponseTimeout):
			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
				"file size from worker", worker.ResponseTimeout)
			return 0
		}
	default:
		return 0
	}
}

////////////////////////////////////////////////////////////////////////////////
// JWW Listener                                                               //
////////////////////////////////////////////////////////////////////////////////

// Listen is called for every logging event. This function adheres to the
// [jwalterweatherman.LogListener] type.
func (l *Logger) Listen(t jww.Threshold) io.Writer {
	if t < l.threshold {
		return nil
	}

	return l
}

// Write sends the bytes to the listener channel. It always returns the length
// of p and a nil error. This function adheres to the io.Writer interface.
func (l *Logger) Write(p []byte) (n int, err error) {
	select {
	case l.listenChan <- append([]byte{}, p...):
	default:
		jww.ERROR.Printf(
			"[LOG] Logger channel filled. Log file recording stopping.")
		l.StopLogging()
		return 0, errors.Errorf(
			"Logger channel filled. Log file recording stopping.")
	}
	return len(p), nil
}

////////////////////////////////////////////////////////////////////////////////
// Log File Mode                                                              //
////////////////////////////////////////////////////////////////////////////////

// mode represents the state of the Logger.
type mode uint32

const (
	initMode mode = iota
	fileMode
	workerMode
)

func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) }
func (l *Logger) getMode() mode  { return mode(l.mode.Load()) }

// String returns a human-readable representation of the mode for logging and
// debugging. This function adheres to the fmt.Stringer interface.
func (m mode) String() string {
	switch m {
	case initMode:
		return "uninitialized mode"
	case fileMode:
		return "file mode"
	case workerMode:
		return "worker mode"
	default:
		return "invalid mode: " + strconv.Itoa(int(m))
	}
}

////////////////////////////////////////////////////////////////////////////////
// Javascript Bindings                                                        //
////////////////////////////////////////////////////////////////////////////////

// GetLoggerJS returns the Logger object, used to manager where logging is
// recorded and accessing the log file.
//
// Returns:
//   - A Javascript representation of the [Logger] object.
func GetLoggerJS(js.Value, []js.Value) any {
	return newLoggerJS(GetLogger())
}

// newLoggerJS creates a new Javascript compatible object (map[string]any) that
// matches the [Logger] structure.
func newLoggerJS(lfw *Logger) map[string]any {
	logFileWorker := map[string]any{
		"LogToFile":       js.FuncOf(lfw.LogToFileJS),
		"LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS),
		"StopLogging":     js.FuncOf(lfw.StopLoggingJS),
		"GetFile":         js.FuncOf(lfw.GetFileJS),
		"Threshold":       js.FuncOf(lfw.ThresholdJS),
		"MaxSize":         js.FuncOf(lfw.MaxSizeJS),
		"Size":            js.FuncOf(lfw.SizeJS),
		"Worker":          js.FuncOf(lfw.WorkerJS),
	}

	return logFileWorker
}

// LogToFileJS starts logging to a local, in-memory log file.
//
// Parameters:
//   - args[0] - Log level (int).
//   - args[1] - Max log file size, in bytes (int).
//
// Returns:
//   - Throws a TypeError if starting the log file fails.
func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any {
	threshold := jww.Threshold(args[0].Int())
	maxLogFileSize := args[1].Int()

	err := l.LogToFile(threshold, maxLogFileSize)
	if err != nil {
		utils.Throw(utils.TypeError, err)
		return nil
	}

	return nil
}

// LogToFileWorkerJS starts a new worker that begins listening for logs and
// writing them to file. This function blocks until the worker has started.
//
// Parameters:
//   - args[0] - Log level (int).
//   - args[1] - Max log file size, in bytes (int).
//   - args[2] - Path to Javascript start file for the worker WASM (string).
//   - args[3] - Name of the worker (used in logs) (string).
//
// Returns a promise:
//   - Resolves to nothing on success (void).
//   - Rejected with an error if starting the worker fails.
func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any {
	threshold := jww.Threshold(args[0].Int())
	maxLogFileSize := args[1].Int()
	wasmJsPath := args[2].String()
	workerName := args[3].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := l.LogToFileWorker(
			threshold, maxLogFileSize, wasmJsPath, workerName)
		if err != nil {
			reject(utils.JsTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// StopLoggingJS stops the logging of log messages and disables the log
// listener. If the log worker is running, it is terminated. Once logging is
// stopped, it cannot be resumed the log file cannot be recovered.
func (l *Logger) StopLoggingJS(js.Value, []js.Value) any {
	l.StopLogging()

	return nil
}

// GetFileJS returns the entire log file.
//
// If the log file is listening locally, it returns it from the local buffer. If
// it is listening from the worker, it blocks until the file is returned.
//
// Returns a promise:
//   - Resolves to the log file contents (string).
func (l *Logger) GetFileJS(js.Value, []js.Value) any {
	promiseFn := func(resolve, _ func(args ...any) js.Value) {
		resolve(string(l.GetFile()))
	}

	return utils.CreatePromise(promiseFn)
}

// ThresholdJS returns the log level threshold used in the file.
//
// Returns:
//   - Log level (int).
func (l *Logger) ThresholdJS(js.Value, []js.Value) any {
	return int(l.Threshold())
}

// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be.
//
// Returns:
//   - Max file size (int).
func (l *Logger) MaxSizeJS(js.Value, []js.Value) any {
	return l.MaxSize()
}

// SizeJS returns the current size, in bytes, written to the log file.
//
// If the log file is listening locally, it returns it from the local buffer. If
// it is listening from the worker, it blocks until the size is returned.
//
// Returns a promise:
//   - Resolves to the current file size (int).
func (l *Logger) SizeJS(js.Value, []js.Value) any {
	promiseFn := func(resolve, _ func(args ...any) js.Value) {
		resolve(l.Size())
	}

	return utils.CreatePromise(promiseFn)
}

// WorkerJS returns the web worker object.
//
// Returns:
//   - Javascript worker object. If the worker has not been initialized, it
//     returns null.
func (l *Logger) WorkerJS(js.Value, []js.Value) any {
	if l.getMode() == workerMode {
		return l.wm.GetWorker()
	}

	return js.Null()
}