package main

import (
	"bufio"
	"fmt"
	stdlog "log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strconv"
	"syscall"
	"time"

	"github.com/gobuffalo/packr"

	"github.com/hmage/golibs/log"
	"golang.org/x/crypto/ssh/terminal"
)

// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"

const (
	// Used in config to indicate that syslog or eventlog (win) should be used for logger output
	configSyslog = "syslog"
)

// main is the entry point
func main() {
	// config can be specified, which reads options from there, but other command line flags have to override config values
	// therefore, we must do it manually instead of using a lib
	args := loadOptions()

	if args.serviceControlAction != "" {
		handleServiceControlAction(args.serviceControlAction)
		return
	}

	signalChannel := make(chan os.Signal)
	signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
	go func() {
		<-signalChannel
		cleanup()
		os.Exit(0)
	}()

	// run the protection
	run(args)
}

// run initializes configuration and runs the AdGuard Home
// run is a blocking method and it won't exit until the service is stopped!
func run(args options) {
	// config file path can be overridden by command-line arguments:
	if args.configFilename != "" {
		config.ourConfigFilename = args.configFilename
	}

	// configure working dir and config path
	initWorkingDir(args)

	// configure log level and output
	configureLogger(args)

	// print the first message after logger is configured
	log.Printf("AdGuard Home, version %s\n", VersionString)
	log.Printf("Current working directory is %s", config.ourBinaryDir)
	if args.runningAsService {
		log.Printf("AdGuard Home is running as a service")
	}

	err := askUsernamePasswordIfPossible()
	if err != nil {
		log.Fatal(err)
	}

	// Do the upgrade if necessary
	err = upgradeConfig()
	if err != nil {
		log.Fatal(err)
	}

	// parse from config file
	err = parseConfig()
	if err != nil {
		log.Fatal(err)
	}

	// override bind host/port from the console
	if args.bindHost != "" {
		config.BindHost = args.bindHost
	}
	if args.bindPort != 0 {
		config.BindPort = args.bindPort
	}

	// Load filters from the disk
	// And if any filter has zero ID, assign a new one
	for i := range config.Filters {
		filter := &config.Filters[i] // otherwise we're operating on a copy
		if filter.ID == 0 {
			filter.ID = assignUniqueFilterID()
		}
		err = filter.load()
		if err != nil {
			// This is okay for the first start, the filter will be loaded later
			log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
			// clear LastUpdated so it gets fetched right away
		}
		if len(filter.Rules) == 0 {
			filter.LastUpdated = time.Time{}
		}
	}

	// Save the updated config
	err = config.write()
	if err != nil {
		log.Fatal(err)
	}

	err = startDNSServer()
	if err != nil {
		log.Fatal(err)
	}

	err = startDHCPServer()
	if err != nil {
		log.Fatal(err)
	}

	// Update filters we've just loaded right away, don't wait for periodic update timer
	go func() {
		refreshFiltersIfNecessary(false)
		// Save the updated config
		err := config.write()
		if err != nil {
			log.Fatal(err)
		}
	}()
	// Schedule automatic filters updates
	go periodicallyRefreshFilters()

	// Initialize and run the admin Web interface
	box := packr.NewBox("build/static")
	http.Handle("/", optionalAuthHandler(http.FileServer(box)))
	registerControlHandlers()

	address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
	URL := fmt.Sprintf("http://%s", address)
	log.Println("Go to " + URL)
	log.Fatal(http.ListenAndServe(address, nil))
}

// initWorkingDir initializes the ourBinaryDir (basically, we use it as a working dir)
func initWorkingDir(args options) {
	exec, err := os.Executable()
	if err != nil {
		panic(err)
	}

	if args.configFilename != "" {
		// If there is a custom config file, use it's directory as our working dir
		config.ourBinaryDir = filepath.Dir(args.configFilename)
	} else {
		config.ourBinaryDir = filepath.Dir(exec)
	}
}

// configureLogger configures logger level and output
func configureLogger(args options) {
	ls := getLogSettings()

	// command-line arguments can override config settings
	if args.verbose {
		ls.Verbose = true
	}
	if args.logFile != "" {
		ls.LogFile = args.logFile
	}

	log.Verbose = ls.Verbose

	if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
		// When running as a Windows service, use eventlog by default if nothing else is configured
		// Otherwise, we'll simply loose the log output
		ls.LogFile = configSyslog
	}

	if ls.LogFile == "" {
		return
	}

	if ls.LogFile == configSyslog {
		// Use syslog where it is possible and eventlog on Windows
		err := configureSyslog()
		if err != nil {
			log.Fatalf("cannot initialize syslog: %s", err)
		}
	} else {
		logFilePath := filepath.Join(config.ourBinaryDir, ls.LogFile)
		file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
		if err != nil {
			log.Fatalf("cannot create a log file: %s", err)
		}
		stdlog.SetOutput(file)
	}
}

