package forcedstatus

import (
	"fmt"
	"log"
	"path"
	"strconv"

	"github.com/fsnotify/fsnotify"
)

type StatusWatcher interface {
	Close() error
	GetStatus(port string) *administrativeStatus
}

type administrativeStatus struct {
	healthy      bool
	healthReason string
}

func NewStatusWatcher(ports []int, reasonService ReasonService) (StatusWatcher, error) {
	internalWatcher, _ := fsnotify.NewWatcher()
	return newStatusWatcherFromFileWatcher(ports, reasonService, &fsnotifyFileWatcher{internalFileWatcher: internalWatcher})
}

//We need this for unit tests, so we can mock out the file watcher.  Might be preferable to set up a composition root in
//pg-healthcheck but for now~~~
func newStatusWatcherFromFileWatcher(ports []int, reasonService ReasonService, fileWatcher FileWatcher) (StatusWatcher, error) {
	statusMap := make(map[string]*administrativeStatus)

	//Make an ./all directory & watch it
	err := reasonService.MkdirIfNecessary("all")
	if err != nil {
		return nil, err
	}
	err = fileWatcher.Add("./all")
	if err != nil {
		return nil, err
	}

	//Make a directory for each healthchecked port & watch it
	for _, port := range ports {
		portStr := strconv.Itoa(port)
		statusMap[portStr] = nil

		err = reasonService.MkdirIfNecessary(portStr)
		if err != nil {
			return nil, err
		}

		err = fileWatcher.Add(fmt.Sprintf("./%s", portStr))
		if err != nil {
			return nil, err
		}
	}

	watcher := &fileStatusWatcher{
		internalWatcher: fileWatcher,
		statusMap:       statusMap,
		reasonService:   reasonService,
	}

	//Set the initial state on forced statuses
	err = watcher.refreshPort("all", false)
	if err != nil {
		return nil, err
	}
	for _, port := range ports {
		err = watcher.refreshPort(strconv.Itoa(port), false)
		if err != nil {
			return nil, err
		}
	}

	//Start some goroutines to pull file system events from the watcher & process them
	go watcher.workFileEvents()
	go watcher.workWatcherErrors()

	return watcher, nil
}

type fileStatusWatcher struct {
	internalWatcher FileWatcher
	rootStatus      *administrativeStatus
	statusMap       map[string]*administrativeStatus
	reasonService   ReasonService
}

//Close the internal filewatcher, which has some channels and whatnot that need cleaning up
func (watcher *fileStatusWatcher) Close() error {
	return watcher.internalWatcher.Close()
}

//GetStatus grabs the current forced status state of the underlying reason service.
//The forcedstatuschecker uses this to determine what the forced state currently is.
//nil indicates that the underlying checkers' status should be used
func (watcher *fileStatusWatcher) GetStatus(port string) *administrativeStatus {
	status, ok := watcher.statusMap[port]

	if ok && status != nil {
		return status
	}

	return watcher.rootStatus
}

//This is run as a goroutine to pull fsnotify events off a channel
//and process them into updated statuses
func (watcher *fileStatusWatcher) workFileEvents() {
	for evt := range watcher.internalWatcher.Events() {
		scope, healthStr := getFileProps(evt.Name)

		var healthy bool
		if healthStr == "healthy" {
			healthy = true
		} else if healthStr == "unhealthy" {
			healthy = false
		} else {
			continue
		}

		var err error
		if evt.Op == fsnotify.Rename || evt.Op == fsnotify.Remove {
			err = watcher.refreshPort(scope, !healthy)
		} else {
			err = watcher.refreshPort(scope, healthy)
		}

		if err != nil {
			log.Println(err)
		}
	}
}

//fsnotify has an errors channel that reports errors, so we just log all of them
func (watcher *fileStatusWatcher) workWatcherErrors() {
	for err := range watcher.internalWatcher.Errors() {
		log.Println(err)
	}
}

//Try to update the current forced status data based on the state of the reasonservice-
//Each call to verifyReason potentially hits the file system via the ReasonService,
//so this method accepts a call to the file that most likely has the new correct reason
//to try and reduce hits.
func (watcher *fileStatusWatcher) refreshPort(scope string, tryFirst bool) error {
	reason, err := watcher.verifyReason(scope, tryFirst)
	if err != nil {
		return err
	}

	if reason == "" {
		reason, err = watcher.verifyReason(scope, !tryFirst)
		if err != nil {
			return err
		}
	}

	if reason == "" {
		if scope == "all" {
			watcher.rootStatus = nil
		} else {
			delete(watcher.statusMap, scope)
		}
	}

	return nil
}

//Check a file on the file system, looking for a live health reason.  If we find one, make
//that the current health/reason, otherwise wipe out the file if it exists
func (watcher *fileStatusWatcher) verifyReason(scope string, healthy bool) (string, error) {
	healthReason := watcher.reasonService.GetReason(scope, healthy)

	if healthReason != "" {
		err := watcher.reasonService.Cleanup(scope, !healthy)
		if err != nil {
			return "", err
		}

		watcher.setHealth(scope, healthy, healthReason)
	} else {
		err := watcher.reasonService.Cleanup(scope, healthy)
		if err != nil {
			return "", err
		}
	}

	return healthReason, nil
}

//Set the current health status
func (watcher *fileStatusWatcher) setHealth(scope string, healthy bool, reason string) {
	status := &administrativeStatus{
		healthy:      healthy,
		healthReason: reason,
	}

	if scope == "all" {
		watcher.rootStatus = status
	} else {
		watcher.statusMap[scope] = status
	}
}

//Grab the port/scope & health value from a health file path
func getFileProps(pathStr string) (scope string, healthyStr string) {
	return path.Base(path.Dir(pathStr)), path.Base(pathStr)
}

//This isn't great, but I have to do it.  I'd like to be able to mock out the filewatcher in tests,
//but fsnotify exports structs instead of interfaces.  So I could make my own interface, but
//fsnotify's watcher struct exports Events/Errors as a FIELD instead of a method, so I also have
//to make my own struct to wrap it.  :\ :\ :\

type FileWatcher interface {
	Close() error
	Add(name string) error
	Events() chan fsnotify.Event
	Errors() chan error
}

type fsnotifyFileWatcher struct {
	internalFileWatcher *fsnotify.Watcher
}

func (fw *fsnotifyFileWatcher) Close() error {
	return fw.internalFileWatcher.Close()
}

func (fw *fsnotifyFileWatcher) Add(name string) error {
	return fw.internalFileWatcher.Add(name)
}

func (fw *fsnotifyFileWatcher) Events() chan fsnotify.Event {
	return fw.internalFileWatcher.Events
}

func (fw *fsnotifyFileWatcher) Errors() chan error {
	return fw.internalFileWatcher.Errors
}
