package start

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	_ "net/http/pprof"
	"path"
	"path/filepath"
	"strings"

	"github.com/coreos/go-systemd/daemon"
	"github.com/golang/glog"
	"github.com/spf13/cobra"

	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
	"github.com/openshift/origin/pkg/cmd/server/kubernetes"

	configapi "github.com/openshift/origin/pkg/cmd/server/api"
	configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
	"github.com/openshift/origin/pkg/cmd/server/certs"
	cmdutil "github.com/openshift/origin/pkg/cmd/util"
	"github.com/openshift/origin/pkg/cmd/util/docker"
)

type NodeOptions struct {
	NodeArgs *NodeArgs

	WriteConfigOnly bool
	ConfigFile      string
}

const longNodeCommandDesc = `
Start an OpenShift node
This command helps you launch an OpenShift node.  Running

    $ openshift start node --master=<masterIP>

will start an OpenShift node that attempts to connect to the master on the provided IP. The 
node will run in the foreground until you terminate the process.
`

// NewCommandStartMaster provides a CLI handler for 'start' command
func NewCommandStartNode() (*cobra.Command, *NodeOptions) {
	options := &NodeOptions{}

	cmd := &cobra.Command{
		Use:   "node",
		Short: "Launch OpenShift node",
		Long:  longNodeCommandDesc,
		Run: func(c *cobra.Command, args []string) {
			if err := options.Complete(); err != nil {
				fmt.Println(err.Error())
				c.Help()
				return
			}
			if err := options.Validate(args); err != nil {
				fmt.Println(err.Error())
				c.Help()
				return
			}

			if err := options.StartNode(); err != nil {
				glog.Fatal(err)
			}
		},
	}

	flags := cmd.Flags()

	flags.BoolVar(&options.WriteConfigOnly, "write-config", false, "Indicates that the command should build the configuration from command-line arguments, write it to the location specified by --config, and exit.")
	flags.StringVar(&options.ConfigFile, "config", "", "Location of the node configuration file to run from, or write to (when used with --write-config). When running from a configuration file, all other command-line arguments are ignored.")

	options.NodeArgs = NewDefaultNodeArgs()
	// make sure that KubeConnectionArgs and NodeArgs use the same CertArgs for this command
	options.NodeArgs.KubeConnectionArgs.CertArgs = options.NodeArgs.CertArgs

	BindNodeArgs(options.NodeArgs, flags, "")
	BindBindAddrArg(options.NodeArgs.BindAddrArg, flags, "")
	BindImageFormatArgs(options.NodeArgs.ImageFormatArgs, flags, "")
	BindKubeConnectionArgs(options.NodeArgs.KubeConnectionArgs, flags, "")
	BindCertArgs(options.NodeArgs.CertArgs, flags, "")

	return cmd, options
}

func (o NodeOptions) Validate(args []string) error {
	if len(args) != 0 {
		return errors.New("no arguments are supported for start node")
	}
	if o.WriteConfigOnly {
		if len(o.ConfigFile) == 0 {
			return errors.New("--config is required if --write-config is true")
		}
	}

	return nil
}

func (o NodeOptions) Complete() error {
	o.NodeArgs.NodeName = strings.ToLower(o.NodeArgs.NodeName)

	return nil
}

// StartNode calls RunNode and then waits forever
func (o NodeOptions) StartNode() error {
	if err := o.RunNode(); err != nil {
		return err
	}

	if o.WriteConfigOnly {
		return nil
	}

	daemon.SdNotify("READY=1")
	select {}

	return nil
}

