// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package v2

import (
	"strings"

	"github.com/pkg/errors"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/apache/apisix-ingress-controller/api/adc"
)

// ApisixRouteSpec is the spec definition for ApisixRoute.
// It defines routing rules for both HTTP and stream traffic.
type ApisixRouteSpec struct {
	// IngressClassName is the name of the IngressClass this route belongs to.
	// It allows multiple controllers to watch and reconcile different routes.
	IngressClassName string `json:"ingressClassName,omitempty" yaml:"ingressClassName,omitempty"`
	// HTTP defines a list of HTTP route rules.
	// Each rule specifies conditions to match HTTP requests and how to forward them.
	//
	// +listType=map
	// +listMapKey=name
	HTTP []ApisixRouteHTTP `json:"http,omitempty" yaml:"http,omitempty"`
	// Stream defines a list of stream route rules.
	// Each rule specifies conditions to match TCP/UDP traffic and how to forward them.
	//
	// +listType=map
	// +listMapKey=name
	Stream []ApisixRouteStream `json:"stream,omitempty" yaml:"stream,omitempty"`
}

// ApisixRouteStatus defines the observed state of ApisixRoute.
type ApisixRouteStatus = ApisixStatus

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=ar
// +kubebuilder:printcolumn:name="Hosts",type="string",JSONPath=".spec.http[].match.hosts",description="HTTP Hosts",priority=0
// +kubebuilder:printcolumn:name="URIs",type="string",JSONPath=".spec.http[].match.paths",description="HTTP Paths",priority=0
// +kubebuilder:printcolumn:name="Target Service (HTTP)",type="string",JSONPath=".spec.http[].backends[].serviceName",description="Backend Service for HTTP",priority=1
// +kubebuilder:printcolumn:name="Ingress Port (TCP)",type="integer",JSONPath=".spec.tcp[].match.ingressPort",description="TCP Ingress Port",priority=1
// +kubebuilder:printcolumn:name="Target Service (TCP)",type="string",JSONPath=".spec.tcp[].match.backend.serviceName",description="Backend Service for TCP",priority=1
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Creation time",priority=0

// ApisixRoute defines configuration for HTTP and stream routes.
type ApisixRoute struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	// ApisixRouteSpec defines HTTP and stream route configuration.
	Spec   ApisixRouteSpec   `json:"spec,omitempty"`
	Status ApisixRouteStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// ApisixRouteList contains a list of ApisixRoute.
type ApisixRouteList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []ApisixRoute `json:"items"`
}

// ApisixRouteHTTP represents a single HTTP route configuration.
type ApisixRouteHTTP struct {
	// Name is the unique rule name and cannot be empty.
	Name string `json:"name" yaml:"name"`
	// Priority defines the route priority when multiple routes share the same URI path.
	// Higher values mean higher priority in route matching.
	Priority int `json:"priority,omitempty" yaml:"priority,omitempty"`
	// Timeout specifies upstream timeout settings.
	Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"`
	// Match defines the HTTP request matching criteria.
	Match ApisixRouteHTTPMatch `json:"match,omitempty" yaml:"match,omitempty"`
	// Backends lists potential backend services to proxy requests to.
	// If more than one backend is specified, the `traffic-split` plugin is used
	// to distribute traffic according to backend weights.
	Backends []ApisixRouteHTTPBackend `json:"backends,omitempty" yaml:"backends,omitempty"`
	// Upstreams references ApisixUpstream CRDs.
	Upstreams []ApisixRouteUpstreamReference `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`

	// Websocket enables or disables websocket support for this route.
	// +kubebuilder:validation:Optional
	Websocket *bool `json:"websocket" yaml:"websocket"`
	// PluginConfigName specifies the name of the plugin config to apply.
	PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
	// PluginConfigNamespace specifies the namespace of the plugin config.
	// Defaults to the namespace of the ApisixRoute if not set.
	PluginConfigNamespace string `json:"plugin_config_namespace,omitempty" yaml:"plugin_config_namespace,omitempty"`
	// Plugins lists additional plugins applied to this route.
	Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
	// Authentication holds authentication-related configuration for this route.
	Authentication ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
}

// ApisixRouteStream defines the configuration for a Layer 4 (TCP/UDP) route.
type ApisixRouteStream struct {
	// Name is a unique identifier for the route. This field must not be empty.
	Name string `json:"name" yaml:"name"`
	// Protocol specifies the L4 protocol to match. Can be `TCP` or `UDP`.
	//
	// +kubebuilder:validation:Enum=TCP;UDP
	Protocol string `json:"protocol" yaml:"protocol"`
	// Match defines the criteria used to match incoming TCP or UDP connections.
	Match ApisixRouteStreamMatch `json:"match" yaml:"match"`
	// Backend specifies the destination service to which traffic should be forwarded.
	Backend ApisixRouteStreamBackend `json:"backend" yaml:"backend"`
	// Plugins defines a list of plugins to apply to this route.
	Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
}

