package admin

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"os"
	"path"
	"strconv"

	"github.com/spf13/cobra"

	"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/apimachinery/registered"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/master/ports"
	"k8s.io/kubernetes/pkg/runtime"
	"k8s.io/kubernetes/pkg/util/crypto"

	"github.com/openshift/origin/pkg/cmd/flagtypes"
	configapi "github.com/openshift/origin/pkg/cmd/server/api"
	latestconfigapi "github.com/openshift/origin/pkg/cmd/server/api/latest"
	cmdutil "github.com/openshift/origin/pkg/cmd/util"
	"github.com/openshift/origin/pkg/cmd/util/variable"
)

const NodeConfigCommandName = "create-node-config"

type CreateNodeConfigOptions struct {
	SignerCertOptions *SignerCertOptions

	NodeConfigDir string

	NodeName            string
	Hostnames           []string
	VolumeDir           string
	ImageTemplate       variable.ImageTemplate
	AllowDisabledDocker bool
	DNSDomain           string
	DNSIP               string
	ListenAddr          flagtypes.Addr

	ClientCertFile    string
	ClientKeyFile     string
	ServerCertFile    string
	ServerKeyFile     string
	NodeClientCAFile  string
	APIServerCAFiles  []string
	APIServerURL      string
	Output            io.Writer
	NetworkPluginName string
}

func NewCommandNodeConfig(commandName string, fullName string, out io.Writer) *cobra.Command {
	options := NewDefaultCreateNodeConfigOptions()
	options.Output = out

	cmd := &cobra.Command{
		Use:   commandName,
		Short: "Create a configuration bundle for a node",
		Run: func(cmd *cobra.Command, args []string) {
			if err := options.Validate(args); err != nil {
				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error()))
			}

			if err := options.CreateNodeFolder(); err != nil {
				kcmdutil.CheckErr(err)
			}
		},
	}

	flags := cmd.Flags()

	BindSignerCertOptions(options.SignerCertOptions, flags, "")

	flags.StringVar(&options.NodeConfigDir, "node-dir", "", "The client data directory.")

	flags.StringVar(&options.NodeName, "node", "", "The name of the node as it appears in etcd.")
	flags.StringSliceVar(&options.Hostnames, "hostnames", options.Hostnames, "Every hostname or IP you want server certs to be valid for. Comma delimited list")
	flags.StringVar(&options.VolumeDir, "volume-dir", options.VolumeDir, "The volume storage directory.  This path is not relativized.")
	flags.StringVar(&options.ImageTemplate.Format, "images", options.ImageTemplate.Format, "When fetching the network container image, use this format. The latest release will be used by default.")
	flags.BoolVar(&options.ImageTemplate.Latest, "latest-images", options.ImageTemplate.Latest, "If true, attempt to use the latest images for the cluster instead of the latest release.")
	flags.BoolVar(&options.AllowDisabledDocker, "allow-disabled-docker", options.AllowDisabledDocker, "Allow the node to start without docker being available.")
	flags.StringVar(&options.DNSDomain, "dns-domain", options.DNSDomain, "DNS domain for the cluster.")
	flags.StringVar(&options.DNSIP, "dns-ip", options.DNSIP, "DNS server IP for the cluster.")
	flags.Var(&options.ListenAddr, "listen", "The address to listen for connections on (scheme://host:port).")

	flags.StringVar(&options.ClientCertFile, "client-certificate", "", "The client cert file for the node to contact the API.")
	flags.StringVar(&options.ClientKeyFile, "client-key", "", "The client key file for the node to contact the API.")
	flags.StringVar(&options.ServerCertFile, "server-certificate", "", "The server cert file for the node to serve secure traffic.")
	flags.StringVar(&options.ServerKeyFile, "server-key", "", "The server key file for the node to serve secure traffic.")
	flags.StringVar(&options.NodeClientCAFile, "node-client-certificate-authority", options.NodeClientCAFile, "The file containing signing authorities to use to verify requests to the node. If empty, all requests will be allowed.")
	flags.StringVar(&options.APIServerURL, "master", options.APIServerURL, "The API server's URL.")
	flags.StringSliceVar(&options.APIServerCAFiles, "certificate-authority", options.APIServerCAFiles, "Files containing signing authorities to use to verify the API server's serving certificate.")
	flags.StringVar(&options.NetworkPluginName, "network-plugin", options.NetworkPluginName, "Name of the network plugin to hook to for pod networking. Optional for OpenShift network plugin, node will auto detect network plugin configured by OpenShift master.")

	// autocompletion hints
	cmd.MarkFlagFilename("node-dir")
	cmd.MarkFlagFilename("volume-dir")
	cmd.MarkFlagFilename("client-certificate")
	cmd.MarkFlagFilename("client-key")
	cmd.MarkFlagFilename("server-certificate")
	cmd.MarkFlagFilename("server-key")
	cmd.MarkFlagFilename("node-client-certificate-authority")
	cmd.MarkFlagFilename("certificate-authority")

	return cmd
}

