package cluster

import (
	"errors"
	"fmt"
	"net"
	"net/url"
	"strings"

	kapi "k8s.io/kubernetes/pkg/api"
	kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"

	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
	osclient "github.com/openshift/origin/pkg/client"
	configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
	"github.com/openshift/origin/pkg/diagnostics/types"

	sdnapi "github.com/openshift/origin/pkg/sdn/api"
)

const masterNotRunningAsANode = `Unable to find a node matching the cluster server IP.
This may indicate the master is not also running a node, and is unable
to proxy to pods over the Open vSwitch SDN.
`

// MasterNode is a Diagnostic for checking that the OpenShift master is also running as node.
// This is currently required to have the master on the Open vSwitch SDN and able to communicate
// with other nodes.
type MasterNode struct {
	KubeClient       *kclientset.Clientset
	OsClient         *osclient.Client
	ServerUrl        string
	MasterConfigFile string // may often be empty if not being run on the host
}

const MasterNodeName = "MasterNode"

func (d *MasterNode) Name() string {
	return MasterNodeName
}

func (d *MasterNode) Description() string {
	return "Check if master is also running node (for Open vSwitch)"
}

func (d *MasterNode) CanRun() (bool, error) {
	if d.KubeClient == nil || d.OsClient == nil {
		return false, errors.New("must have kube and os client")
	}
	if d.ServerUrl == "" {
		return false, errors.New("must have a server URL")
	}

	// If there is a master config file available, we'll perform an additional
	// check to see if an OVS network plugin is in use. If no master config,
	// we assume this is the case for now and let the check run anyhow.
	if len(d.MasterConfigFile) > 0 {
		// Parse the master config and check the network plugin name:
		masterCfg, masterErr := configapilatest.ReadAndResolveMasterConfig(d.MasterConfigFile)
		if masterErr != nil {
			return false, types.DiagnosticError{ID: "DClu3008",
				LogMessage: fmt.Sprintf("Master config provided but unable to parse: %s", masterErr), Cause: masterErr}
		}
		if !sdnapi.IsOpenShiftNetworkPlugin(masterCfg.NetworkConfig.NetworkPluginName) {
			return false, errors.New(fmt.Sprintf("Network plugin does not require master to also run node: %s", masterCfg.NetworkConfig.NetworkPluginName))
		}
	}

	can, err := userCan(d.OsClient, authorizationapi.Action{
		Verb:     "list",
		Group:    kapi.GroupName,
		Resource: "nodes",
	})
	if err != nil {
		return false, types.DiagnosticError{ID: "DClu3000", LogMessage: fmt.Sprintf(clientErrorGettingNodes, err), Cause: err}
	} else if !can {
		return false, types.DiagnosticError{ID: "DClu3001", LogMessage: "Client does not have access to see node status", Cause: err}
	}
	return true, nil
}

func (d *MasterNode) Check() types.DiagnosticResult {
	r := types.NewDiagnosticResult(MasterNodeName)

	nodes, err := d.KubeClient.Core().Nodes().List(kapi.ListOptions{})
	if err != nil {
		r.Error("DClu3002", err, fmt.Sprintf(clientErrorGettingNodes, err))
		return r
	}

	// Provide the actual net.LookupHost as the DNS resolver:
	serverIps, err := resolveServerIP(d.ServerUrl, net.LookupHost)
	if err != nil {
		r.Error("DClu3007", err, "Error resolving servers IP")
		return r
	}

	return searchNodesForIP(nodes.Items, serverIps)
}

// Define a resolve callback function type, use to swap in a dummy implementation
// in tests and avoid actual DNS calls.
type dnsResolver func(string) ([]string, error)

// resolveServerIP extracts the hostname portion of the API server URL passed in,
// and attempts dns resolution. It also attempts to catch server URL's that already
// contain both IPv4 and IPv6 addresses.
func resolveServerIP(serverUrl string, fn dnsResolver) ([]string, error) {
	// Extract the hostname from the API server URL:
	u, err := url.Parse(serverUrl)
	if err != nil || u.Host == "" {
		return nil, errors.New(fmt.Sprintf("Unable to parse hostname from URL: %s", serverUrl))
	}

	// Trim the port, if one exists, and watchout for IPv6 URLs.
	if strings.Count(u.Host, ":") > 1 {
		// Check if this is an IPv6 address as is to avoid problems with splitting
		// off the port:
		ipv6 := net.ParseIP(u.Host)
		if ipv6 != nil {
			return []string{ipv6.String()}, nil
		}
	}
	hostname, _, err := net.SplitHostPort(u.Host)
	if err != nil && hostname == "" {
		// Likely didn't have a port, carry on:
		hostname = u.Host
	}

	// Check if the hostname already looks like an IPv4 or IPv6 address:
	goIp := net.ParseIP(hostname)
	if goIp != nil {
		return []string{goIp.String()}, nil
	}

	// If not, attempt a DNS lookup. We may get multiple addresses for the hostname,
	// we'll return them all and search for any match in Kube nodes:
	ips, err := fn(hostname)
	if err != nil {
		return nil, errors.New(fmt.Sprintf("Unable to perform DNS lookup for: %s", hostname))
	}
	return ips, nil
}

func searchNodesForIP(nodes []kapi.Node, ips []string) types.DiagnosticResult {
	r := types.NewDiagnosticResult(MasterNodeName)
	r.Debug("DClu3005", fmt.Sprintf("Seaching for a node with master IP: %s", ips))

	// Loops = # of nodes * number of IPs per node (2 commonly) * # of IPs the
	// server hostname resolves to. (should usually be 1)
	for _, node := range nodes {
		for _, address := range node.Status.Addresses {
			for _, ipAddress := range ips {
				r.Debug("DClu3006", fmt.Sprintf("Checking node %s address %s",
					node.ObjectMeta.Name, address.Address))
				if address.Address == ipAddress {
					r.Info("DClu3003", fmt.Sprintf("Found a node with same IP as master: %s",
						node.ObjectMeta.Name))
					return r
				}
			}
		}
	}
	r.Warn("DClu3004", nil, masterNotRunningAsANode)
	return r
}