package sfcrmappservice

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"regexp"
	"sort"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
)

const effectiveObjectActionComponentKind = "metadata.effective.object-action.v1"

var actionConfigIdentifierPattern = regexp.MustCompile(`[A-Za-z_][A-Za-z0-9_]*`)

type objectQuickActionsRoute struct {
	version  string
	objectID string
}

type effectiveObjectActionDocument struct {
	ObjectAPIName string                      `json:"objectApiName"`
	Actions       []effectiveObjectActionSpec `json:"actions"`
}

type effectiveObjectActionSpec struct {
	ID           string         `json:"id"`
	Name         string         `json:"name"`
	Label        string         `json:"label"`
	Type         string         `json:"type"`
	Icon         string         `json:"icon,omitempty"`
	TargetObject string         `json:"targetObject,omitempty"`
	Config       map[string]any `json:"config,omitempty"`
}

type quickActionWireResponse struct {
	ID            string         `json:"id"`
	ObjectAPIName string         `json:"objectApiName"`
	Name          string         `json:"name"`
	Label         string         `json:"label"`
	Type          string         `json:"type"`
	Icon          string         `json:"icon"`
	TargetObject  *string        `json:"targetObject,omitempty"`
	Config        map[string]any `json:"config,omitempty"`
}

func matchObjectQuickActionsRoute(path string) (objectQuickActionsRoute, bool) {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) != 6 || parts[0] != "services" || parts[1] != "data" ||
		parts[3] != "sobjects" || parts[5] != "quickActions" ||
		!supportedAPIVersion(parts[2]) || !validSearchIdentifier(parts[4]) {
		return objectQuickActionsRoute{}, false
	}
	return objectQuickActionsRoute{version: parts[2], objectID: parts[4]}, true
}

func objectActionsFromView(
	view catalogsemantic.AppMetadataViewV3,
	objectID string,
) ([]effectiveObjectActionSpec, error) {
	var component *catalogsemantic.EffectiveMetadataComponent
	for index := range view.EffectiveComponents {
		candidate := &view.EffectiveComponents[index]
		if candidate.ComponentKind != effectiveObjectActionComponentKind ||
			!strings.EqualFold(candidate.BusinessKey, objectID) {
			continue
		}
		if component != nil || candidate.ComponentFormatVersion != 1 {
			return nil, unavailableSearch()
		}
		component = candidate
	}
	if component == nil {
		return nil, &permissionsemantic.Failure{Category: permissionsemantic.FailureMissing}
	}
	var document effectiveObjectActionDocument
	decoder := json.NewDecoder(bytes.NewReader(component.CanonicalBehavior))
	if err := decoder.Decode(&document); err != nil ||
		!strings.EqualFold(document.ObjectAPIName, objectID) || document.Actions == nil {
		return nil, unavailableSearch()
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, unavailableSearch()
	}
	seen := make(map[string]struct{}, len(document.Actions))
	for _, action := range document.Actions {
		if strings.TrimSpace(action.Name) == "" || strings.TrimSpace(action.Label) == "" ||
			strings.TrimSpace(action.Type) == "" {
			return nil, unavailableSearch()
		}
		key := strings.ToLower(action.Name)
		if _, duplicate := seen[key]; duplicate {
			return nil, unavailableSearch()
		}
		seen[key] = struct{}{}
	}
	sort.Slice(document.Actions, func(left, right int) bool {
		return document.Actions[left].Name < document.Actions[right].Name
	})
	return document.Actions, nil
}

func projectQuickActions(
	objectID string,
	actions []effectiveObjectActionSpec,
	specs map[string]searchObjectSpec,
	permissions permissionsemantic.Response,
) []quickActionWireResponse {
	result := make([]quickActionWireResponse, 0, len(actions))
	for _, action := range actions {
		if !quickActionAuthorized(objectID, action, specs, permissions) {
			continue
		}
		var target *string
		if action.TargetObject != "" {
			value := action.TargetObject
			target = &value
		}
		result = append(result, quickActionWireResponse{
			ID: action.ID, ObjectAPIName: objectID, Name: action.Name,
			Label: action.Label, Type: action.Type, Icon: action.Icon,
			TargetObject: target, Config: action.Config,
		})
	}
	return result
}