// UpstreamTimeout defines timeout settings for connecting, sending, and reading from the upstream.
type UpstreamTimeout struct {
	// Connect timeout for establishing a connection to the upstream.
	Connect metav1.Duration `json:"connect,omitempty" yaml:"connect,omitempty"`
	// Send timeout for sending data to the upstream.
	Send metav1.Duration `json:"send,omitempty" yaml:"send,omitempty"`
	// Read timeout for reading data from the upstream.
	Read metav1.Duration `json:"read,omitempty" yaml:"read,omitempty"`
}

// ApisixRouteHTTPMatch defines the conditions used to match incoming HTTP requests.
type ApisixRouteHTTPMatch struct {
	// Paths is a list of URI path patterns to match.
	// At least one path must be specified.
	// Supports exact matches and prefix matches.
	// For prefix matches, append `*` to the path, such as `/foo*`.
	Paths []string `json:"paths" yaml:"paths"`

	// Methods specifies the HTTP methods to match.
	Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"`

	// Hosts specifies Host header values to match.
	// Supports exact and wildcard domains.
	// Only one level of wildcard is allowed (e.g., `*.example.com` is valid,
	// but `*.*.example.com` is not).
	Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"`

	// RemoteAddrs is a list of source IP addresses or CIDR ranges to match.
	// Supports both IPv4 and IPv6 formats.
	RemoteAddrs []string `json:"remoteAddrs,omitempty" yaml:"remoteAddrs,omitempty"`

	// NginxVars defines match conditions based on Nginx variables.
	NginxVars ApisixRouteHTTPMatchExprs `json:"exprs,omitempty" yaml:"exprs,omitempty"`

	// FilterFunc is a user-defined function for advanced request filtering.
	// The function can use Nginx variables through the `vars` parameter.
	FilterFunc string `json:"filter_func,omitempty" yaml:"filter_func,omitempty"`
}

// ApisixRoutePlugin represents an APISIX plugin.
type ApisixRoutePlugin struct {
	// The plugin name.
	Name string `json:"name" yaml:"name"`
	// Whether this plugin is in use, default is true.
	// +kubebuilder:default=true
	Enable bool `json:"enable" yaml:"enable"`
	// Plugin configuration.
	// +kubebuilder:validation:Optional
	Config apiextensionsv1.JSON `json:"config" yaml:"config"`
	// Plugin configuration secretRef.
	// +kubebuilder:validation:Optional
	SecretRef string `json:"secretRef" yaml:"secretRef"`
}

// ApisixRouteHTTPBackend represents an HTTP backend (Kubernetes Service).
type ApisixRouteHTTPBackend struct {
	// ServiceName is the name of the Kubernetes Service.
	// Cross-namespace references are not supported—ensure the ApisixRoute
	// and the Service are in the same namespace.
	ServiceName string `json:"serviceName" yaml:"serviceName"`
	// ServicePort is the port of the Kubernetes Service.
	// This can be either the port name or port number.
	ServicePort intstr.IntOrString `json:"servicePort" yaml:"servicePort"`
	// ResolveGranularity determines how the backend service is resolved.
	// Valid values are `endpoints` and `service`. When set to `endpoints`,
	// individual pod IPs will be used; otherwise, the Service's ClusterIP or ExternalIP is used.
	// The default is `endpoints`.
	ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"`
	// Weight specifies the relative traffic weight for this backend.
	// +kubebuilder:validation:Optional
	Weight *int `json:"weight" yaml:"weight"`
	// Subset specifies a named subset of the target Service.
	// The subset must be pre-defined in the corresponding ApisixUpstream resource.
	Subset string `json:"subset,omitempty" yaml:"subset,omitempty"`
}

// ApisixRouteUpstreamReference references an ApisixUpstream CRD to be used as a backend.
// It can be used in traffic-splitting scenarios or to select a specific upstream configuration.
type ApisixRouteUpstreamReference struct {
	// Name is the name of the ApisixUpstream resource.
	Name string `json:"name,omitempty" yaml:"name"`
	// Weight is the weight assigned to this upstream.
	// +kubebuilder:validation:Optional
	Weight *int `json:"weight,omitempty" yaml:"weight"`
}

