package networkpolicy

import (
	"github.com/tidbcloud/shared-provider/pkg/types"
	netv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
)

const (
	DefaultDenyName       = "default-deny"
	AllowDNSAccessName    = "allow-dns-access"
	AllowAllInNamespace   = "allow-all-in-namespace"
	AllowMonitorToThanos  = "allow-monitor-to-thanos"
	AllowAccessAWSService = "allow-to-aws-service"
	AllowFromManager      = "allow-from-manager"
	AllowToAPIServer      = "allow-to-api-server"
	AllowFromDBaaSProxy   = "allow-from-dbaas-proxy"
	AllowFromSliReporter  = "allow-from-sli-reporter"
	AllowFromLoadBalancer = "allow-from-load-balancer"
)

var AllPolicyNames = []string{
	DefaultDenyName,
	AllowDNSAccessName,
	AllowAllInNamespace,
	AllowMonitorToThanos,
	AllowAccessAWSService,
	AllowFromManager,
	AllowToAPIServer,
	AllowFromDBaaSProxy,
	AllowFromSliReporter,
	AllowFromLoadBalancer,
}

const (
	// Policy depend on kubernete should contains this label,
	// We will fetch all policy contains this label and re-sync.
	DependOnKubernetesLabelKey   = "depend-on-kubernetes"
	DependOnKubernetesLabelValue = "true"
)

func NewDefaultDeny(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      DefaultDenyName,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeIngress,
				netv1.PolicyTypeEgress,
			},
			PodSelector: metav1.LabelSelector{},
		},
	}
}

func NewAllowDNSAccess(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowDNSAccessName,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeEgress,
			},
			PodSelector: metav1.LabelSelector{},
			Egress: []netv1.NetworkPolicyEgressRule{
				{
					To: []netv1.NetworkPolicyPeer{
						{
							NamespaceSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"kubernetes.io/metadata.name": "kube-system",
								},
							},
						},
					},
					Ports: []netv1.NetworkPolicyPort{
						{
							Protocol: &tcpProtocol,
							Port:     &dnsPort,
						},
						{
							Protocol: &udpProtocol,
							Port:     &dnsPort,
						},
					},
				},
			},
		},
	}
}

func NewAllowAllInNamespce(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowAllInNamespace,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeEgress,
				netv1.PolicyTypeIngress,
			},
			PodSelector: metav1.LabelSelector{},
			Egress: []netv1.NetworkPolicyEgressRule{
				{
					To: []netv1.NetworkPolicyPeer{
						{
							PodSelector: &metav1.LabelSelector{},
						},
					},
				},
			},
			Ingress: []netv1.NetworkPolicyIngressRule{
				{
					From: []netv1.NetworkPolicyPeer{
						{
							PodSelector: &metav1.LabelSelector{},
						},
					},
				},
			},
		},
	}
}

func NewAllowMonitorToThanos(ns string, exceptCIDR []string) *netv1.NetworkPolicy {
	httpPort := intstr.FromInt(80)

	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowMonitorToThanos,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeEgress,
			},
			PodSelector: metav1.LabelSelector{
				MatchLabels: map[string]string{
					types.LabelKeyComponent: types.LabelValComponentMonitor,
				},
			},
			Egress: []netv1.NetworkPolicyEgressRule{
				{
					To: []netv1.NetworkPolicyPeer{
						{
							IPBlock: &netv1.IPBlock{
								CIDR:   "0.0.0.0/0",
								Except: exceptCIDR,
							},
						},
					},
					Ports: []netv1.NetworkPolicyPort{
						{
							Port: &httpPort,
						},
					},
				},
			},
		},
	}
}

// NewAllowAccessAWSService allow some component to access aws service.
// Currently allow port 443 and 80 to outside cluster.
func NewAllowAccessAWSService(ns string, exceptCIDR []string) *netv1.NetworkPolicy {
	httpPort := intstr.FromInt(80)
	httpsPort := intstr.FromInt(443)
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowAccessAWSService,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeEgress,
			},
			PodSelector: metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{
					{
						Key:      types.LabelKeyComponent,
						Operator: metav1.LabelSelectorOpIn,
						Values: []string{
							types.LabelValComponentTiKV,
							types.LabelValComponentTiFlash,
							types.LabelValComponentBackup,
							types.LabelValComponentRestore,
							types.LabelValComponentClean,
							types.LabelValComponentTiDBLightning,
						},
					},
				},
			},
			Egress: []netv1.NetworkPolicyEgressRule{
				{
					To: []netv1.NetworkPolicyPeer{
						{
							IPBlock: &netv1.IPBlock{
								CIDR:   "0.0.0.0/0",
								Except: exceptCIDR,
							},
						},
					},
					Ports: []netv1.NetworkPolicyPort{
						{
							Port: &httpPort,
						},
						{
							Port: &httpsPort,
						},
					},
				},
			},
		},
	}
}

