package router

import (
	"errors"
	"fmt"
	"os"

	"github.com/golang/glog"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"

	ocmd "github.com/openshift/origin/pkg/cmd/cli/cmd"
	"github.com/openshift/origin/pkg/cmd/templates"
	"github.com/openshift/origin/pkg/cmd/util"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	routeapi "github.com/openshift/origin/pkg/route/api"
	"github.com/openshift/origin/pkg/router/controller"
	f5plugin "github.com/openshift/origin/pkg/router/f5"
)

var (
	f5Long = templates.LongDesc(`
		Start an F5 route synchronizer

		This command launches a process that will synchronize an F5 to the route configuration of your master.

		You may restrict the set of routes exposed to a single project (with --namespace), projects your client has
		access to with a set of labels (--project-labels), namespaces matching a label (--namespace-labels), or all
		namespaces (no argument). You can limit the routes to those matching a --labels or --fields selector. Note
		that you must have a cluster-wide administrative role to view all namespaces.`)
)

// F5RouterOptions represent the complete structure needed to start an F5 router
// sync process.
type F5RouterOptions struct {
	Config *clientcmd.Config

	F5Router
	RouterSelection
}

// F5Router is the config necessary to start an F5 router plugin.
type F5Router struct {
	RouterName string

	// Host specifies the hostname or IP address of the F5 BIG-IP host.
	Host string

	// Username specifies the username with which the plugin should authenticate
	// with the F5 BIG-IP host.
	Username string

	// Password specifies the password with which the plugin should authenticate
	// with the F5 BIG-IP host.
	Password string

	// HttpVserver specifies the name of the vserver object in F5 BIG-IP that the
	// plugin will configure for HTTP connections.
	HttpVserver string

	// HttpsVserver specifies the name of the vserver object in F5 BIG-IP that the
	// plugin will configure for HTTPS connections.
	HttpsVserver string

	// PrivateKey specifies the filename of an SSH private key for
	// authenticating with F5.  This key is required to copy certificates
	// to the F5 BIG-IP host.
	PrivateKey string

	// Insecure specifies whether the F5 plugin should perform strict certificate
	// validation for connections to the F5 BIG-IP host.
	Insecure bool

	// PartitionPath specifies the path to the F5 partition. This is
	// normally used to create access control boundaries for users
	// and applications.
	PartitionPath string

	// VxlanGateway is the ip address assigned to the local tunnel interface
	// inside F5 box. This address is the one that the packets generated from F5
	// will carry. The pods will return the packets to this address itself.
	// It is important that the gateway be one of the ip addresses of the subnet
	// that has been generated for F5.
	VxlanGateway string

	// InternalAddress is the ip address of the vtep interface used to connect to
	// VxLAN overlay. It is the hostIP address listed in the subnet generated for F5
	InternalAddress string
}

// Bind binds F5Router arguments to flags
func (o *F5Router) Bind(flag *pflag.FlagSet) {
	flag.StringVar(&o.RouterName, "name", util.Env("ROUTER_SERVICE_NAME", "public"), "The name the router will identify itself with in the route status")
	flag.StringVar(&o.Host, "f5-host", util.Env("ROUTER_EXTERNAL_HOST_HOSTNAME", ""), "The host of F5 BIG-IP's management interface")
	flag.StringVar(&o.Username, "f5-username", util.Env("ROUTER_EXTERNAL_HOST_USERNAME", ""), "The username for F5 BIG-IP's management utility")
	flag.StringVar(&o.Password, "f5-password", util.Env("ROUTER_EXTERNAL_HOST_PASSWORD", ""), "The password for F5 BIG-IP's management utility")
	flag.StringVar(&o.HttpVserver, "f5-http-vserver", util.Env("ROUTER_EXTERNAL_HOST_HTTP_VSERVER", "ose-vserver"), "The F5 BIG-IP virtual server for HTTP connections")
	flag.StringVar(&o.HttpsVserver, "f5-https-vserver", util.Env("ROUTER_EXTERNAL_HOST_HTTPS_VSERVER", "https-ose-vserver"), "The F5 BIG-IP virtual server for HTTPS connections")
	flag.StringVar(&o.PrivateKey, "f5-private-key", util.Env("ROUTER_EXTERNAL_HOST_PRIVKEY", ""), "The path to the F5 BIG-IP SSH private key file")
	flag.BoolVar(&o.Insecure, "f5-insecure", util.Env("ROUTER_EXTERNAL_HOST_INSECURE", "") == "true", "Skip strict certificate verification")
	flag.StringVar(&o.PartitionPath, "f5-partition-path", util.Env("ROUTER_EXTERNAL_HOST_PARTITION_PATH", f5plugin.F5DefaultPartitionPath), "The F5 BIG-IP partition path to use")
	flag.StringVar(&o.InternalAddress, "f5-internal-address", util.Env("ROUTER_EXTERNAL_HOST_INTERNAL_ADDRESS", ""), "The F5 BIG-IP internal interface's IP address")
	flag.StringVar(&o.VxlanGateway, "f5-vxlan-gateway-cidr", util.Env("ROUTER_EXTERNAL_HOST_VXLAN_GW_CIDR", ""), "The F5 BIG-IP gateway-ip-address/cidr-mask for setting up the VxLAN")
}

