package preflight

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
	"github.com/Azure/terraform-provider-azapi/internal/azure/identity"
	aztypes "github.com/Azure/terraform-provider-azapi/internal/azure/types"
	"github.com/Azure/terraform-provider-azapi/internal/clients"
	"github.com/Azure/terraform-provider-azapi/internal/services/dynamic"
	"github.com/Azure/terraform-provider-azapi/internal/services/parse"
	"github.com/Azure/terraform-provider-azapi/utils"
	"github.com/hashicorp/terraform-plugin-framework/attr"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-log/tflog"
	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
)

type RequestBodyModel struct {
	Provider  string                   `json:"provider"`
	Type      string                   `json:"type"`
	Location  string                   `json:"location"`
	Scope     string                   `json:"scope"`
	Resources []map[string]interface{} `json:"resources"`
}

// ParentIdPlaceholder generates a placeholder for the parentID based on the resource definition and subscription ID
func ParentIdPlaceholder(resourceDef *aztypes.ResourceType, subscriptionId string) (string, error) {
	// since the parentID is faked, there should exist only one scope type
	if resourceDef == nil || len(resourceDef.ScopeTypes) != 1 {
		return "", fmt.Errorf("failed to generate parentID placeholder because the resource definition is invalid")
	}

	scopeId := ""
	switch resourceDef.ScopeTypes[0] {
	case aztypes.Tenant:
		scopeId = "/"
	case aztypes.ManagementGroup:
		scopeId = "/providers/Microsoft.Management/managementGroups/" + NamePlaceholder()
	case aztypes.Subscription:
		scopeId = fmt.Sprintf("/subscriptions/%s", subscriptionId)
	case aztypes.ResourceGroup:
		scopeId = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionId, NamePlaceholder())
	default:
		return "", fmt.Errorf("failed to generate parentID placeholder because the scope type is not supported")
	}

	// for top-level resources, the parent ID is the same as the scope ID
	resourceType := strings.Split(resourceDef.Name, "@")[0]
	if utils.IsTopLevelResourceType(resourceType) {
		return scopeId, nil
	}

	parts := strings.Split(resourceType, "/")

	parentId := fmt.Sprintf("%s/providers/%s", scopeId, parts[0])
	for i := 1; i < len(parts)-1; i++ {
		parentId += fmt.Sprintf("/%s/%s", parts[i], NamePlaceholder())
	}

	return parentId, nil
}

// NamePlaceholder generates a random name placeholder
func NamePlaceholder() string {
	return acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)
}

// Validate validates the resource using the preflight API
func Validate(ctx context.Context, client *clients.ResourceClient, resourceType string, parentId string, name string, location string, body types.Dynamic, identity types.List) error {
	azureResourceType, apiVersion, err := utils.GetAzureResourceTypeApiVersion(resourceType)
	if err != nil {
		return err
	}

	payload := RequestBodyModel{}
	payload.Provider, payload.Type, _ = strings.Cut(azureResourceType, "/")
	id, err := parse.NewResourceIDSkipScopeValidation(name, parentId, resourceType)
	if err != nil {
		return err
	}
	scopeId, err := ScopeID(id.ID())
	if err != nil {
		return err
	}
	payload.Scope = scopeId
	if location != "" {
		payload.Location = location
	}

	resource := make(map[string]interface{})
	err = unmarshalPreflightBody(body, identity, &resource)
	if err != nil {
		tflog.Warn(ctx, fmt.Sprintf("Skipping preflight validation for resource %s because the body is invalid: %v", resourceType, err))
		return nil
	}

	// the resource type and name must have same number of segment("/"), for example, if type is "virtualNetworks/subnets", the name field should be "vnetName/subnetName"
	resourceAndParentName, err := ResourceAndParentName(id.ID(), scopeId)
	if err != nil {
		return err
	}
	resource["name"] = resourceAndParentName

	resource["apiVersion"] = apiVersion

	payload.Resources = []map[string]interface{}{resource}

	_, err = client.Action(ctx, "/providers/Microsoft.Resources", "validateResources", "2020-10-01", "POST", payload, clients.DefaultRequestOptions())
	return err
}

func ScopeID(resourceId string) (string, error) {
	armId, err := arm.ParseResourceID(resourceId)
	if err != nil {
		return "", err
	}
	if armId.Parent == nil {
		return "/", nil
	}
	scopeId := armId.Parent
	for scopeId.Parent != nil &&
		!strings.EqualFold(scopeId.ResourceType.String(), arm.SubscriptionResourceType.String()) &&
		!strings.EqualFold(scopeId.ResourceType.String(), arm.ResourceGroupResourceType.String()) &&
		!strings.EqualFold(scopeId.ResourceType.String(), arm.TenantResourceType.String()) &&
		!strings.EqualFold(scopeId.ResourceType.String(), "Microsoft.Management/managementGroups") {
		scopeId = scopeId.Parent
	}

	if scopeId == nil || scopeId.ResourceType.String() == arm.TenantResourceType.String() {
		return "/", nil
	}

	return scopeId.String(), nil
}

func ResourceAndParentName(resourceId, scopeId string) (string, error) {
	armId, err := arm.ParseResourceID(resourceId)
	if err != nil {
		return "", err
	}

	resourceAndParentName := armId.Name
	for armId.Parent != nil && armId.Parent.String() != scopeId && armId.Parent.Name != "" {
		resourceAndParentName = fmt.Sprintf("%s/%s", armId.Parent.Name, resourceAndParentName)
		armId = armId.Parent
	}

	return resourceAndParentName, nil
}

func unmarshalPreflightBody(input types.Dynamic, identityList types.List, out *map[string]interface{}) error {
	if input.IsNull() || input.IsUnknown() || input.IsUnderlyingValueUnknown() {
		return fmt.Errorf("input is null or unknown")
	}

	const unknownPlaceholder = "[length('foo')]"

	data, err := dynamic.ToJSONWithUnknownValueHandler(input, func(value attr.Value) ([]byte, error) {
		return json.Marshal(unknownPlaceholder)
	})
	if err != nil {
		return fmt.Errorf("marshaling failed: %v", err)
	}

	if err = json.Unmarshal(data, &out); err != nil {
		return fmt.Errorf(`unmarshaling failed: value: %s, err: %+v`, string(data), err)
	}

	if out == nil {
		out = &map[string]interface{}{}
	}
	// make sure that there's no unknown value outside the properties bag
	for k, v := range *out {
		if k == "properties" {
			continue
		}
		if searchForValue(v, unknownPlaceholder) {
			return fmt.Errorf("unknown value found outside the properties bag")
		}
	}

	if (*out)["identity"] == nil && !identityList.IsNull() && !identityList.IsUnknown() {
		identityModel := identity.FromList(identityList)
		expandedIdentity, err := identity.ExpandIdentity(identityModel)
		if err != nil {
			return err
		}
		(*out)["identity"] = expandedIdentity
	}
	return nil
}

func searchForValue(input interface{}, target string) bool {
	switch v := input.(type) {
	case map[string]interface{}:
		for _, value := range v {
			if searchForValue(value, target) {
				return true
			}
		}
	case []interface{}:
		for _, value := range v {
			if searchForValue(value, target) {
				return true
			}
		}
	case string:
		if v == target {
			return true
		}
	}

	return false
}
