// Watch a torrent directory, poll rtorrent, and download completed torrents over SFTP.
package main

import (
	"errors"
	"flag"
	"github.com/adampresley/sigint"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// Load information from a given config file config_path
func loadConfig(configPath string) (map[string]string, error) {
	file, err := os.Open(configPath)
	if err != nil {
		log.Println("Failed to open configuration file " + configPath)
		return nil, err
	}

	data, err := ioutil.ReadAll(file)
	if err != nil {
		log.Println("Failed to read configuration file " + configPath)
		return nil, err
	}

	config := make(map[string]string)

	lines := strings.Split(string(data), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		// Ignore comments
		if len(line) <= 2 || line[:2] == "//" {
			continue
		}

		// Ignore malformed lines
		sepPosition := strings.Index(line, ": \"")
		if sepPosition == -1 {
			continue
		}

		config[line[:sepPosition]] = line[sepPosition+3 : len(line)-1]
	}

	return config, nil
}

// Checker routine to see if torrents are completed
func checker(config map[string]string, checkerChan <-chan map[string]string, com chan<- error) error {
	for {
		torrentInfo := <-checkerChan

		log.Println("Started checking " + torrentInfo["torrent_path"])

		torrent, err := NewTorrent(config["xml_user"], config["xml_pass"], config["xml_address"], torrentInfo["torrent_path"])
		if err != nil {
			if !os.IsNotExist(err) {
				log.Println("Failed to initialize torrent for " + torrentInfo["torrent_path"] + ": " + err.Error())
			}

			continue
		}

		syncer, err := NewSync(config["threads"], config["ssh_user"], config["ssh_pass"], config["ssh_server"], config["ssh_port"])
		defer syncer.Close()
		if err != nil {
			log.Println("Failed to create a new sync: " + err.Error())
			com <- err
			return err
		}

		completed, err := torrent.GetTorrentComplete()
		if err != nil {
			log.Println("Failed to see if " + torrent.path + " is completed: " + err.Error())
			com <- err
			return err
		}

		name, err := torrent.GetTorrentName()
		if err != nil {
			com <- err
			return err
		}

		if completed {
			log.Println(name + " is completed, starting download now")

			remoteDownloadPath := filepath.Join(config["remote_download_dir"], name)
			exists, err := syncer.Exists(remoteDownloadPath)
			if err != nil {
				log.Println("Failed to see if " + remoteDownloadPath + " exists: " + err.Error())
				com <- err
				return err
			}

			// file/dir to downlaod does not exist!
			if !exists {
				err = errors.New(remoteDownloadPath + " does not exist on remote server")
				com <- err
				return err
			}

			completedDestination := filepath.Join(torrentInfo["local_download_dir"], name)

			_, err = os.Stat(completedDestination)
			if err == nil {
				err = errors.New(completedDestination + " already exists, not downloading")
				continue
			} else if !os.IsNotExist(err) {
				log.Println("Failed to stat: " + completedDestination + ": " + err.Error())
				com <- err
				return err
			}

			err = syncer.GetPath(remoteDownloadPath, config["temp_download_dir"])
			if err != nil {
				log.Println("Failed to download " + remoteDownloadPath + ": " + err.Error())
				com <- err
				return err
			}

			log.Println("Successfully downloaded " + name)

			tempDestination := filepath.Join(config["temp_download_dir"], name)

			err = os.Rename(tempDestination, completedDestination)
			if err != nil {
				log.Println("Failed to move " + tempDestination + " to " + completedDestination + ": " + err.Error())
				com <- err
				return err
			}

			err = os.Remove(torrent.path)
			if err != nil && !os.IsNotExist(err) {
				log.Println("Failed to remove " + torrent.path + ": " + err.Error())
				com <- err
				return err
			}
		} else {
			log.Println(name + " is not completed, waiting for it to finish")
		}

		syncer.Close()
	}

	com <- nil
	return nil
}

// Scanner routine to see if there are new torrent_files
func scanner(config map[string]string, checkerChan chan<- map[string]string, com chan<- error) error {
	watchDirs := map[string]string{config["local_torrent_dir"]: config["local_download_dir"]}
	dirContents, err := ioutil.ReadDir(config["local_torrent_dir"])

	if err != nil {
		com <- err
		return err
	}

	for _, file := range dirContents {
		if file.IsDir() {
			watchDirs[filepath.Join(config["local_torrent_dir"], file.Name())] = filepath.Join(config["local_download_dir"], file.Name())
		}
	}

	uploaded := make(map[string]bool)
	downloadingTorrentPath := ""
	for {
		for watchDir, downloadDir := range watchDirs {
			torrentFiles, err := ioutil.ReadDir(watchDir)
			if err != nil {
				com <- err
				return err
			}

			for _, torrentFile := range torrentFiles {
				if torrentFile.IsDir() {
					// skip because we don't do more than one level of watching
					continue
				}

				torrentPath := filepath.Join(watchDir, torrentFile.Name())

				if !uploaded[torrentPath] {
					syncer, err := NewSync("1", config["ssh_user"], config["ssh_pass"], config["ssh_server"], config["ssh_port"])
					if err != nil {
						log.Println("Failed to create a new sync: " + err.Error())
						syncer.Close()
						continue
					}

					destinationTorrent := filepath.Join(config["remote_torrent_dir"], filepath.Base(torrentPath))
					exists, err := syncer.Exists(destinationTorrent)
					if err != nil {
						log.Println("Failed to see if " + torrentPath + " already exists on the server: " + err.Error())
						syncer.Close()
						continue
					}

					if exists {
						uploaded[torrentPath] = true
					} else {
						err = syncer.SendFiles(map[string]string{torrentPath: destinationTorrent})
						if err == nil {
							log.Println("Successfully uploaded " + torrentPath + " to " + destinationTorrent)
							uploaded[torrentPath] = true
						} else {
							log.Println("Failed to upload " + torrentPath + " to " + destinationTorrent + ": " + err.Error())
						}

						syncer.Close()
						continue
					}

					syncer.Close()
				}

				downloadInfo := map[string]string{
					"torrent_path":       torrentPath,
					"local_download_dir": downloadDir,
				}

				// try to send the info to the checker goroutine (nonblocking)
				select {
				case checkerChan <- downloadInfo:
					// don't keep track of completed downloads in the uploaded map
					if downloadingTorrentPath != "" {
						delete(uploaded, downloadingTorrentPath)
					}

					downloadingTorrentPath = torrentPath
					break
				default:
					break
				}
			}
		}

		time.Sleep(time.Second * 30)
	}

	com <- nil
	return nil
}

func die(exitCode int) {
	log.Println("Quiting")
	os.Exit(exitCode)
}

func main() {
	sigint.ListenForSIGINT(func() {
		die(1)
	})

	var configPath string
	flag.StringVar(&configPath, "config", "", "Location of the config file")
	flag.Parse()

	if configPath == "" {
		log.Println("Missing argument for configuration file path")
		flag.PrintDefaults()
		die(1)
	}

	log.Println("Reading configuration file")
	config, err := loadConfig(configPath)
	if err != nil {
		log.Println(err)
		die(1)
	}

	log.Println("Successfully read configuration file")

	checkerChan := make(chan map[string]string, 50)

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

	log.Println("Starting the scanner routine")
	scannerCom := make(chan error)
	go scanner(config, checkerChan, scannerCom)

	log.Println("Starting the checker routine")
	checkerCom := make(chan error)
	go checker(config, checkerChan, checkerCom)

	restartOnError := true
	if config["restart_on_error"] != "" {
		restartOnError = config["restart_on_error"] == "true"
	}

	for {
		select {
		case err := <-scannerCom:
			if err != nil {
				log.Println("Scanner failed: " + err.Error())

				if restartOnError {
					log.Println("Restarting scanner")
					go scanner(config, checkerChan, scannerCom)
				} else {
					log.Println("Quiting due to scanner error")
					die(1)
				}
			}
		case err := <-checkerCom:
			if err != nil {
				log.Println("Checker failed: " + err.Error())

				if restartOnError {
					log.Println("Restarting checker")
					go checker(config, checkerChan, checkerCom)
				} else {
					log.Println("Quiting due to checker error")
					die(1)
				}
			}
		default:
			break
		}

		time.Sleep(time.Second * 5)
	}
}