package dialectsfcrm

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

func flowToolingDefinitionPatchIntent(body []byte) (contract.FlowAdminDefinitionPatchIntent, *dialect.Response, error) {
	fields, resp, err := decodeFlowToolingPatchObject(body)
	if resp != nil || err != nil {
		return contract.FlowAdminDefinitionPatchIntent{}, resp, err
	}
	return resolveFlowToolingDefinitionPatch(fields)
}

func flowToolingVersionPatchIntent(body []byte) (contract.FlowAdminVersionPatchIntent, *dialect.Response, error) {
	fields, resp, err := decodeFlowToolingPatchObject(body)
	if resp != nil || err != nil {
		return contract.FlowAdminVersionPatchIntent{}, resp, err
	}
	return resolveFlowToolingVersionPatch(fields)
}

func decodeFlowToolingPatchObject(body []byte) (map[string]json.RawMessage, *dialect.Response, error) {
	var raw json.RawMessage
	if err := decodeFlowToolingJSON(body, &raw); err != nil {
		return nil, flowToolingWireError(http.StatusBadRequest, wire.SFCodeJSONParserError, "Invalid JSON"), nil
	}
	fields, err := decodeFlowToolingPatchObjectRaw(raw)
	return fields, nil, err
}

func decodeFlowToolingPatchObjectRaw(raw json.RawMessage) (map[string]json.RawMessage, error) {
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return nil, sharedErrors.NewInvalidFieldError("Sparse PATCH body must be a JSON object")
	}
	if fields == nil {
		return nil, sharedErrors.NewInvalidFieldError("Sparse PATCH body must be a JSON object")
	}
	return fields, nil
}

func resolveFlowToolingDefinitionPatch(fields map[string]json.RawMessage) (contract.FlowAdminDefinitionPatchIntent, *dialect.Response, error) {
	if metadataRaw, ok := fields["Metadata"]; ok {
		if len(fields) != 1 {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata", "Metadata envelope PATCH cannot be mixed with top-level FlowDefinition fields")
		}
		if isFlowToolingJSONNull(metadataRaw) {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata", "Metadata cannot be null")
		}
		metadataFields, err := decodeFlowToolingPatchObjectRaw(metadataRaw)
		if err != nil {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, err
		}
		return resolveFlowToolingDefinitionMetadataPatch(metadataFields)
	}

	var denied []string
	var invalid []string
	intent := contract.FlowAdminDefinitionPatchIntent{Action: contract.FlowAdminDefinitionPatchActionUpdate}

	for name, raw := range fields {
		switch name {
		case "DeveloperName":
			value, err := decodeFlowToolingRequiredStringField(raw, name)
			if err != nil {
				return contract.FlowAdminDefinitionPatchIntent{}, nil, err
			}
			intent.Name = &value
		case "MasterLabel":
			value, err := decodeFlowToolingRequiredStringField(raw, name)
			if err != nil {
				return contract.FlowAdminDefinitionPatchIntent{}, nil, err
			}
			intent.Label = &value
		case "Description":
			value, err := decodeFlowToolingNullableStringField(raw, name)
			if err != nil {
				return contract.FlowAdminDefinitionPatchIntent{}, nil, err
			}
			intent.Description = &value
		case "ProcessType", "TriggerType", "TriggerObjectOrEvent", "ActiveVersionId", "LatestVersionId":
			denied = append(denied, name)
		default:
			invalid = append(invalid, name)
		}
	}

	if len(denied) > 0 {
		return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingFieldDeniedError(denied)
	}
	if len(invalid) > 0 {
		sort.Strings(invalid)
		return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError(invalid[0], "Unsupported FlowDefinition field in sparse PATCH")
	}
	if intent.Name == nil && intent.Label == nil && intent.Description == nil {
		return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("FlowDefinition", "No supported FlowDefinition fields to update")
	}

	return intent, nil, nil
}

func resolveFlowToolingDefinitionMetadataPatch(fields map[string]json.RawMessage) (contract.FlowAdminDefinitionPatchIntent, *dialect.Response, error) {
	if activeVersionRaw, ok := fields["activeVersionNumber"]; ok {
		if len(fields) != 1 {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata.activeVersionNumber", "activeVersionNumber PATCH cannot be mixed with other Metadata fields")
		}
		if isFlowToolingJSONNull(activeVersionRaw) {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata.activeVersionNumber", "activeVersionNumber cannot be null")
		}
		var value int
		if err := json.Unmarshal(activeVersionRaw, &value); err != nil {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata.activeVersionNumber", "activeVersionNumber must be an integer")
		}
		if value < 0 {
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata.activeVersionNumber", "activeVersionNumber cannot be negative")
		}
		action := contract.FlowAdminDefinitionPatchActionActivateVersion
		if value == 0 {
			action = contract.FlowAdminDefinitionPatchActionDeactivate
		}
		return contract.FlowAdminDefinitionPatchIntent{
			Action:              action,
			ActiveVersionNumber: value,
		}, nil, nil
	}

	intent := contract.FlowAdminDefinitionPatchIntent{Action: contract.FlowAdminDefinitionPatchActionUpdate}
	for name, raw := range fields {
		switch name {
		case "masterLabel":
			value, err := decodeFlowToolingRequiredStringField(raw, "Metadata.masterLabel")
			if err != nil {
				return contract.FlowAdminDefinitionPatchIntent{}, nil, err
			}
			intent.Label = &value
		case "description":
			value, err := decodeFlowToolingNullableStringField(raw, "Metadata.description")
			if err != nil {
				return contract.FlowAdminDefinitionPatchIntent{}, nil, err
			}
			intent.Description = &value
		default:
			return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata."+name, "Unsupported FlowDefinition Metadata field")
		}
	}
	if intent.Name == nil && intent.Label == nil && intent.Description == nil {
		return contract.FlowAdminDefinitionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata", "Unsupported FlowDefinition Metadata patch")
	}
	return intent, nil, nil
}