// ApisixRouteAuthentication represents authentication-related configuration in ApisixRoute.
type ApisixRouteAuthentication struct {
	// Enable toggles authentication on or off.
	Enable bool `json:"enable" yaml:"enable"`
	// Type specifies the authentication type.
	Type string `json:"type" yaml:"type"`
	// KeyAuth defines configuration for key authentication.
	KeyAuth ApisixRouteAuthenticationKeyAuth `json:"keyAuth,omitempty" yaml:"keyAuth,omitempty"`
	// JwtAuth defines configuration for JWT authentication.
	JwtAuth ApisixRouteAuthenticationJwtAuth `json:"jwtAuth,omitempty" yaml:"jwtAuth,omitempty"`
	// LDAPAuth defines configuration for LDAP authentication.
	LDAPAuth ApisixRouteAuthenticationLDAPAuth `json:"ldapAuth,omitempty" yaml:"ldapAuth,omitempty"`
}

// ApisixRouteStreamMatch represents the matching conditions for a stream route.
type ApisixRouteStreamMatch struct {
	// IngressPort is the port on which the APISIX Ingress proxy server listens.
	// This must be a statically configured port, as APISIX does not support dynamic port binding.
	//
	// +kubebuilder:validation:Minimum=0
	// +kubebuilder:validation:Maximum=65535
	IngressPort int32 `json:"ingressPort" yaml:"ingressPort"`
	// Host is the destination host address used to match the incoming TCP/UDP traffic.
	Host string `json:"host,omitempty" yaml:"host,omitempty"`
}

// ApisixRouteStreamBackend represents the backend service for a TCP or UDP stream route.
type ApisixRouteStreamBackend struct {
	// ServiceName is the name of the Kubernetes Service.
	// Cross-namespace references are not supported—ensure the ApisixRoute
	// and the Service are in the same namespace.
	ServiceName string `json:"serviceName" yaml:"serviceName"`
	// ServicePort is the port of the Kubernetes Service.
	// This can be either the port name or port number.
	ServicePort intstr.IntOrString `json:"servicePort" yaml:"servicePort"`
	// ResolveGranularity determines how the backend service is resolved.
	// Valid values are `endpoint` and `service`. When set to `endpoint`,
	// individual pod IPs will be used; otherwise, the Service's ClusterIP or ExternalIP is used.
	// The default is `endpoint`.
	//
	// +kubebuilder:default=endpoint
	// +kubebuilder:validation:Enum=endpoint;service
	ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"`
	// Subset specifies a named subset of the target Service.
	// The subset must be pre-defined in the corresponding ApisixUpstream resource.
	Subset string `json:"subset,omitempty" yaml:"subset,omitempty"`
}

// ApisixRouteHTTPMatchExpr represents a binary expression used to match requests based on Nginx variables.
type ApisixRouteHTTPMatchExpr struct {
	// Subject defines the left-hand side of the expression.
	// It can be any [APISIX variable](https://apisix.apache.org/docs/apisix/apisix-variable) or string literal.
	Subject ApisixRouteHTTPMatchExprSubject `json:"subject" yaml:"subject"`

	// Op specifies the operator used in the expression.
	// Can be `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanEqual`, `LessThan`, `LessThanEqual`, `RegexMatch`,
	// `RegexNotMatch`, `RegexMatchCaseInsensitive`, `RegexNotMatchCaseInsensitive`, `In`, or `NotIn`.
	Op string `json:"op" yaml:"op"`

	// Set provides a list of acceptable values for the expression.
	// This should be used when Op is `In` or `NotIn`.
	// +kubebuilder:validation:Optional
	Set []string `json:"set" yaml:"set"`

	// Value defines a single value to compare against the subject.
	// This should be used when Op is not `In` or `NotIn`.
	// Set and Value are mutually exclusive—only one should be set at a time.
	// +kubebuilder:validation:Optional
	Value *string `json:"value" yaml:"value"`
}

type ApisixRouteHTTPMatchExprs []ApisixRouteHTTPMatchExpr