// Validate verifies the required F5 flags are present
func (o *F5Router) Validate() error {
	if o.Host == "" {
		return errors.New("F5 host must be specified")
	}

	if o.Username == "" {
		return errors.New("F5 username must be specified")
	}

	if o.Password == "" {
		return errors.New("F5 password must be specified")
	}

	if len(o.HttpVserver) == 0 && len(o.HttpsVserver) == 0 {
		return errors.New("F5 HTTP and HTTPS vservers cannot both be blank")
	}

	valid := (len(o.VxlanGateway) == 0 && len(o.InternalAddress) == 0) || (len(o.VxlanGateway) != 0 && len(o.InternalAddress) != 0)
	if !valid {
		return errors.New("For VxLAN setup, both internal-address and gateway-cidr must be specified")
	}

	return nil
}

// NewCommandF5Router provides CLI handler for the F5 router sync plugin.
func NewCommandF5Router(name string) *cobra.Command {
	options := &F5RouterOptions{
		Config: clientcmd.NewConfig(),
	}
	options.Config.FromFile = true

	cmd := &cobra.Command{
		Use:   fmt.Sprintf("%s%s", name, clientcmd.ConfigSyntax),
		Short: "Start an F5 route synchronizer",
		Long:  f5Long,
		Run: func(c *cobra.Command, args []string) {
			options.RouterSelection.Namespace = cmdutil.GetFlagString(c, "namespace")
			cmdutil.CheckErr(options.Complete())
			cmdutil.CheckErr(options.Validate())
			cmdutil.CheckErr(options.Run())
		},
	}

	cmd.AddCommand(ocmd.NewCmdVersion(name, nil, os.Stdout, ocmd.VersionOptions{}))

	flag := cmd.Flags()
	options.Config.Bind(flag)
	options.F5Router.Bind(flag)
	options.RouterSelection.Bind(flag)

	return cmd
}

func (o *F5RouterOptions) Complete() error {
	if len(o.PartitionPath) == 0 {
		o.PartitionPath = f5plugin.F5DefaultPartitionPath
		glog.Warningf("Partition path was empty, using default: %q",
			f5plugin.F5DefaultPartitionPath)
	}

	return o.RouterSelection.Complete()
}

func (o *F5RouterOptions) Validate() error {
	return o.F5Router.Validate()
}

// F5RouteAdmitterFunc returns a func that checks if a route is a
// wildcard route and currently denies it.
func (o *F5RouterOptions) F5RouteAdmitterFunc() controller.RouteAdmissionFunc {
	return func(route *routeapi.Route) error {
		if err := o.AdmissionCheck(route); err != nil {
			return err
		}

		switch route.Spec.WildcardPolicy {
		case routeapi.WildcardPolicyNone:
			return nil

		case routeapi.WildcardPolicySubdomain:
			// TODO: F5 wildcard route support.
			return fmt.Errorf("Wildcard routes are currently not supported by the F5 router")
		}

		return fmt.Errorf("unknown wildcard policy %v", route.Spec.WildcardPolicy)
	}
}

// Run launches an F5 route sync process using the provided options. It never exits.
func (o *F5RouterOptions) Run() error {
	cfg := f5plugin.F5PluginConfig{
		Host:            o.Host,
		Username:        o.Username,
		Password:        o.Password,
		HttpVserver:     o.HttpVserver,
		HttpsVserver:    o.HttpsVserver,
		PrivateKey:      o.PrivateKey,
		Insecure:        o.Insecure,
		PartitionPath:   o.PartitionPath,
		InternalAddress: o.InternalAddress,
		VxlanGateway:    o.VxlanGateway,
	}
	f5Plugin, err := f5plugin.NewF5Plugin(cfg)
	if err != nil {
		return err
	}

	oc, _, kc, err := o.Config.Clients()
	if err != nil {
		return err
	}

	statusPlugin := controller.NewStatusAdmitter(f5Plugin, oc, o.RouterName)
	uniqueHostPlugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
	plugin := controller.NewHostAdmitter(uniqueHostPlugin, o.F5RouteAdmitterFunc(), false, statusPlugin)

	factory := o.RouterSelection.NewFactory(oc, kc)
	watchNodes := (len(o.InternalAddress) != 0 && len(o.VxlanGateway) != 0)
	controller := factory.Create(plugin, watchNodes)
	controller.Run()

	select {}
}