package service

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

	"github.com/fsnotify/fsnotify"
	"github.com/sirupsen/logrus"

	"github.com/authelia/authelia/v4/internal/authentication"
)

func ProvisionUsersFileWatcher(ctx Context) (service Provider, err error) {
	config := ctx.GetConfiguration()
	providers := ctx.GetProviders()

	if config.AuthenticationBackend.File != nil && config.AuthenticationBackend.File.Watch {
		provider, ok := providers.UserProvider.(*authentication.FileUserProvider)

		if !ok {
			return nil, errors.New("error occurred asserting user provider")
		}

		if service, err = NewFileWatcher("users", config.AuthenticationBackend.File.Path, provider, ctx.GetLogger()); err != nil {
			return nil, err
		}
	}

	return service, nil
}

// NewFileWatcher creates a new FileWatcher with the appropriate logger etc.
func NewFileWatcher(name, path string, reload ReloadableProvider, log *logrus.Entry) (service *FileWatcher, err error) {
	if path == "" {
		return nil, fmt.Errorf("error initializing file watcher: path must be specified")
	}

	if path, err = filepath.Abs(path); err != nil {
		return nil, fmt.Errorf("error initializing file watcher: could not determine the absolute path of file '%s': %w", path, err)
	}

	var info os.FileInfo

	if info, err = os.Stat(path); err != nil {
		switch {
		case os.IsNotExist(err):
			return nil, fmt.Errorf("error initializing file watcher: error stating file '%s': file does not exist", path)
		case os.IsPermission(err):
			return nil, fmt.Errorf("error initializing file watcher: error stating file '%s': permission denied trying to read the file", path)
		default:
			return nil, fmt.Errorf("error initializing file watcher: error stating file '%s': %w", path, err)
		}
	}

	var watcher *fsnotify.Watcher

	if watcher, err = fsnotify.NewWatcher(); err != nil {
		return nil, err
	}

	entry := log.WithFields(map[string]any{logFieldService: serviceTypeWatcher, serviceTypeWatcher: name})

	if info.IsDir() {
		service = &FileWatcher{
			name:      name,
			watcher:   watcher,
			reload:    reload,
			log:       entry,
			directory: filepath.Clean(path),
		}
	} else {
		service = &FileWatcher{
			name:      name,
			watcher:   watcher,
			reload:    reload,
			log:       entry,
			directory: filepath.Dir(path),
			file:      filepath.Base(path),
		}
	}

	if err = service.watcher.Add(service.directory); err != nil {
		return nil, fmt.Errorf("failed to add path '%s' to watch list: %w", path, err)
	}

	return service, nil
}

// FileWatcher is a Provider that watches files for changes.
type FileWatcher struct {
	name string

	watcher *fsnotify.Watcher
	reload  ReloadableProvider

	log       *logrus.Entry
	file      string
	directory string
}

// ServiceType returns the service type for this service, which is always 'watcher'.
func (service *FileWatcher) ServiceType() string {
	return serviceTypeWatcher
}

// ServiceName returns the individual name for this service.
func (service *FileWatcher) ServiceName() string {
	return service.name
}

// Run the FileWatcher.
func (service *FileWatcher) Run() (err error) {
	defer func() {
		if r := recover(); r != nil {
			service.log.WithError(recoverErr(r)).Error("Critical error caught (recovered)")
		}
	}()

	service.log.WithField(logFieldFile, filepath.Join(service.directory, service.file)).Info("Watching file for changes")

	for {
		select {
		case event, ok := <-service.watcher.Events:
			if !ok {
				return nil
			}

			log := service.log.WithFields(map[string]any{logFieldFile: event.Name, logFieldOP: event.Op})

			if service.file != "" && service.file != filepath.Base(event.Name) {
				log.Trace("File modification detected to irrelevant file")
				break
			}

			switch {
			case event.Op&fsnotify.Write == fsnotify.Write, event.Op&fsnotify.Create == fsnotify.Create:
				log.Debug("File modification was detected")

				var reloaded bool

				switch reloaded, err = service.reload.Reload(); {
				case err != nil:
					log.WithError(err).Error("Error occurred during reload")
				case reloaded:
					log.Info("Reloaded successfully")
				default:
					log.Debug("Reload was triggered but it was skipped")
				}
			case event.Op&fsnotify.Remove == fsnotify.Remove:
				log.Debug("File remove was detected")
			}
		case err, ok := <-service.watcher.Errors:
			if !ok {
				return nil
			}

			service.log.WithError(err).Error("Error while watching file for changes")
		}
	}
}

// Shutdown the FileWatcher.
func (service *FileWatcher) Shutdown() {
	if err := service.watcher.Close(); err != nil {
		service.log.WithError(err).Error("Error occurred during shutdown")
	}
}

// Log returns the *logrus.Entry of the FileWatcher.
func (service *FileWatcher) Log() *logrus.Entry {
	return service.log
}