func resolveFlowToolingVersionPatch(fields map[string]json.RawMessage) (contract.FlowAdminVersionPatchIntent, *dialect.Response, error) {
	if statusRaw, ok := fields["Status"]; ok {
		if len(fields) != 1 {
			return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Status", "Status PATCH cannot be mixed with other Flow fields")
		}
		if isFlowToolingJSONNull(statusRaw) {
			return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Status", "Status cannot be null")
		}
		var status string
		if err := json.Unmarshal(statusRaw, &status); err != nil {
			return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Status", "Status must be a string")
		}
		switch status {
		case models.FlowStatusActive:
			return contract.FlowAdminVersionPatchIntent{Action: contract.FlowAdminVersionPatchActionActivate}, nil, nil
		case models.FlowStatusDraft, "Obsolete":
			return contract.FlowAdminVersionPatchIntent{Action: contract.FlowAdminVersionPatchActionDeactivate}, nil, nil
		default:
			return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Status", "Unsupported Flow Status transition")
		}
	}

	var denied []string
	var invalid []string
	intent := contract.FlowAdminVersionPatchIntent{Action: contract.FlowAdminVersionPatchActionUpdate}

	for name, raw := range fields {
		switch name {
		case "Description":
			value, err := decodeFlowToolingNullableStringField(raw, name)
			if err != nil {
				return contract.FlowAdminVersionPatchIntent{}, nil, err
			}
			intent.Description = value
			intent.DescriptionProvided = true
		case "Metadata":
			if isFlowToolingJSONNull(raw) {
				return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata", "Metadata cannot be null")
			}
			if _, err := decodeFlowToolingPatchObjectRaw(raw); err != nil {
				return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Metadata", "Metadata must be a JSON object")
			}
			if err := rejectNullFlowToolingMetadataRaw("Metadata", raw); err != nil {
				return contract.FlowAdminVersionPatchIntent{}, nil, err
			}
			intent.MetadataPatchRaw = append([]byte(nil), raw...)
		case "DefinitionId", "ProcessType":
			denied = append(denied, name)
		default:
			invalid = append(invalid, name)
		}
	}

	if len(denied) > 0 {
		return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingFieldDeniedError(denied)
	}
	if len(invalid) > 0 {
		sort.Strings(invalid)
		return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError(invalid[0], "Unsupported Flow field in sparse PATCH")
	}
	if !intent.DescriptionProvided && len(intent.MetadataPatchRaw) == 0 {
		return contract.FlowAdminVersionPatchIntent{}, nil, newFlowToolingInvalidFieldError("Flow", "No supported Flow fields to update")
	}
	return intent, nil, nil
}

func decodeFlowToolingRequiredStringField(raw json.RawMessage, field string) (string, error) {
	if isFlowToolingJSONNull(raw) {
		return "", newFlowToolingInvalidFieldError(field, field+" cannot be null")
	}
	var value string
	if err := json.Unmarshal(raw, &value); err != nil {
		return "", newFlowToolingInvalidFieldError(field, field+" must be a string")
	}
	return value, nil
}

func decodeFlowToolingNullableStringField(raw json.RawMessage, field string) (string, error) {
	if isFlowToolingJSONNull(raw) {
		return "", nil
	}
	var value string
	if err := json.Unmarshal(raw, &value); err != nil {
		return "", newFlowToolingInvalidFieldError(field, field+" must be a string")
	}
	return value, nil
}

func rejectNullFlowToolingMetadataRaw(path string, raw json.RawMessage) error {
	trimmed := bytes.TrimSpace(raw)
	if bytes.Equal(trimmed, []byte("null")) {
		return newFlowToolingInvalidFieldError(path, path+" cannot be null")
	}
	if len(trimmed) == 0 {
		return nil
	}
	switch trimmed[0] {
	case '{':
		var fields map[string]json.RawMessage
		if err := json.Unmarshal(trimmed, &fields); err != nil {
			return nil
		}
		for key, nested := range fields {
			if err := rejectNullFlowToolingMetadataRaw(path+"."+key, nested); err != nil {
				return err
			}
		}
	case '[':
		var items []json.RawMessage
		if err := json.Unmarshal(trimmed, &items); err != nil {
			return nil
		}
		for idx, nested := range items {
			if err := rejectNullFlowToolingMetadataRaw(fmt.Sprintf("%s[%d]", path, idx), nested); err != nil {
				return err
			}
		}
	}
	return nil
}

func isFlowToolingJSONNull(raw json.RawMessage) bool {
	return bytes.Equal(bytes.TrimSpace(raw), []byte("null"))
}

func newFlowToolingInvalidFieldError(field string, message string) error {
	return sharedErrors.NewInvalidFieldError(message, field)
}

func newFlowToolingFieldDeniedError(fields []string) error {
	fields = append([]string(nil), fields...)
	sort.Strings(fields)
	return &contract.FlowAuthoringError{
		Kind:    contract.FlowAuthoringErrorFieldDenied,
		Message: fmt.Sprintf("Unable to create/update fields: %s. Please check the security settings of this field and verify that it is read/write for your profile or permission set.", strings.Join(fields, ", ")),
		Fields:  fields,
	}
}