func NewDefaultCreateNodeConfigOptions() *CreateNodeConfigOptions {
	options := &CreateNodeConfigOptions{SignerCertOptions: NewDefaultSignerCertOptions()}
	options.VolumeDir = "openshift.local.volumes"
	// TODO: replace me with a proper round trip of config options through decode
	options.DNSDomain = "cluster.local"
	options.APIServerURL = "https://localhost:8443"
	options.APIServerCAFiles = []string{"openshift.local.config/master/ca.crt"}
	options.NodeClientCAFile = "openshift.local.config/master/ca.crt"

	options.ImageTemplate = variable.NewDefaultImageTemplate()

	options.ListenAddr = flagtypes.Addr{Value: "0.0.0.0:10250", DefaultScheme: "https", DefaultPort: 10250, AllowPrefix: true}.Default()
	options.NetworkPluginName = ""

	return options
}

func (o CreateNodeConfigOptions) IsCreateClientCertificate() bool {
	return len(o.ClientCertFile) == 0 && len(o.ClientKeyFile) == 0
}

func (o CreateNodeConfigOptions) IsCreateServerCertificate() bool {
	return len(o.ServerCertFile) == 0 && len(o.ServerKeyFile) == 0 && o.UseTLS()
}

func (o CreateNodeConfigOptions) UseTLS() bool {
	return o.ListenAddr.URL.Scheme == "https"
}

func (o CreateNodeConfigOptions) UseNodeClientCA() bool {
	return o.UseTLS() && len(o.NodeClientCAFile) > 0
}

func (o CreateNodeConfigOptions) Validate(args []string) error {
	if len(args) != 0 {
		return errors.New("no arguments are supported")
	}
	if len(o.NodeConfigDir) == 0 {
		return errors.New("--node-dir must be provided")
	}
	if len(o.NodeName) == 0 {
		return errors.New("--node must be provided")
	}
	if len(o.APIServerURL) == 0 {
		return errors.New("--master must be provided")
	}
	if len(o.APIServerCAFiles) == 0 {
		return fmt.Errorf("--certificate-authority must be a valid certificate file")
	} else {
		for _, caFile := range o.APIServerCAFiles {
			if _, err := crypto.CertPoolFromFile(caFile); err != nil {
				return fmt.Errorf("--certificate-authority must be a valid certificate file: %v", err)
			}
		}
	}
	if len(o.Hostnames) == 0 {
		return errors.New("at least one hostname must be provided")
	}

	if len(o.ClientCertFile) != 0 {
		if len(o.ClientKeyFile) == 0 {
			return errors.New("--client-key must be provided if --client-certificate is provided")
		}
	} else if len(o.ClientKeyFile) != 0 {
		return errors.New("--client-certificate must be provided if --client-key is provided")
	}

	if len(o.ServerCertFile) != 0 {
		if len(o.ServerKeyFile) == 0 {
			return errors.New("--server-key must be provided if --server-certificate is provided")
		}
	} else if len(o.ServerKeyFile) != 0 {
		return errors.New("--server-certificate must be provided if --server-key is provided")
	}

	if o.IsCreateClientCertificate() || o.IsCreateServerCertificate() {
		if len(o.SignerCertOptions.KeyFile) == 0 {
			return errors.New("--signer-key must be provided to create certificates")
		}
		if len(o.SignerCertOptions.CertFile) == 0 {
			return errors.New("--signer-cert must be provided to create certificates")
		}
		if len(o.SignerCertOptions.SerialFile) == 0 {
			return errors.New("--signer-serial must be provided to create certificates")
		}
	}

	return nil
}