// NewAllowFromManager allow manager to access need services monitor/tidb-lightning.
// Mananger will query prometheus.
// Mananger will query tidb-lightning.
func NewAllowFromManager(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowFromManager,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeIngress,
			},
			PodSelector: metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{
					{
						Key:      types.LabelKeyComponent,
						Operator: metav1.LabelSelectorOpIn,
						Values: []string{
							types.LabelValComponentMonitor,
							types.LabelValComponentTiDBLightning,
						},
					},
				},
			},
			Ingress: []netv1.NetworkPolicyIngressRule{
				{
					From: []netv1.NetworkPolicyPeer{
						{
							// allow all namespace. we deploy manager under tidb-admin, but there are e2e test will
							// deploy at other namespace too.
							NamespaceSelector: &metav1.LabelSelector{},
							PodSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"control-plane": "manager",
								},
							},
						},
					},
				},
			},
		},
	}
}

// nolint: dupl // nolint
func NewAllowFromDBaaSProxy(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowFromDBaaSProxy,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeIngress,
			},
			// monitor & shell pod create by proxy should be enough? but allow all now.
			PodSelector: metav1.LabelSelector{},
			Ingress: []netv1.NetworkPolicyIngressRule{
				{
					From: []netv1.NetworkPolicyPeer{
						{
							NamespaceSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"kubernetes.io/metadata.name": "tidb-admin",
								},
							},
							PodSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"app": "dbaas-proxy",
								},
							},
						},
					},
				},
			},
		},
	}
}

func NewAllowToAPIServer(ns string, apiServerCIDR []string) *netv1.NetworkPolicy {
	httpsPort := intstr.FromInt(443)

	var blocks []netv1.NetworkPolicyPeer
	for _, cidr := range apiServerCIDR {
		blocks = append(blocks, netv1.NetworkPolicyPeer{
			IPBlock: &netv1.IPBlock{
				CIDR: cidr,
			},
		})
	}
	if len(blocks) == 0 {
		blocks = append(blocks, netv1.NetworkPolicyPeer{
			IPBlock: &netv1.IPBlock{
				CIDR: "0.0.0.0/0",
			},
		})
	}

	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowToAPIServer,
			Namespace: ns,
			Labels: map[string]string{
				DependOnKubernetesLabelKey: DependOnKubernetesLabelValue,
			},
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeEgress,
			},
			PodSelector: metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{
					{
						Key:      types.LabelKeyComponent,
						Operator: metav1.LabelSelectorOpIn,
						Values: []string{
							types.LabelValComponentControllerManager,
							types.LabelValComponentDiscovery,
							types.LabelValComponentBackup,
							types.LabelValComponentRestore,
							types.LabelValComponentClean,
							types.LabelValComponentMonitor,
						},
					},
				},
			},
			Egress: []netv1.NetworkPolicyEgressRule{
				{
					To: blocks,
					Ports: []netv1.NetworkPolicyPort{
						{
							Protocol: &tcpProtocol,
							Port:     &httpsPort,
						},
					},
				},
			},
		},
	}
}

// nolint: dupl // nolint
func NewAllowFromSliReporter(ns string) *netv1.NetworkPolicy {
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowFromSliReporter,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeIngress,
			},
			PodSelector: metav1.LabelSelector{},
			Ingress: []netv1.NetworkPolicyIngressRule{
				{
					From: []netv1.NetworkPolicyPeer{
						{
							NamespaceSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"kubernetes.io/metadata.name": "tidb-admin",
								},
							},
							PodSelector: &metav1.LabelSelector{
								MatchLabels: map[string]string{
									"app": "sli-reporter",
								},
							},
						},
					},
				},
			},
		},
	}
}

func NewAllowFromLoadBalancer(ns string, cidrs ...string) *netv1.NetworkPolicy {
	peers := []netv1.NetworkPolicyPeer{}
	for _, cidr := range cidrs {
		peers = append(peers, netv1.NetworkPolicyPeer{
			IPBlock: &netv1.IPBlock{CIDR: cidr},
		})
	}
	return &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AllowFromLoadBalancer,
			Namespace: ns,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{
				netv1.PolicyTypeIngress,
			},
			PodSelector: metav1.LabelSelector{
				MatchLabels: map[string]string{
					types.LabelKeyComponent: types.LabelValComponentTiDB,
				},
			},
			Ingress: []netv1.NetworkPolicyIngressRule{
				{
					From: peers,
					Ports: []netv1.NetworkPolicyPort{
						{
							Port:     &tidbPort,
							Protocol: &tcpProtocol,
						},
					},
				},
			},
		},
	}
}
