package main

import (
	"flag"
	"fmt"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"

	"golang.org/x/sys/unix"

	"git.csclub.uwaterloo.ca/public/merlin/arthur"
	"git.csclub.uwaterloo.ca/public/merlin/config"
	"git.csclub.uwaterloo.ca/public/merlin/logger"
	"git.csclub.uwaterloo.ca/public/merlin/sync"
)

var DEFAULT_CONFIG_PATH = "merlin-config.ini"

func main() {

	// custom help message
	flag.Usage = func() {
		w := flag.CommandLine.Output()

		fmt.Fprintf(w, "USAGE: merlin [-h | --help] [--config=<config-path>]\n")
		flag.PrintDefaults()
	}

	// parse command args
	// if more are added can prob use a library
	configPath := flag.String("config", DEFAULT_CONFIG_PATH, "alternate config file")
	flag.Parse()

	// check the user that program is running under (should be mirror)
	if os.Getuid() == 0 {
		panic("Merlin should not be run as root")
	}

	// receives a Result struct when a repo is done/fails syncing
	doneChan := make(chan config.SyncResult)
	// closed when merlin is told to stop running
	stopChan := make(chan struct{})

	// receives a Conn when a client makes a connection to unix socket
	connChan := make(chan net.Conn)
	// closed or receives a signal to stop listening to the unix socket
	stopLisChan := make(chan struct{})

	// gets unblocked when SIGINT or SIGTERM is sent, will begin process of stopping the program
	stopSig := make(chan os.Signal, 1)
	signal.Notify(stopSig, syscall.SIGINT, syscall.SIGTERM)

	// gets unblocked with SIGHUP is sent, will attempt to reload the config
	reloadSig := make(chan os.Signal, 1)
	signal.Notify(reloadSig, syscall.SIGHUP)

	unix.Umask(002)
	repoIdx := 0
	numJobsRunning := 0

	loadConfig := func() {
		config.LoadConfig(*configPath, doneChan, stopChan)
		logger.OutLog("Loaded config:\n" + fmt.Sprintf("%+v\n", config.Conf))

		// reset the round-robin index and recreate the socket listener
		repoIdx = 0
		go arthur.StartListener(connChan, stopLisChan)
	}

	// We use a round-robin strategy. It's not the most efficient, but it's simple
	// (read: easy to understand) and guarantees each repo will eventually get a chance to run.
	runAsManyAsPossible := func() {
		startIdx := repoIdx

		for numJobsRunning < config.Conf.MaxJobs {
			if sync.SyncIfPossible(config.Repos[repoIdx], false) {
				numJobsRunning++
			}
			repoIdx = (repoIdx + 1) % len(config.Repos)
			if repoIdx == startIdx {
				// we've tried to run every repo and have come full circle
				return
			}
		}
	}

	loadConfig()
	// ensure that IsRunning is false otherwise repo will never sync.
	// only on startup can we assume that repos were not previously syncing
	// since reloading the config does not stop repos with a sync in progress
	for _, repo := range config.Repos {
		repo.State.IsRunning = false
	}
	runAsManyAsPossible()

runLoop:
	for {
		select {
		case <-stopSig: // caught a SIGINT or SIGTERM
			// kill all syncing repos and the socket listener
			close(stopChan)
			close(stopLisChan)
			break runLoop

		case <-reloadSig: // caught a SIGHUP
			// temporary stop the socket listener and load the config again
			stopLisChan <- struct{}{}
			loadConfig()

		case done := <-doneChan: // a sync is done and sends its exit status
			// repo could be removed from config while sync in progress
			if repo, check := config.RepoMap[done.Name]; check {
				sync.SyncCompleted(repo, done.Exit)
			}
			numJobsRunning--

		case conn := <-connChan: // a connection was accepted by the socket listener
			command, repoName := arthur.GetCommand(conn)
			switch command {
			case "status":
				arthur.SendStatus(conn)
			case "sync":
				if arthur.ForceSync(conn, repoName) {
					numJobsRunning++
				}
			default:
				arthur.SendAndLog(conn, "Received unrecognized command: "+command)
			}
			// close the received connection so that the message is sent
			conn.Close()

		case <-time.After(1 * time.Minute):
		}
		runAsManyAsPossible()
	}

	// allow some time for jobs to terminate before force exiting the program
	go func() {
		<-time.After(time.Minute)
		logger.ErrLog("One minute has passed, forcefully exiting the program")
		os.Exit(1)
	}()

	// wait on repos to get terminated
	for {
		select {
		case done := <-doneChan:
			if repo, check := config.RepoMap[done.Name]; check {
				sync.SyncCompleted(repo, done.Exit)
			}
			numJobsRunning--

		case <-time.After(1 * time.Second):
		}

		if numJobsRunning <= 0 {
			os.Exit(0)
		}
	}
}