// readFiles returns a byte array containing the contents of all the given filenames,
// optionally separated by a delimiter, or an error if any of the files cannot be read
func readFiles(srcFiles []string, separator []byte) ([]byte, error) {
	data := []byte{}
	for _, srcFile := range srcFiles {
		fileData, err := ioutil.ReadFile(srcFile)
		if err != nil {
			return nil, err
		}
		if len(data) > 0 && len(separator) > 0 {
			data = append(data, separator...)
		}
		data = append(data, fileData...)
	}
	return data, nil
}

func CopyFile(src, dest string, permissions os.FileMode) error {
	// copy the cert and key over
	if content, err := ioutil.ReadFile(src); err != nil {
		return err
	} else if err := ioutil.WriteFile(dest, content, permissions); err != nil {
		return err
	}

	return nil
}

func (o CreateNodeConfigOptions) CreateNodeFolder() error {
	servingCertInfo := DefaultNodeServingCertInfo(o.NodeConfigDir)
	clientCertInfo := DefaultNodeClientCertInfo(o.NodeConfigDir)

	clientCertFile := clientCertInfo.CertFile
	clientKeyFile := clientCertInfo.KeyFile
	apiServerCAFile := DefaultCAFilename(o.NodeConfigDir, CAFilePrefix)

	serverCertFile := servingCertInfo.CertFile
	serverKeyFile := servingCertInfo.KeyFile
	nodeClientCAFile := DefaultCAFilename(o.NodeConfigDir, "node-client-ca")

	kubeConfigFile := DefaultNodeKubeConfigFile(o.NodeConfigDir)
	nodeConfigFile := path.Join(o.NodeConfigDir, "node-config.yaml")
	nodeJSONFile := path.Join(o.NodeConfigDir, "node-registration.json")

	fmt.Fprintf(o.Output, "Generating node credentials ...\n")

	if err := o.MakeClientCert(clientCertFile, clientKeyFile); err != nil {
		return err
	}
	if o.UseTLS() {
		if err := o.MakeAndWriteServerCert(serverCertFile, serverKeyFile); err != nil {
			return err
		}
		if o.UseNodeClientCA() {
			if err := o.MakeNodeClientCA(nodeClientCAFile); err != nil {
				return err
			}
		}
	}
	if err := o.MakeAPIServerCA(apiServerCAFile); err != nil {
		return err
	}
	if err := o.MakeKubeConfig(clientCertFile, clientKeyFile, apiServerCAFile, kubeConfigFile); err != nil {
		return err
	}
	if err := o.MakeNodeConfig(serverCertFile, serverKeyFile, nodeClientCAFile, kubeConfigFile, nodeConfigFile); err != nil {
		return err
	}
	if err := o.MakeNodeJSON(nodeJSONFile); err != nil {
		return err
	}

	fmt.Fprintf(o.Output, "Created node config for %s in %s\n", o.NodeName, o.NodeConfigDir)

	return nil
}

func (o CreateNodeConfigOptions) MakeClientCert(clientCertFile, clientKeyFile string) error {
	if o.IsCreateClientCertificate() {
		createNodeClientCert := CreateClientCertOptions{
			SignerCertOptions: o.SignerCertOptions,

			CertFile: clientCertFile,
			KeyFile:  clientKeyFile,

			User:   "system:node:" + o.NodeName,
			Groups: []string{bootstrappolicy.NodesGroup},
			Output: o.Output,
		}

		if err := createNodeClientCert.Validate(nil); err != nil {
			return err
		}
		if _, err := createNodeClientCert.CreateClientCert(); err != nil {
			return err
		}

	} else {
		if err := CopyFile(o.ClientCertFile, clientCertFile, 0644); err != nil {
			return err
		}
		if err := CopyFile(o.ClientKeyFile, clientKeyFile, 0600); err != nil {
			return err
		}
	}

	return nil
}

func (o CreateNodeConfigOptions) MakeAndWriteServerCert(serverCertFile, serverKeyFile string) error {
	if o.IsCreateServerCertificate() {
		nodeServerCertOptions := CreateServerCertOptions{
			SignerCertOptions: o.SignerCertOptions,

			CertFile: serverCertFile,
			KeyFile:  serverKeyFile,

			Hostnames: o.Hostnames,
			Output:    o.Output,
		}

		if err := nodeServerCertOptions.Validate(nil); err != nil {
			return err
		}
		if _, err := nodeServerCertOptions.CreateServerCert(); err != nil {
			return err
		}

	} else {
		if err := CopyFile(o.ServerCertFile, serverCertFile, 0644); err != nil {
			return err
		}
		if err := CopyFile(o.ServerKeyFile, serverKeyFile, 0600); err != nil {
			return err
		}
	}

	return nil
}