// RunNode takes the options and:
// 1.  Creates certs if needed
// 2.  Reads fully specified node config OR builds a fully specified node config from the args
// 3.  Writes the fully specified node config and exits if needed
// 4.  Starts the node based on the fully specified config
func (o NodeOptions) RunNode() error {
	startUsingConfigFile := !o.WriteConfigOnly && (len(o.ConfigFile) > 0)
	mintCerts := o.NodeArgs.CertArgs.CreateCerts && !startUsingConfigFile

	if mintCerts {
		if err := o.CreateCerts(); err != nil {
			return nil
		}
	}

	var nodeConfig *configapi.NodeConfig
	var err error
	if startUsingConfigFile {
		nodeConfig, err = ReadNodeConfig(o.ConfigFile)
	} else {
		nodeConfig, err = o.NodeArgs.BuildSerializeableNodeConfig()
	}
	if err != nil {
		return err
	}

	if o.WriteConfigOnly {
		content, err := WriteNode(nodeConfig)
		if err != nil {
			return err
		}
		if err := ioutil.WriteFile(o.ConfigFile, content, 0644); err != nil {
			return err
		}
		return nil
	}

	_, kubeClientConfig, err := configapi.GetKubeClient(nodeConfig.MasterKubeConfig)
	if err != nil {
		return err
	}
	glog.Infof("Starting an OpenShift node, connecting to %s", kubeClientConfig.Host)

	if cmdutil.Env("OPENSHIFT_PROFILE", "") == "web" {
		go func() {
			glog.Infof("Starting profiling endpoint at http://127.0.0.1:6060/debug/pprof/")
			glog.Fatal(http.ListenAndServe("127.0.0.1:6060", nil))
		}()
	}

	if err := StartNode(*nodeConfig); err != nil {
		return err
	}

	return nil
}

func (o NodeOptions) CreateCerts() error {
	username := "node-" + o.NodeArgs.NodeName
	signerOptions := &certs.CreateSignerCertOptions{
		CertFile:   certs.DefaultCertFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
		KeyFile:    certs.DefaultKeyFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
		SerialFile: certs.DefaultSerialFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
		Name:       certs.DefaultSignerName(),
	}
	if _, err := signerOptions.CreateSignerCert(); err != nil {
		return err
	}
	getSignerOptions := &certs.GetSignerCertOptions{
		CertFile:   certs.DefaultCertFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
		KeyFile:    certs.DefaultKeyFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
		SerialFile: certs.DefaultSerialFilename(o.NodeArgs.CertArgs.CertDir, "ca"),
	}

	mintNodeClientCert := certs.CreateNodeClientCertOptions{
		GetSignerCertOptions: getSignerOptions,
		CertFile:             certs.DefaultCertFilename(o.NodeArgs.CertArgs.CertDir, username),
		KeyFile:              certs.DefaultKeyFilename(o.NodeArgs.CertArgs.CertDir, username),
		NodeName:             o.NodeArgs.NodeName,
	}
	if _, err := mintNodeClientCert.CreateNodeClientCert(); err != nil {
		return err
	}

	masterAddr, err := o.NodeArgs.KubeConnectionArgs.GetKubernetesAddress(&o.NodeArgs.DefaultKubernetesURL)
	if err != nil {
		return err
	}

	createKubeConfigOptions := certs.CreateKubeConfigOptions{
		APIServerURL:    masterAddr.String(),
		APIServerCAFile: getSignerOptions.CertFile,
		ServerNick:      "master",

		CertFile: mintNodeClientCert.CertFile,
		KeyFile:  mintNodeClientCert.KeyFile,
		UserNick: username,

		KubeConfigFile: path.Join(filepath.Dir(mintNodeClientCert.CertFile), ".kubeconfig"),
	}
	if _, err := createKubeConfigOptions.CreateKubeConfig(); err != nil {
		return err
	}

	return nil
}

func ReadNodeConfig(filename string) (*configapi.NodeConfig, error) {
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	config := &configapi.NodeConfig{}

	if err := configapilatest.Codec.DecodeInto(data, config); err != nil {
		return nil, err
	}
	return config, nil
}

func StartNode(config configapi.NodeConfig) error {
	if config.RecordEvents {
		kubeClient, _, err := configapi.GetKubeClient(config.MasterKubeConfig)
		if err != nil {
			return err
		}

		// TODO: recording should occur in individual components
		record.StartRecording(kubeClient.Events(""), kapi.EventSource{Component: "node"})
	}

	nodeConfig, err := kubernetes.BuildKubernetesNodeConfig(config)
	if err != nil {
		return err
	}

	nodeConfig.EnsureVolumeDir()
	nodeConfig.EnsureDocker(docker.NewHelper())
	nodeConfig.RunProxy()
	nodeConfig.RunKubelet()

	return nil
}