func quickActionAuthorized(
	baseObject string,
	action effectiveObjectActionSpec,
	specs map[string]searchObjectSpec,
	permissions permissionsemantic.Response,
) bool {
	targetObject := quickActionTargetObject(action)
	createsTarget, valid := quickActionCreatesTarget(action, targetObject)
	if !valid {
		return false
	}
	basePermission, readable := objectPermissionForDescribe(permissions, baseObject)
	if !readable {
		return false
	}
	switch action.Type {
	case "ChangeOwner", "UpdateStatus":
		if !basePermission.CanEdit {
			return false
		}
	case "NavigateToNewRecord", "NavigateToRelatedList":
		if targetObject == "" {
			return false
		}
		targetPermission, ok := objectPermissionForDescribe(permissions, targetObject)
		if !ok || (createsTarget && !targetPermission.CanCreate) {
			return false
		}
	case "Standard":
		allowed := basePermission.CanEdit
		switch action.Name {
		case "Clone":
			allowed = basePermission.CanCreate
		case "Delete":
			allowed = basePermission.CanDelete
		}
		if !allowed {
			return false
		}
	default:
		if !basePermission.CanEdit {
			return false
		}
	}

	objects, identifiers, ok := quickActionReferences(baseObject, action, specs)
	if !ok {
		return false
	}
	for objectKey := range objects {
		spec, found := specs[strings.ToLower(objectKey)]
		if !found || !describeAuthorityIsCurrent(spec) {
			return false
		}
		if _, readable := objectPermissionForDescribe(permissions, spec.APIName); !readable {
			return false
		}
		fieldPermissions := fieldPermissionsForDescribe(permissions, spec.APIName)
		for _, field := range spec.Fields {
			if _, referenced := identifiers[strings.ToLower(field.APIName)]; !referenced {
				continue
			}
			permission, readable := fieldPermissions[strings.ToLower(field.APIName)]
			if !readable || !permission.CanRead {
				return false
			}
		}
	}
	if lookup, ok := action.Config["lookupField"].(string); ok && lookup != "" {
		target := targetObject
		if target == "" {
			target = baseObject
		}
		spec, found := specs[strings.ToLower(target)]
		permission, permitted := fieldPermissionsForDescribe(permissions, target)[strings.ToLower(lookup)]
		field, exists := layoutFieldSpec(spec, lookup)
		if !found || !exists || !permitted || !permission.CanRead {
			return false
		}
		// Use the same resolved operation as the object gate. A navigation
		// filter needs read access; a configured creation writes this field.
		if createsTarget {
			if !permission.CanCreate || !field.Createable {
				return false
			}
		} else if action.Type != "NavigateToRelatedList" {
			return false
		}
	}
	return true
}

// quickActionCreatesTarget derives creation from the existing action payload,
// not its display name. Both object and lookup-field gates consume this fact.
// A malformed/conflicting picker must not degrade into read-only navigation.
func quickActionCreatesTarget(action effectiveObjectActionSpec, target string) (bool, bool) {
	if configured, ok := action.Config["targetObject"]; ok {
		value, valid := configured.(string)
		if !valid || strings.TrimSpace(value) != target {
			return false, false
		}
	}
	if action.Type == "NavigateToNewRecord" {
		return true, true
	}
	if action.Type != "NavigateToRelatedList" {
		return false, true
	}
	raw, present := action.Config["productPicker"]
	if !present {
		return false, true
	}
	picker, ok := raw.(map[string]any)
	if !ok {
		return false, false
	}
	batch, ok := picker["batchCreate"].(map[string]any)
	if !ok {
		return false, false
	}
	configuredTarget, _ := action.Config["targetObject"].(string)
	batchTarget, _ := batch["targetObject"].(string)
	lineTarget, _ := picker["lineItemObject"].(string)
	parentLookup, _ := picker["parentLookupField"].(string)
	lookup, _ := action.Config["lookupField"].(string)
	if target == "" || configuredTarget != target || batchTarget != target || lineTarget != target || lookup == "" || parentLookup != lookup {
		return false, false
	}
	return true, true
}

func quickActionTargetObject(action effectiveObjectActionSpec) string {
	targetObject := strings.TrimSpace(action.TargetObject)
	if targetObject == "" {
		if configuredTarget, ok := action.Config["targetObject"].(string); ok {
			targetObject = strings.TrimSpace(configuredTarget)
		}
	}
	return targetObject
}

