package main

import (
	"fmt"
	"os"
	"time"

	"golang.org/x/crypto/ssh/terminal"

	"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
	"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
	"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
	"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
	"github.com/cloudflare/cloudflared/log"
	"github.com/cloudflare/cloudflared/metrics"

	"github.com/getsentry/raven-go"
	"github.com/mitchellh/go-homedir"
	"gopkg.in/urfave/cli.v2"
	"gopkg.in/urfave/cli.v2/altsrc"

	"github.com/pkg/errors"
)

const (
	developerPortal = "https://developers.cloudflare.com/argo-tunnel"
	licenseUrl      = developerPortal + "/licence/"
)

var (
	Version   = "DEV"
	BuildTime = "unknown"
	logger    = log.CreateLogger()
)

func main() {
	metrics.RegisterBuildInfo(BuildTime, Version)
	raven.SetRelease(Version)

	// Force shutdown channel used by the app. When closed, app must terminate.
	// Windows service manager closes this channel when it receives shutdown command.
	shutdownC := make(chan struct{})
	// Graceful shutdown channel used by the app. When closed, app must terminate.
	// Windows service manager closes this channel when it receives stop command.
	graceShutdownC := make(chan struct{})

	app := &cli.App{}
	app.Name = "cloudflared"
	app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
   Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
	app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
	app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network. 
	You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
	and configure access control.`
	app.Flags = flags()
	app.Action = action(Version, shutdownC, graceShutdownC)
	app.Before = before(app.Flags)
	app.Commands = commands()

	tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
	runApp(app, shutdownC, graceShutdownC)
}

func commands() []*cli.Command {
	cmds := []*cli.Command{
		{
			Name:      "update",
			Action:    updater.Update,
			Usage:     "Update the agent if a new version exists",
			ArgsUsage: " ",
			Description: `Looks for a new version on the official download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.

To determine if an update happened in a script, check for error code 64.`,
		},
	}
	cmds = append(cmds, tunnel.Commands()...)
	cmds = append(cmds, access.Commands()...)
	return cmds
}

func flags() []cli.Flag {
	flags := tunnel.Flags()
	return append(flags, access.Flags()...)
}

func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
	return func(c *cli.Context) (err error) {
		if isRunningFromTerminal() {
			logger.Error("Use of cloudflared without commands is deprecated.")
			cli.ShowAppHelp(c)
			return nil
		}
		tags := make(map[string]string)
		tags["hostname"] = c.String("hostname")
		raven.SetTagsContext(tags)
		raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
		if err != nil {
			raven.CaptureError(err, nil)
		}
		return err
	}
}

func before(flags []cli.Flag) cli.BeforeFunc {
	return func(context *cli.Context) error {
		inputSource, err := config.FindInputSourceContext(context)
		if err != nil {
			logger.WithError(err).Infof("Cannot load configuration from %s", context.String("config"))
			return err
		} else if inputSource != nil {
			err := altsrc.ApplyInputSourceValues(context, inputSource, flags)
			if err != nil {
				logger.WithError(err).Infof("Cannot apply configuration from %s", context.String("config"))
				return err
			}
			logger.Infof("Applied configuration from %s", context.String("config"))
		}
		return nil
	}
}

func userHomeDir() (string, error) {
	// This returns the home dir of the executing user using OS-specific method
	// for discovering the home dir. It's not recommended to call this function
	// when the user has root permission as $HOME depends on what options the user
	// use with sudo.
	homeDir, err := homedir.Dir()
	if err != nil {
		logger.WithError(err).Error("Cannot determine home directory for the user")
		return "", errors.Wrap(err, "Cannot determine home directory for the user")
	}
	return homeDir, nil
}

func isRunningFromTerminal() bool {
	return terminal.IsTerminal(int(os.Stdout.Fd()))
}