func (exprs ApisixRouteHTTPMatchExprs) ToVars() (result adc.Vars, err error) {
	for _, expr := range exprs {
		if expr.Subject.Name == "" && expr.Subject.Scope != ScopePath {
			return result, errors.New("empty subject.name")
		}

		// process key
		var (
			subj string
			this adc.StringOrSlice
		)
		switch expr.Subject.Scope {
		case ScopeQuery:
			subj = "arg_" + expr.Subject.Name
		case ScopeHeader:
			subj = "http_" + strings.ReplaceAll(strings.ToLower(expr.Subject.Name), "-", "_")
		case ScopeCookie:
			subj = "cookie_" + expr.Subject.Name
		case ScopePath:
			subj = "uri"
		case ScopeVariable:
			subj = expr.Subject.Name
		case ScopeBody:
			subj = "post_arg." + expr.Subject.Name
		default:
			return result, errors.New("invalid http match expr: subject.scope should be one of [Query, Header, Cookie, Path, Variable, Body]")
		}
		this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj})

		// process operator
		var (
			op string
		)
		switch expr.Op {
		case OpEqual:
			op = "=="
		case OpGreaterThan:
			op = ">"
		case OpGreaterThanEqual:
			op = ">="
		case OpIn:
			op = "in"
		case OpLessThan:
			op = "<"
		case OpLessThanEqual:
			op = "<="
		case OpNotEqual:
			op = "~="
		case OpNotIn:
			op = "in"
		case OpRegexMatch:
			op = "~~"
		case OpRegexMatchCaseInsensitive:
			op = "~*"
		case OpRegexNotMatch:
			op = "~~"
		case OpRegexNotMatchCaseInsensitive:
			op = "~*"
		default:
			return result, errors.New("unknown operator")
		}
		if expr.Op == OpNotIn || expr.Op == OpRegexNotMatch || expr.Op == OpRegexNotMatchCaseInsensitive {
			this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"})
		}
		this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op})

		// process value
		switch expr.Op {
		case OpIn, OpNotIn:
			if expr.Set == nil {
				return result, errors.New("empty set value")
			}
			var value adc.StringOrSlice
			for _, item := range expr.Set {
				value.SliceVal = append(value.SliceVal, adc.StringOrSlice{StrVal: item})
			}
			this.SliceVal = append(this.SliceVal, value)
		default:
			if expr.Value == nil {
				return result, errors.New("empty value")
			}
			this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: *expr.Value})
		}

		// append to result
		result = append(result, this.SliceVal)
	}

	return result, nil
}

// ApisixRoutePluginConfig is the configuration for
// any plugins.
type ApisixRoutePluginConfig map[string]apiextensionsv1.JSON

// ApisixRouteAuthenticationKeyAuth defines key authentication configuration in ApisixRouteAuthentication.
type ApisixRouteAuthenticationKeyAuth struct {
	// Header specifies the HTTP header name to look for the key authentication token.
	Header string `json:"header,omitempty" yaml:"header,omitempty"`
}

// ApisixRouteAuthenticationJwtAuth defines JWT authentication configuration in ApisixRouteAuthentication.
type ApisixRouteAuthenticationJwtAuth struct {
	// Header specifies the HTTP header name to look for the JWT token.
	Header string `json:"header,omitempty" yaml:"header,omitempty"`
	// Query specifies the URL query parameter name to look for the JWT token.
	Query string `json:"query,omitempty" yaml:"query,omitempty"`
	// Cookie specifies the cookie name to look for the JWT token.
	Cookie string `json:"cookie,omitempty" yaml:"cookie,omitempty"`
}

// ApisixRouteAuthenticationLDAPAuth defines LDAP authentication configuration in ApisixRouteAuthentication.
type ApisixRouteAuthenticationLDAPAuth struct {
	// BaseDN is the base distinguished name (DN) for LDAP searches.
	BaseDN string `json:"base_dn,omitempty" yaml:"base_dn,omitempty"`
	// LDAPURI is the URI of the LDAP server.
	LDAPURI string `json:"ldap_uri,omitempty" yaml:"ldap_uri,omitempty"`
	// UseTLS indicates whether to use TLS for the LDAP connection.
	UseTLS bool `json:"use_tls,omitempty" yaml:"use_tls,omitempty"`
	// UID is the user identifier attribute in LDAP.
	UID string `json:"uid,omitempty" yaml:"uid,omitempty"`
}

// ApisixRouteHTTPMatchExprSubject describes the subject of a route matching expression.
// +kubebuilder:validation:XValidation:rule="self.scope == 'Path' || size(self.name) > 0",message="name is required when scope is not Path"
type ApisixRouteHTTPMatchExprSubject struct {
	// Scope specifies the subject scope.
	// Supported values: `Header`, `Query`, `Path`, `Cookie`, `Variable`, `Body`.
	// When Scope is `Path`, Name will be ignored.
	// When Scope is `Body`, Name supports dot-notation JSON path (e.g., "model.version",
	// "messages[*].role") and maps to APISIX's `post_arg.<name>` variable, which works with
	// application/json, application/x-www-form-urlencoded, and multipart/form-data.
	// +kubebuilder:validation:Enum=Header;Query;Path;Cookie;Variable;Body
	Scope string `json:"scope" yaml:"scope"`
	// Name is the name of the subject within the given scope: the header name, query
	// parameter name, cookie name, Nginx variable name, or body field name (dot-notation
	// JSON path supported for Body scope). Optional when Scope is Path.
	// +kubebuilder:validation:Optional
	Name string `json:"name,omitempty" yaml:"name,omitempty"`
}

func init() {
	SchemeBuilder.Register(&ApisixRoute{}, &ApisixRouteList{})
}
