/*
 * Copyright (c) Microsoft Corporation.
 * Licensed under the MIT license.
 */

// +kubebuilder:validation:Optional
package genruntime

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
	"github.com/rotisserie/eris"
	"k8s.io/apimachinery/pkg/runtime/schema"
	kerrors "k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	"github.com/Azure/azure-service-operator/v2/internal/set"
)

// KnownResourceReference is a resource reference to a known type.
// +kubebuilder:object:generate=true
type KnownResourceReference struct {
	// TODO: In practice this type is used only for Owner fields and so might more appropriately have been called OwnerReference
	// TODO: but changing to that would be a breaking change so avoiding it.

	// This is the name of the Kubernetes resource to reference.
	Name string `json:"name,omitempty"`

	// References across namespaces are not supported.
	// Note that ownership across namespaces in Kubernetes is not allowed, but technically resource
	// references are. There are RBAC considerations here though so probably easier to just start by
	// disallowing cross-namespace references for now

	// +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)"
	ARMID string `json:"armId,omitempty"`
}

// AsResourceReference transforms this KnownResourceReference into a ResourceReference
func (ref *KnownResourceReference) AsResourceReference(group string, kind string) *ResourceReference {
	if ref.Name == "" {
		group = ""
		kind = ""
	}

	return &ResourceReference{
		Group: group,
		Kind:  kind,
		Name:  ref.Name,
		ARMID: ref.ARMID,
	}
}

// KubernetesOwnerReference is a resource reference to a known type in Kuberentes. Most types support
// ARM references as well but some (such as SQL users) do not.
// +kubebuilder:object:generate=true
type KubernetesOwnerReference struct {
	// +kubebuilder:validation:Required
	// This is the name of the Kubernetes resource to reference.
	Name string `json:"name,omitempty"`
}

// AsResourceReference transforms this KnownResourceReference into a ResourceReference
func (ref *KubernetesOwnerReference) AsResourceReference(group string, kind string) *ResourceReference {
	return &ResourceReference{
		Group: group,
		Kind:  kind,
		Name:  ref.Name,
	}
}

// TODO: This type and ResourceReference are almost exactly the same now...
// ArbitraryOwnerReference is an owner reference to an unknown type.
// +kubebuilder:object:generate=true
type ArbitraryOwnerReference struct {
	// This is the name of the Kubernetes resource to reference.
	Name string `json:"name,omitempty"`

	// Group is the Kubernetes group of the resource.
	Group string `json:"group,omitempty"`

	// Kind is the Kubernetes kind of the resource.
	Kind string `json:"kind,omitempty"`

	// Ownership across namespaces is not supported.
	// +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)"
	ARMID string `json:"armId,omitempty"`
}

// AsResourceReference transforms this ArbitraryOwnerReference into a ResourceReference
func (ref *ArbitraryOwnerReference) AsResourceReference() *ResourceReference {
	return &ResourceReference{
		Group: ref.Group,
		Kind:  ref.Kind,
		Name:  ref.Name,
		ARMID: ref.ARMID,
	}
}

var _ fmt.Stringer = ResourceReference{}

// ResourceReference represents a resource reference, either to a Kubernetes resource or directly to an Azure resource via ARMID
// +kubebuilder:object:generate=true
//
//nolint:recvcheck
type ResourceReference struct {
	// Group is the Kubernetes group of the resource.
	Group string `json:"group,omitempty"`
	// Kind is the Kubernetes kind of the resource.
	Kind string `json:"kind,omitempty"`
	// Name is the Kubernetes name of the resource.
	Name string `json:"name,omitempty"`

	// Note: Version is not required here because references are all about linking one Kubernetes
	// resource to another, and Kubernetes resources are uniquely identified by group, kind, (optionally namespace) and
	// name - the versions are just giving a different view on the same resource
	// Here are some test patterns for it: https://regex101.com/r/K7l3sv/1

	// +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)"
	// ARMID is a string of the form /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}.
	// The /resourcegroups/{resourceGroupName} bit is optional as some resources are scoped at the subscription level
	// ARMID is mutually exclusive with Group, Kind, Namespace and Name.
	ARMID string `json:"armId,omitempty"`
}