func (o CreateNodeConfigOptions) MakeAPIServerCA(clientCopyOfCAFile string) error {
	content, err := readFiles(o.APIServerCAFiles, []byte("\n"))
	if err != nil {
		return err
	}
	return ioutil.WriteFile(clientCopyOfCAFile, content, 0644)
}

func (o CreateNodeConfigOptions) MakeNodeClientCA(clientCopyOfCAFile string) error {
	if err := CopyFile(o.NodeClientCAFile, clientCopyOfCAFile, 0644); err != nil {
		return err
	}

	return nil
}

func (o CreateNodeConfigOptions) MakeKubeConfig(clientCertFile, clientKeyFile, clientCopyOfCAFile, kubeConfigFile string) error {
	createKubeConfigOptions := CreateKubeConfigOptions{
		APIServerURL:     o.APIServerURL,
		APIServerCAFiles: []string{clientCopyOfCAFile},

		CertFile: clientCertFile,
		KeyFile:  clientKeyFile,

		ContextNamespace: kapi.NamespaceDefault,

		KubeConfigFile: kubeConfigFile,
		Output:         o.Output,
	}
	if err := createKubeConfigOptions.Validate(nil); err != nil {
		return err
	}
	if _, err := createKubeConfigOptions.CreateKubeConfig(); err != nil {
		return err
	}

	return nil
}

func (o CreateNodeConfigOptions) MakeNodeConfig(serverCertFile, serverKeyFile, nodeClientCAFile, kubeConfigFile, nodeConfigFile string) error {
	config := &configapi.NodeConfig{
		NodeName: o.NodeName,

		ServingInfo: configapi.ServingInfo{
			BindAddress: net.JoinHostPort(o.ListenAddr.Host, strconv.Itoa(ports.KubeletPort)),
		},

		VolumeDirectory:     o.VolumeDir,
		AllowDisabledDocker: o.AllowDisabledDocker,

		ImageConfig: configapi.ImageConfig{
			Format: o.ImageTemplate.Format,
			Latest: o.ImageTemplate.Latest,
		},

		DNSDomain: o.DNSDomain,
		DNSIP:     o.DNSIP,

		MasterKubeConfig: kubeConfigFile,

		NetworkConfig: configapi.NodeNetworkConfig{
			NetworkPluginName: o.NetworkPluginName,
		},
	}

	if o.UseTLS() {
		config.ServingInfo.ServerCert = configapi.CertInfo{
			CertFile: serverCertFile,
			KeyFile:  serverKeyFile,
		}
		config.ServingInfo.ClientCA = nodeClientCAFile
	}

	// Resolve relative to CWD
	cwd, err := os.Getwd()
	if err != nil {
		return err
	}
	if err := configapi.ResolveNodeConfigPaths(config, cwd); err != nil {
		return err
	}

	// Relativize to config file dir
	base, err := cmdutil.MakeAbs(o.NodeConfigDir, cwd)
	if err != nil {
		return err
	}
	if err := configapi.RelativizeNodeConfigPaths(config, base); err != nil {
		return err
	}

	// Roundtrip the config to v1 and back to ensure proper defaults are set.
	ext, err := configapi.Scheme.ConvertToVersion(config, latestconfigapi.Version)
	if err != nil {
		return err
	}
	internal, err := configapi.Scheme.ConvertToVersion(ext, configapi.SchemeGroupVersion)
	if err != nil {
		return err
	}

	content, err := latestconfigapi.WriteYAML(internal)
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(nodeConfigFile, content, 0644); err != nil {
		return err
	}

	return nil
}

func (o CreateNodeConfigOptions) MakeNodeJSON(nodeJSONFile string) error {
	node := &kapi.Node{}
	node.Name = o.NodeName

	groupMeta := registered.GroupOrDie(kapi.GroupName)

	json, err := runtime.Encode(kapi.Codecs.LegacyCodec(groupMeta.GroupVersions[0]), node)
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(nodeJSONFile, json, 0644); err != nil {
		return err
	}

	return nil
}