func cleanup() {
	log.Printf("Stopping AdGuard Home")

	err := stopDNSServer()
	if err != nil {
		log.Printf("Couldn't stop DNS server: %s", err)
	}
	err = stopDHCPServer()
	if err != nil {
		log.Printf("Couldn't stop DHCP server: %s", err)
	}
}

func getInput() (string, error) {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	text := scanner.Text()
	err := scanner.Err()
	return text, err
}

// command-line arguments
type options struct {
	verbose        bool   // is verbose logging enabled
	configFilename string // path to the config file
	bindHost       string // host address to bind HTTP server on
	bindPort       int    // port to serve HTTP pages on
	logFile        string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog

	// service control action (see service.ControlAction array + "status" command)
	serviceControlAction string

	// runningAsService flag is set to true when options are passed from the service runner
	runningAsService bool
}

// loadOptions reads command line arguments and initializes configuration
func loadOptions() options {
	o := options{}

	var printHelp func()
	var opts = []struct {
		longName          string
		shortName         string
		description       string
		callbackWithValue func(value string)
		callbackNoValue   func()
	}{
		{"config", "c", "path to config file", func(value string) { o.configFilename = value }, nil},
		{"host", "h", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
		{"port", "p", "port to serve HTTP pages on", func(value string) {
			v, err := strconv.Atoi(value)
			if err != nil {
				panic("Got port that is not a number")
			}
			o.bindPort = v
		}, nil},
		{"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) {
			o.serviceControlAction = value
		}, nil},
		{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
			o.logFile = value
		}, nil},
		{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
		{"help", "", "print this help", nil, func() {
			printHelp()
			os.Exit(64)
		}},
	}
	printHelp = func() {
		fmt.Printf("Usage:\n\n")
		fmt.Printf("%s [options]\n\n", os.Args[0])
		fmt.Printf("Options:\n")
		for _, opt := range opts {
			if opt.shortName != "" {
				fmt.Printf("  -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
			} else {
				fmt.Printf("  %-34s %s\n", "--"+opt.longName, opt.description)
			}
		}
	}
	for i := 1; i < len(os.Args); i++ {
		v := os.Args[i]
		knownParam := false
		for _, opt := range opts {
			if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
				if opt.callbackWithValue != nil {
					if i+1 >= len(os.Args) {
						log.Printf("ERROR: Got %s without argument\n", v)
						os.Exit(64)
					}
					i++
					opt.callbackWithValue(os.Args[i])
				} else if opt.callbackNoValue != nil {
					opt.callbackNoValue()
				}
				knownParam = true
				break
			}
		}
		if !knownParam {
			log.Printf("ERROR: unknown option %v\n", v)
			printHelp()
			os.Exit(64)
		}
	}

	return o
}

func promptAndGet(prompt string) (string, error) {
	for {
		fmt.Print(prompt)
		input, err := getInput()
		if err != nil {
			log.Printf("Failed to get input, aborting: %s", err)
			return "", err
		}
		if len(input) != 0 {
			return input, nil
		}
		// try again
	}
}

func promptAndGetPassword(prompt string) (string, error) {
	for {
		fmt.Print(prompt)
		password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
		fmt.Print("\n")
		if err != nil {
			log.Printf("Failed to get input, aborting: %s", err)
			return "", err
		}
		if len(password) != 0 {
			return string(password), nil
		}
		// try again
	}
}

func askUsernamePasswordIfPossible() error {
	configFile := config.getConfigFilename()
	_, err := os.Stat(configFile)
	if !os.IsNotExist(err) {
		// do nothing, file exists
		return nil
	}
	if !terminal.IsTerminal(int(os.Stdin.Fd())) {
		return nil // do nothing
	}
	if !terminal.IsTerminal(int(os.Stdout.Fd())) {
		return nil // do nothing
	}
	fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
	yesno, err := promptAndGet("Please type 'yes' or 'no': ")
	if err != nil {
		return err
	}
	if yesno[0] != 'y' && yesno[0] != 'Y' {
		return nil
	}
	username, err := promptAndGet("Please enter the username: ")
	if err != nil {
		return err
	}

	password, err := promptAndGetPassword("Please enter the password: ")
	if err != nil {
		return err
	}

	password2, err := promptAndGetPassword("Please enter password again: ")
	if err != nil {
		return err
	}
	if password2 != password {
		fmt.Printf("Passwords do not match! Aborting\n")
		os.Exit(1)
	}

	config.AuthName = username
	config.AuthPass = password
	return nil
}