package network

import (
	"fmt"
	"strings"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	utilnet "k8s.io/utils/net"

	configv1 "github.com/openshift/api/config/v1"
	configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
	"github.com/openshift/library-go/pkg/operator/configobserver"
	"github.com/openshift/library-go/pkg/operator/configobserver/network"
	"github.com/openshift/library-go/pkg/operator/events"

	"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
)

// ObserveRestrictedCIDRs watches the network configuration and updates the
// RestrictedEndpointsAdmission controller config.
func ObserveRestrictedCIDRs(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (map[string]interface{}, []error) {
	listers := genericListers.(configobservation.Listers)

	var errs []error
	configPath := []string{"admission", "pluginConfig", "network.openshift.io/RestrictedEndpointsAdmission", "configuration"}

	admissionControllerConfig := unstructured.Unstructured{}
	prev, ok, err := unstructured.NestedMap(existingConfig, configPath...)
	if err != nil {
		errs = append(errs, err)
	}
	if ok && err == nil {
		// If we ever bump the API version, we'll have to do conversion here.
		admissionControllerConfig.SetUnstructuredContent(prev)
	}
	admissionControllerConfig.SetAPIVersion("network.openshift.io/v1")
	admissionControllerConfig.SetKind("RestrictedEndpointsAdmissionConfig")

	clusterCIDRs, err := network.GetClusterCIDRs(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
	}

	serviceCIDRs, err := GetServiceCIDRs(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
	}

	// If we weren't able to retrieve cidrs, then return the previous configuration
	if len(errs) > 0 || len(clusterCIDRs) == 0 || len(serviceCIDRs) == 0 {
		previouslyObservedConfig := map[string]interface{}{}
		unstructured.SetNestedMap(previouslyObservedConfig, admissionControllerConfig.Object, configPath...)
		return previouslyObservedConfig, errs
	}

	// set observed values
	//  admission:
	//    pluginConfig:
	//      network.openshift.io/RestrictedEndpointsAdmission:
	//        configuration:
	//          version: network.openshift.io/v1
	//          kind: RestrictedEndpointsAdmissionConfig
	//          restrictedCIDRs:
	//            - 10.3.0.0/16 # ServiceCIDR
	//            - 10.2.0.0/16 # ClusterCIDR
	var restrictedCIDRs []string
	restrictedCIDRs = append(restrictedCIDRs, clusterCIDRs...)
	restrictedCIDRs = append(restrictedCIDRs, serviceCIDRs...)

	if err := unstructured.SetNestedStringSlice(admissionControllerConfig.Object, restrictedCIDRs, "restrictedCIDRs"); err != nil {
		errs = append(errs, err)
	}

	observedConfig := map[string]interface{}{}
	unstructured.SetNestedMap(observedConfig, admissionControllerConfig.Object, configPath...)

	return observedConfig, errs
}

// ObserveServicesSubnet watches the network configuration and generates the
// servicesSubnet (and bindAddress)
func ObserveServicesSubnet(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (map[string]interface{}, []error) {
	listers := genericListers.(configobservation.Listers)

	out := map[string]interface{}{}
	servicesSubnetConfigPath := []string{"servicesSubnet"}
	bindAddressConfigPath := []string{"servingInfo", "bindAddress"}
	bindNetworkConfigPath := []string{"servingInfo", "bindNetwork"}

	previouslyObservedConfig, errs := extractPreviouslyObservedConfig(existingConfig, servicesSubnetConfigPath, bindAddressConfigPath, bindNetworkConfigPath)

	serviceCIDRs, err := GetServiceCIDRs(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
		return previouslyObservedConfig, errs
	}
	servicesSubnet := strings.Join(serviceCIDRs, ",")

	if err := unstructured.SetNestedField(out, servicesSubnet, servicesSubnetConfigPath...); err != nil {
		errs = append(errs, err)
	}
	bindAddress := "0.0.0.0:6443"
	bindNetwork := "tcp4"
	if len(serviceCIDRs) == 1 && utilnet.IsIPv6CIDRString(serviceCIDRs[0]) {
		bindAddress = "[::]:6443"
		bindNetwork = "tcp6"
	}
	if err := unstructured.SetNestedField(out, bindAddress, bindAddressConfigPath...); err != nil {
		errs = append(errs, err)
	}
	if err := unstructured.SetNestedField(out, bindNetwork, bindNetworkConfigPath...); err != nil {
		errs = append(errs, err)
	}

	return out, errs
}

// ObserveExternalIPPolicy observes the network configuration and generates the
// ExternalIPRanger admission controller accordingly.
func ObserveExternalIPPolicy(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (map[string]interface{}, []error) {
	listers := genericListers.(configobservation.Listers)

	// set observed values
	//  admission:
	//    pluginConfig:
	//      network.openshift.io/ExternalIPRanger:
	//        configuration:
	//          version: network.openshift.io/v1
	//          kind: ExternalIPRangerAdmissionConfig
	//          externalIPNetworkCIDRs: [...]
	configPath := []string{"admission", "pluginConfig", "network.openshift.io/ExternalIPRanger", "configuration"}

	// Need to handle 3 cases:
	// 1. retrieval error: pass through existing, if it exists
	// 2. Null externalip policy: enable the externalip admission controller with deny-all
	// 3. Non-null policy: enable the externalip admission controller, pass through configuration.

	previouslyObservedConfig, errs := extractPreviouslyObservedConfig(existingConfig, configPath)

	externalIPPolicy, err := network.GetExternalIPPolicy(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
	}

	// called "ingress ips" in the controller
	autoExternalIPs, err := network.GetExternalIPAutoAssignCIDRs(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
	}

	// Case 1: retrieval error. Pass through our existing configuration path,
	// if it exists.
	if len(errs) > 0 {
		return previouslyObservedConfig, errs
	}

	// Case 2, 3: Policy, synthesize config
	// Simply by creating this configuration, the admission controller will
	// be enabled and block all IPs by default.

	admissionControllerConfig := unstructured.Unstructured{}
	admissionControllerConfig.SetAPIVersion("network.openshift.io/v1")
	admissionControllerConfig.SetKind("ExternalIPRangerAdmissionConfig")

	conf := []string{}

	if externalIPPolicy != nil {
		for _, cidr := range externalIPPolicy.RejectedCIDRs {
			conf = append(conf, "!"+cidr)
		}
		for _, cidr := range externalIPPolicy.AllowedCIDRs {
			conf = append(conf, cidr)
		}
	}

	// Logic carried forward from 3.X. The "autoExternalIPs" is internally
	// called IngressIPs (this is a nasty naming collision). We need to
	// activate a special mode in the controller if this is enabled.
	allowIngressIP := len(autoExternalIPs) > 0

	if len(conf) > 0 {
		unstructured.SetNestedStringSlice(admissionControllerConfig.Object, conf, "externalIPNetworkCIDRs")
	}
	unstructured.SetNestedField(admissionControllerConfig.Object, allowIngressIP, "allowIngressIP")
	observedConfig := map[string]interface{}{}
	unstructured.SetNestedMap(observedConfig, admissionControllerConfig.Object, configPath...)

	return observedConfig, errs
}

// ObserveServiceNodePortRange watches the network configuration and generates the
// serviceNodePortRange
func ObserveServicesNodePortRange(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (map[string]interface{}, []error) {
	listers := genericListers.(configobservation.Listers)

	out := map[string]interface{}{}
	servicesNodePortRangeConfigPath := []string{"apiServerArguments", "service-node-port-range"}

	previouslyObservedConfig, errs := extractPreviouslyObservedConfig(existingConfig, servicesNodePortRangeConfigPath)

	serviceNodePortRange, err := network.GetServiceNodePortRange(listers.NetworkLister, recorder)
	if err != nil {
		errs = append(errs, err)
		return previouslyObservedConfig, errs
	}
	// We want to make sure that a range user sets doesn't shrink. This
	// is handled by a custom resource validator in openshift-api-server code.
	// If the user setting for this field goes from
	//      1) not being set to
	//      2) set to 30000-36000 to
	//      3) not being set
	// The third change should fail customresourcevalidation. So once the field
	// is set in the yaml, it should never come here for anything other than the
	// default case of never being set.

	if serviceNodePortRange == "" {
		return map[string]interface{}{}, errs
	}
	snpr := []string{serviceNodePortRange}
	if err := unstructured.SetNestedStringSlice(out, snpr, servicesNodePortRangeConfigPath...); err != nil {
		errs = append(errs, err)
		return previouslyObservedConfig, errs
	}
	return out, errs
}

// extractPreviouslyObservedConfig extracts the previously observed config from the existing config.
func extractPreviouslyObservedConfig(existing map[string]interface{}, paths ...[]string) (map[string]interface{}, []error) {
	var errs []error
	previous := map[string]interface{}{}
	for _, fields := range paths {
		value, found, err := unstructured.NestedFieldCopy(existing, fields...)
		if !found {
			continue
		}
		if err != nil {
			errs = append(errs, err)
		}
		err = unstructured.SetNestedField(previous, value, fields...)
		if err != nil {
			errs = append(errs, err)
		}
	}
	return previous, errs
}

// GetServiceCIDRs reads the service IP ranges from the global network configuration resource. Emits events if CIDRs are not found.
// Copied from library-go master.
func GetServiceCIDRs(lister configlistersv1.NetworkLister, recorder events.Recorder) ([]string, error) {
	network, err := lister.Get("cluster")
	if errors.IsNotFound(err) {
		recorder.Warningf("GetServiceCIDRFailed", "Required networks.%s/cluster not found", configv1.GroupName)
		return nil, nil
	}
	if err != nil {
		recorder.Warningf("GetServiceCIDRFailed", "error getting networks.%s/cluster: %v", configv1.GroupName, err)
		return nil, err
	}

	if len(network.Status.ServiceNetwork) == 0 || len(network.Status.ServiceNetwork[0]) == 0 {
		recorder.Warningf("GetServiceCIDRFailed", "Required status.serviceNetwork field is not set in networks.%s/cluster", configv1.GroupName)
		return nil, fmt.Errorf("networks.%s/cluster: status.serviceNetwork not found", configv1.GroupName)
	}

	return network.Status.ServiceNetwork, nil
}