// CreateResourceReferenceFromARMID creates a new ResourceReference from a string representing an ARM ID
func CreateResourceReferenceFromARMID(armID string) ResourceReference {
	return ResourceReference{
		ARMID: armID,
	}
}

// IsDirectARMReference returns true if this ResourceReference is referring to an ARMID directly.
func (ref *ResourceReference) IsDirectARMReference() bool {
	return ref.ARMID != "" && ref.Name == "" && ref.Group == "" && ref.Kind == ""
}

// IsKubernetesReference returns true if this ResourceReference is referring to a Kubernetes resource.
func (ref *ResourceReference) IsKubernetesReference() bool {
	return ref.ARMID == "" && ref.Name != "" && ref.Group != "" && ref.Kind != ""
}

func (ref ResourceReference) String() string {
	if ref.IsDirectARMReference() {
		return ref.ARMID
	}

	if ref.IsKubernetesReference() {
		return fmt.Sprintf("%s, Group/Kind: %s/%s", ref.Name, ref.Group, ref.Kind)
	}

	// Printing all the fields here just in case something weird happens and we have an ARMID and also Kubernetes reference stuff
	return fmt.Sprintf("Group: %q, Kind: %q, Name: %q, ARMID: %q", ref.Group, ref.Kind, ref.Name, ref.ARMID)
}

// TODO: We wouldn't need this if controller-gen supported DUs or OneOf better, see: https://github.com/kubernetes-sigs/controller-tools/issues/461
// Validate validates the ResourceReference to ensure that it is structurally valid.
func (ref *ResourceReference) Validate() (admission.Warnings, error) {
	if ref.ARMID == "" && ref.Name == "" && ref.Group == "" && ref.Kind == "" {
		return nil, eris.Errorf("at least one of ['ARMID'] or ['Group', 'Kind', 'Namespace', 'Name'] must be set for ResourceReference")
	}

	if ref.ARMID != "" && !ref.IsDirectARMReference() {
		return nil, eris.Errorf("the 'ARMID' field is mutually exclusive with 'Group', 'Kind', 'Namespace', and 'Name' for ResourceReference: %s", ref.String())
	}

	if ref.ARMID == "" && !ref.IsKubernetesReference() {
		return nil, eris.Errorf("when referencing a Kubernetes resource, 'Group', 'Kind', 'Namespace', and 'Name' must all be specified for ResourceReference: %s", ref.String())
	}

	return nil, nil
}

// AsNamespacedRef creates a NamespacedResourceReference from this reference.
func (ref *ResourceReference) AsNamespacedRef(namespace string) NamespacedResourceReference {
	// If this is a direct ARM reference, don't append a namespace as it reads weird
	if ref.IsDirectARMReference() {
		return NamespacedResourceReference{
			ResourceReference: *ref,
		}
	}

	return NamespacedResourceReference{
		ResourceReference: *ref,
		Namespace:         namespace,
	}
}

// AsArbitraryOwnerReference creates an ArbitraryOwnerReference from this reference.
func (ref *ResourceReference) AsArbitraryOwnerReference() ArbitraryOwnerReference {
	// If this is a direct ARM reference, return just the ARM  ID
	if ref.IsDirectARMReference() {
		return ArbitraryOwnerReference{
			ARMID: ref.ARMID,
		}
	}

	// Otherwise return GVK
	return ArbitraryOwnerReference{
		Group: ref.Group,
		Kind:  ref.Kind,
		Name:  ref.Name,
	}
}

// AsKnownResourceReference creates a KnownResourceReference from this reference.
func (ref *ResourceReference) AsKnownResourceReference() KnownResourceReference {
	// If this is a direct ARM reference, return just the ARM  ID
	if ref.IsDirectARMReference() {
		return KnownResourceReference{
			ARMID: ref.ARMID,
		}
	}

	// Otherwise return just the name
	return KnownResourceReference{
		Name: ref.Name,
	}
}

// GroupKind returns the GroupKind of the resource reference
func (ref *ResourceReference) GroupKind() schema.GroupKind {
	return schema.GroupKind{
		Group: ref.Group,
		Kind:  ref.Kind,
	}
}