func quickActionReferences(
	baseObject string,
	action effectiveObjectActionSpec,
	specs map[string]searchObjectSpec,
) (map[string]struct{}, map[string]struct{}, bool) {
	encoded, err := json.Marshal(action.Config)
	if err != nil {
		return nil, nil, false
	}
	identifiers := make(map[string]struct{})
	for _, identifier := range actionConfigIdentifierPattern.FindAllString(string(encoded), -1) {
		identifiers[strings.ToLower(identifier)] = struct{}{}
	}
	objects := map[string]struct{}{strings.ToLower(baseObject): {}}
	if targetObject := quickActionTargetObject(action); targetObject != "" {
		objects[strings.ToLower(targetObject)] = struct{}{}
	}
	for objectKey, spec := range specs {
		if _, referenced := identifiers[strings.ToLower(spec.APIName)]; referenced {
			objects[objectKey] = struct{}{}
		}
	}
	return objects, identifiers, true
}

func quickActionPermissionSelection(
	baseObject string,
	actions []effectiveObjectActionSpec,
	specs map[string]searchObjectSpec,
) (*permissionsemantic.Selection, error) {
	resources := make(map[string]string)
	baseSpec, found := specs[strings.ToLower(baseObject)]
	if !found || !describeAuthorityIsCurrent(baseSpec) {
		return nil, unavailableSearch()
	}
	resources[strings.ToLower(baseSpec.APIName)] = baseSpec.APIName
	for _, action := range actions {
		objectKeys, _, ok := quickActionReferences(baseObject, action, specs)
		if !ok {
			return nil, unavailableSearch()
		}
		for objectKey := range objectKeys {
			spec, found := specs[strings.ToLower(objectKey)]
			if !found || !describeAuthorityIsCurrent(spec) {
				continue
			}
			resources[strings.ToLower(spec.APIName)] = spec.APIName
		}
	}
	if len(resources) == 0 || len(resources) > permissionsemantic.MaxSelectedResourceTypes {
		return nil, unavailableSearch()
	}
	resourceTypes := make([]string, 0, len(resources))
	for _, resourceType := range resources {
		resourceTypes = append(resourceTypes, resourceType)
	}
	sort.Strings(resourceTypes)
	return &permissionsemantic.Selection{
		Sections: []permissionsemantic.Section{
			permissionsemantic.SectionObjectPermissions,
			permissionsemantic.SectionFieldPermissions,
		},
		ResourceTypes: resourceTypes,
	}, nil
}

func (service *Service) readObjectQuickActions(
	ctx context.Context,
	bearer string,
	route objectQuickActionsRoute,
) (map[string]any, error) {
	view, specs, err := readObjectSpecs(ctx, service.appMetadataV3)
	if err != nil {
		return nil, err
	}
	spec, found := specs[strings.ToLower(route.objectID)]
	if !found || !describeAuthorityIsCurrent(spec) {
		return nil, &permissionsemantic.Failure{Category: permissionsemantic.FailureMissing}
	}
	actions, err := objectActionsFromView(view, spec.APIName)
	if err != nil {
		return nil, err
	}
	selection, err := quickActionPermissionSelection(spec.APIName, actions, specs)
	if err != nil {
		return nil, err
	}
	revision := view.Receipts.CatalogRevision
	permissionRequest := permissionsemantic.Request{
		MinCatalogRevision: &revision,
		Selection:          selection,
	}
	permissions, err := service.permissionMetadata.readEffective(
		ctx, bearer, permissionRequest,
	)
	if err != nil {
		return nil, err
	}
	if err := permissionsemantic.ValidateResponseForRequest(permissionRequest, permissions); err != nil ||
		validateSearchMetadataAuthority(
			ctx, service.reconcileAppMetadataV3, view, permissions.Receipts,
		) != nil {
		return nil, unavailableSearch()
	}
	_, readable := objectPermissionForDescribe(permissions, spec.APIName)
	if !readable {
		return nil, &permissionsemantic.Failure{Category: permissionsemantic.FailureForbidden}
	}
	return map[string]any{"data": projectQuickActions(spec.APIName, actions, specs, permissions)}, nil
}

func (service *Service) serveObjectQuickActions(writer http.ResponseWriter, request *http.Request) bool {
	route, matched := matchObjectQuickActionsRoute(request.URL.EscapedPath())
	if !matched || service.appMetadataV3 == nil || service.permissionMetadata == nil {
		return false
	}
	if request.Method != http.MethodGet {
		writer.Header().Set("Allow", http.MethodGet)
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	response, err := service.readObjectQuickActions(ctx, bearer, route)
	if err != nil {
		service.failures.Add(1)
		writeSearchFailure(writer, err)
		return true
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, response)
	return true
}