// LookupOwnerGroupKind looks up an owners group and kind annotations using reflection.
// This is primarily used to convert from a KnownResourceReference to the more general
// ResourceReference
func LookupOwnerGroupKind(v interface{}) (string, string) {
	t := reflect.TypeOf(v)
	field, _ := t.FieldByName("Owner")

	group, ok := field.Tag.Lookup("group")
	if !ok {
		panic("Couldn't find owner group tag")
	}
	kind, ok := field.Tag.Lookup("kind")
	if !ok {
		panic("Couldn't find %s owner kind tag")
	}

	return group, kind
}

// Copy makes an independent copy of the KnownResourceReference
func (ref *KnownResourceReference) Copy() KnownResourceReference {
	return *ref
}

// Copy makes an independent copy of the ArbitraryOwnerReference
func (ref *ArbitraryOwnerReference) Copy() ArbitraryOwnerReference {
	return *ref
}

// Copy makes an independent copy of the ResourceReference
func (ref ResourceReference) Copy() ResourceReference {
	return ref
}

// Copy makes an independent copy of the KubernetesOwnerReference
func (ref *KubernetesOwnerReference) Copy() KubernetesOwnerReference {
	return *ref
}

// ValidateResourceReferences calls Validate on each ResourceReference
func ValidateResourceReferences(refs set.Set[ResourceReference]) (admission.Warnings, error) {
	errs := make([]error, 0, len(refs))
	var warnings admission.Warnings
	for ref := range refs {
		warning, err := ref.Validate()
		if warning != nil {
			warnings = append(warnings, warning...)
		}
		if err != nil {
			errs = append(errs, err)
		}
	}

	return warnings, kerrors.NewAggregate(errs)
}

func VerifyResourceOwnerARMID(resource ARMMetaObject) error {
	owner := resource.Owner()
	if owner == nil {
		return nil
	}
	if !owner.IsDirectARMReference() {
		return nil
	}

	armID, err := arm.ParseResourceID(owner.ARMID)
	if err != nil {
		return err
	}

	provider, rootResourceTypes, err := GetResourceTypeAndProvider(resource)
	if err != nil {
		return err
	}
	expectedResourceTypesIncludedInARMID := rootResourceTypes[:len(rootResourceTypes)-1]

	// Ensure that the ARM ID actually has a suffix containing the resource types we expect
	if len(expectedResourceTypesIncludedInARMID) > 0 {
		if !strings.EqualFold(armID.ResourceType.Namespace, provider) {
			return eris.Errorf(
				"expected owner ARM ID to be from provider %q, but was %q",
				provider,
				armID.ResourceType.Namespace)
		}
		expectedARMIDType := strings.Join(expectedResourceTypesIncludedInARMID, "/")
		if !strings.EqualFold(armID.ResourceType.Type, expectedARMIDType) {
			return eris.Errorf(
				"expected owner ARM ID to be of type %q, but was %q",
				fmt.Sprintf("%s/%s", provider, expectedARMIDType),
				armID.ResourceType.String())
		}
	} else if len(expectedResourceTypesIncludedInARMID) == 0 {
		scope := resource.GetResourceScope()
		if scope == ResourceScopeResourceGroup && armID.ResourceType.String() != "Microsoft.Resources/resourceGroups" {
			return eris.Errorf(
				"expected owner ARM ID to be for a resource group, but was %q",
				armID.ResourceType.String())
		}
	}

	return nil
}

// ValidateOwner calls Validate on the resource Owner
func ValidateOwner(obj ARMMetaObject) (admission.Warnings, error) {
	owner := obj.Owner()
	if owner == nil {
		return nil, nil
	}

	var warningsResult admission.Warnings

	warnings, err := owner.Validate()
	warningsResult = append(warningsResult, warnings...)
	if err != nil {
		return warningsResult, err
	}

	err = VerifyResourceOwnerARMID(obj)
	if err != nil {
		return warningsResult, err
	}

	return warningsResult, nil
}

// NamespacedResourceReference is a resource reference with namespace information included
type NamespacedResourceReference struct {
	ResourceReference
	Namespace string
}
