package dialectsfcrm

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strings"
	"time"

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

var legacyStandardActionFieldAPINamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*(__c|__r)?$`)

const legacyChangeOwnerInternalAccessActionID = "legacy-standard-action-change-owner"

type legacyStandardActionDeps struct {
	records       contract.Records
	coordinator   contract.RecordMutationCoordinator
	metadata      contract.Metadata
	policy        contract.CapabilityPolicy
	notifications contract.Notifications
}

type legacyCreateRecordInput struct {
	ActionID string `json:"actionId"`
	RecordID string `json:"recordId"`
}

type legacyCreateRecordConfig struct {
	LookupField string                      `json:"lookupField"`
	Workflow    *legacyCreateRecordWorkflow `json:"workflow"`
}

type legacyCreateRecordWorkflow struct {
	FieldMappings map[string]any `json:"fieldMappings"`
	DefaultValues map[string]any `json:"defaultValues"`
}

// ChangeOwnerActionInput is the neutral input accepted by CRM ChangeOwner handlers.
type ChangeOwnerActionInput struct {
	RecordID         string `json:"recordId"`
	ObjectAPIName    string `json:"objectApiName"`
	NewOwnerID       string `json:"newOwnerId"`
	SendNotification bool   `json:"sendNotification"`
}

type legacyChangeOwnerInput = ChangeOwnerActionInput

// ChangeOwnerActionDeps contains the neutral runtime contracts required to execute ChangeOwner.
type ChangeOwnerActionDeps struct {
	Records                   contract.Records
	RecordMutationCoordinator contract.RecordMutationCoordinator
	Metadata                  contract.Metadata
	Policy                    contract.CapabilityPolicy
	Notifications             contract.Notifications
}

func dispatchLegacyStandardAction(ctx context.Context, req *dialect.Request, deps dialect.Deps, name string, inputs []map[string]any) *dialect.Response {
	legacy, resp := standardActionLegacyDeps(deps, fmt.Sprintf("execute standard action %q", name))
	if resp != nil {
		return resp
	}

	var (
		results []contract.StandardActionInvocationResult
		outcome *contract.StandardActionOutcome
	)
	switch name {
	case "changeOwner":
		results, outcome = legacy.invokeChangeOwner(ctx, inputs, req.User)
	case "createRecord":
		results, outcome = legacy.invokeCreateRecord(ctx, inputs, req.User)
	default:
		return standardActionUnsupportedResponse(name)
	}
	if outcome != nil {
		return standardActionOutcomeResponse(fmt.Sprintf("execute standard action %q", name), name, outcome)
	}
	return jsonResponse(http.StatusOK, standardActionInvocationResultsToWire(results))
}

func standardActionLegacyDeps(deps dialect.Deps, operation string) (*legacyStandardActionDeps, *dialect.Response) {
	if deps.Contracts.Records == nil {
		return nil, standardActionInternalErrorResponse(operation, fmt.Errorf("records contract is not configured"))
	}
	if deps.Contracts.Metadata == nil {
		return nil, standardActionInternalErrorResponse(operation, fmt.Errorf("metadata contract is not configured"))
	}
	if deps.Contracts.RecordMutationCoordinator == nil {
		return nil, standardActionInternalErrorResponse(operation, fmt.Errorf("record mutation coordinator contract is not configured"))
	}
	policy, _ := deps.Contracts.Policy.(contract.CapabilityPolicy)
	return &legacyStandardActionDeps{
		records:       deps.Contracts.Records,
		coordinator:   deps.Contracts.RecordMutationCoordinator,
		metadata:      deps.Contracts.Metadata,
		policy:        policy,
		notifications: deps.Contracts.Notifications,
	}, nil
}

// ExecuteChangeOwnerAction applies Salesforce-compatible ChangeOwner semantics for Flow handlers.
func ExecuteChangeOwnerAction(ctx context.Context, deps ChangeOwnerActionDeps, input ChangeOwnerActionInput, user *models.UserSession) (*contract.StandardActionInvocationResult, error) {
	if deps.Records == nil {
		return nil, fmt.Errorf("records contract is not configured")
	}
	if deps.RecordMutationCoordinator == nil {
		return nil, fmt.Errorf("record mutation coordinator contract is not configured")
	}
	legacy := &legacyStandardActionDeps{
		records:       deps.Records,
		coordinator:   deps.RecordMutationCoordinator,
		metadata:      deps.Metadata,
		policy:        deps.Policy,
		notifications: deps.Notifications,
	}
	return legacy.changeSingleOwner(ctx, input, user)
}

func (d *legacyStandardActionDeps) invokeCreateRecord(ctx context.Context, inputs []map[string]any, user *models.UserSession) ([]contract.StandardActionInvocationResult, *contract.StandardActionOutcome) {
	results := make([]contract.StandardActionInvocationResult, 0, len(inputs))
	for _, raw := range inputs {
		input, outcome := decodeLegacyStandardActionInput[legacyCreateRecordInput]("createRecord", raw)
		if outcome != nil {
			return nil, outcome
		}
		record, err := d.createSingleRecord(ctx, input, user)
		if err != nil {
			results = append(results, contract.StandardActionInvocationResult{
				IsSuccess: false,
				OutputValues: map[string]any{
					"recordId": input.RecordID,
					"actionId": input.ActionID,
				},
				Errors: []contract.StandardActionError{{
					Code:    "UNKNOWN_EXCEPTION",
					Message: err.Error(),
				}},
			})
			continue
		}
		results = append(results, contract.StandardActionInvocationResult{
			IsSuccess:    true,
			OutputValues: map[string]any{"record": record},
		})
	}
	return results, nil
}

func (d *legacyStandardActionDeps) createSingleRecord(ctx context.Context, input legacyCreateRecordInput, user *models.UserSession) (contract.Record, error) {
	if input.ActionID == "" {
		return nil, fmt.Errorf("actionId is required")
	}
	if input.RecordID == "" {
		return nil, fmt.Errorf("recordId is required")
	}

	action, err := d.metadata.ActionByID(ctx, input.ActionID)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve action %s: %w", input.ActionID, err)
	}
	if action == nil {
		return nil, fmt.Errorf("action %s not found", input.ActionID)
	}
	if action.Type != constants.ActionTypeCreateRecord {
		return nil, fmt.Errorf("action %s is not a CreateRecord action", input.ActionID)
	}
	if action.TargetObject == nil || *action.TargetObject == "" {
		return nil, fmt.Errorf("action %s is missing targetObject", input.ActionID)
	}

	config, err := decodeLegacyCreateRecordConfig(action.Config)
	if err != nil {
		return nil, fmt.Errorf("invalid create action config: %w", err)
	}

	targetObject := *action.TargetObject
	if d.coordinator.RequiresSourceAwareTransaction(targetObject) {
		return d.createSingleRecordInContext(ctx, action, config, input.RecordID, targetObject, user)
	}

	var created contract.Record
	err = d.coordinator.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		var txErr error
		created, txErr = d.createSingleRecordInContext(txCtx, action, config, input.RecordID, targetObject, user)
		return txErr
	})
	if err != nil {
		return nil, err
	}
	return created, nil
}

func (d *legacyStandardActionDeps) createSingleRecordInContext(ctx context.Context, action *models.ActionMetadata, config *legacyCreateRecordConfig, sourceRecordID string, targetObject string, user *models.UserSession) (contract.Record, error) {
	sourceRecord, err := d.records.Retrieve(ctx, action.ObjectAPIName, sourceRecordID, user)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve source %s: %w", action.ObjectAPIName, err)
	}
	recordData := d.buildCreateRecordData(ctx, sourceRecordID, config, sourceRecord, user)
	inserted, err := d.records.Insert(ctx, targetObject, recordData, user)
	if err != nil {
		return nil, fmt.Errorf("failed to create %s: %w", targetObject, err)
	}
	return inserted, nil
}

func (d *legacyStandardActionDeps) buildCreateRecordData(ctx context.Context, sourceRecordID string, config *legacyCreateRecordConfig, sourceRecord contract.Record, user *models.UserSession) contract.Record {
	recordData := make(contract.Record)
	applyLegacyDefaultValues(recordData, config.Workflow)
	resolver := d.createRelatedFieldResolver(ctx, sourceRecord, user)
	applyLegacyFieldMappings(recordData, config.Workflow, sourceRecord, resolver)
	if config.LookupField != "" {
		recordData[config.LookupField] = sourceRecordID
	}
	return recordData
}

func (d *legacyStandardActionDeps) invokeChangeOwner(ctx context.Context, inputs []map[string]any, user *models.UserSession) ([]contract.StandardActionInvocationResult, *contract.StandardActionOutcome) {
	results := make([]contract.StandardActionInvocationResult, 0, len(inputs))
	for _, raw := range inputs {
		input, outcome := decodeLegacyStandardActionInput[legacyChangeOwnerInput]("changeOwner", raw)
		if outcome != nil {
			return nil, outcome
		}
		result, err := d.changeSingleOwner(ctx, input, user)
		if err != nil {
			results = append(results, contract.StandardActionInvocationResult{
				IsSuccess:    false,
				OutputValues: map[string]any{"recordId": input.RecordID},
				Errors: []contract.StandardActionError{{
					Code:    legacyStandardActionErrorCode(err),
					Message: err.Error(),
				}},
			})
			continue
		}
		results = append(results, *result)
	}
	return results, nil
}

func (d *legacyStandardActionDeps) changeSingleOwner(ctx context.Context, input legacyChangeOwnerInput, user *models.UserSession) (*contract.StandardActionInvocationResult, error) {
	if input.RecordID == "" {
		return nil, fmt.Errorf("recordId is required")
	}
	if input.ObjectAPIName == "" {
		return nil, fmt.Errorf("objectApiName is required")
	}
	if input.NewOwnerID == "" {
		return nil, fmt.Errorf("newOwnerId is required")
	}

	var (
		record          contract.Record
		newOwner        contract.Record
		previousOwnerID string
	)
	txErr := d.coordinator.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		var err error
		updateCtx := txCtx
		record, err = d.records.Retrieve(txCtx, input.ObjectAPIName, input.RecordID, user)
		if err != nil {
			return fmt.Errorf("failed to retrieve %s: %w", input.ObjectAPIName, err)
		}
		previousOwnerID, _ = record["OwnerId"].(string)
		if user == nil || previousOwnerID != user.ID {
			if !d.callerHasAnyCapability(txCtx, user, legacyChangeOwnerTransferPermissionCandidates(input.ObjectAPIName)) {
				return fmt.Errorf("permission denied: you do not have permission to transfer records you do not own")
			}
			updateCtx = legacyChangeOwnerScopedEditContext(txCtx, input.ObjectAPIName)
		}

		newOwner, err = d.records.Retrieve(txCtx, constants.TableUser, input.NewOwnerID, user)
		if err != nil || len(newOwner) == 0 {
			return fmt.Errorf("newOwnerId does not refer to an accessible User record")
		}
		if newOwnerIDField, _ := newOwner["Id"].(string); newOwnerIDField == "" {
			return fmt.Errorf("newOwnerId does not refer to an accessible User record")
		}
		if !legacyStandardActionTruthy(newOwner[constants.FieldIsActive]) {
			return fmt.Errorf("newOwnerId refers to an inactive user")
		}
		return d.records.Update(updateCtx, input.ObjectAPIName, input.RecordID, contract.Record{
			"OwnerId": input.NewOwnerID,
		}, user)
	})
	if txErr != nil {
		return nil, fmt.Errorf("failed to update owner: %w", txErr)
	}
	if input.SendNotification && d.notifications != nil {
		go d.dispatchChangeOwnerEmail(cloneLegacyRecord(newOwner), cloneLegacyRecord(record), input.ObjectAPIName, user)
	}
	return &contract.StandardActionInvocationResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"recordId":        input.RecordID,
			"previousOwnerId": previousOwnerID,
			"newOwnerId":      input.NewOwnerID,
		},
	}, nil
}

func legacyChangeOwnerTransferPermissionCandidates(objectAPIName string) []contract.Capability {
	perms := []contract.Capability{constants.PermTransferAnyEntity}
	objectAPIName = strings.TrimSpace(objectAPIName)
	if objectAPIName == "" {
		return perms
	}
	return append(perms, contract.Capability("TransferAny"+objectAPIName))
}

func legacyChangeOwnerScopedEditContext(ctx context.Context, objectAPIName string) context.Context {
	return constants.WithStandardActionInternalAccessScopes(ctx, legacyChangeOwnerInternalAccessActionID, []constants.StandardActionInternalAccessScope{{
		StandardActionID: legacyChangeOwnerInternalAccessActionID,
		ObjectAPIName:    objectAPIName,
		Operation:        constants.PermEdit,
	}})
}

func (d *legacyStandardActionDeps) callerHasAnyCapability(ctx context.Context, user *models.UserSession, capabilities []contract.Capability) bool {
	if d == nil || d.policy == nil {
		return false
	}
	for _, capability := range capabilities {
		if strings.TrimSpace(string(capability)) == "" {
			continue
		}
		if d.policy.HasCapability(ctx, user, capability) {
			return true
		}
	}
	return false
}

func decodeLegacyStandardActionInput[T any](actionName string, raw map[string]any) (T, *contract.StandardActionOutcome) {
	var out T
	encoded, err := json.Marshal(raw)
	if err != nil {
		return out, &contract.StandardActionOutcome{Kind: contract.StandardActionOutcomeInvalidRequest, Code: wire.SFCodeInvalidRequest, Name: actionName, Message: err.Error()}
	}
	if err := json.Unmarshal(encoded, &out); err != nil {
		return out, &contract.StandardActionOutcome{Kind: contract.StandardActionOutcomeInvalidRequest, Code: wire.SFCodeInvalidRequest, Name: actionName, Message: err.Error()}
	}
	return out, nil
}

func decodeLegacyCreateRecordConfig(raw map[string]interface{}) (*legacyCreateRecordConfig, error) {
	if len(raw) == 0 {
		return &legacyCreateRecordConfig{}, nil
	}
	encoded, err := json.Marshal(raw)
	if err != nil {
		return nil, err
	}
	var config legacyCreateRecordConfig
	if err := json.Unmarshal(encoded, &config); err != nil {
		return nil, err
	}
	return &config, nil
}

const legacyTodaySentinel = "__TODAY__"

func applyLegacyDefaultValues(target contract.Record, workflow *legacyCreateRecordWorkflow) {
	if workflow == nil {
		return
	}
	for fieldName, value := range workflow.DefaultValues {
		if str, ok := value.(string); ok && str == legacyTodaySentinel {
			target[fieldName] = time.Now().UTC().Format("2006-01-02")
			continue
		}
		target[fieldName] = value
	}
}

type legacyMappedSourceResolver func(mapping string) (interface{}, bool)

func applyLegacyFieldMappings(target contract.Record, workflow *legacyCreateRecordWorkflow, source contract.Record, resolver legacyMappedSourceResolver) {
	if workflow == nil {
		return
	}
	for targetField, rawMapping := range workflow.FieldMappings {
		applyLegacyFieldMapping(target, targetField, rawMapping, source, resolver)
	}
}

func applyLegacyFieldMapping(target contract.Record, targetField string, rawMapping interface{}, source contract.Record, resolver legacyMappedSourceResolver) {
	switch mapping := rawMapping.(type) {
	case string:
		if value, ok := resolveLegacyMappedSourceValue(mapping, source, resolver); ok && value != nil {
			target[targetField] = value
		}
	case map[string]interface{}:
		sourceField, _ := mapping["sourceField"].(string)
		defaultValue, hasDefault := mapping["defaultValue"]
		if sourceField != "" {
			if value, ok := resolveLegacyMappedSourceValue(sourceField, source, resolver); ok && value != nil {
				target[targetField] = value
				return
			}
		}
		if hasDefault {
			target[targetField] = defaultValue
		}
	}
}

func resolveLegacyMappedSourceValue(mapping string, source contract.Record, resolver legacyMappedSourceResolver) (interface{}, bool) {
	if value, ok := source[mapping]; ok && value != nil {
		return value, true
	}
	if resolver == nil || !strings.Contains(mapping, ".") {
		return nil, false
	}
	return resolver(mapping)
}

func (d *legacyStandardActionDeps) createRelatedFieldResolver(ctx context.Context, source contract.Record, user *models.UserSession) legacyMappedSourceResolver {
	relatedCache := make(map[string]contract.Record)
	return func(mapping string) (interface{}, bool) {
		objectAPIName, fieldAPIName, ok := parseLegacyOneHopFieldPath(mapping)
		if !ok {
			return nil, false
		}
		lookupField := objectAPIName + "Id"
		rawRelatedID, ok := source[lookupField]
		if !ok || rawRelatedID == nil {
			return nil, false
		}
		relatedID, ok := rawRelatedID.(string)
		if !ok {
			return nil, false
		}
		relatedID = strings.TrimSpace(relatedID)
		if relatedID == "" {
			return nil, false
		}

		cacheKey := strings.ToLower(objectAPIName) + ":" + relatedID
		related, ok := relatedCache[cacheKey]
		if !ok {
			record, err := d.records.Retrieve(ctx, objectAPIName, relatedID, user)
			if err != nil {
				return nil, false
			}
			related = record
			relatedCache[cacheKey] = related
		}
		value, ok := related[fieldAPIName]
		return value, ok && value != nil
	}
}

func parseLegacyOneHopFieldPath(mapping string) (string, string, bool) {
	parts := strings.Split(mapping, ".")
	if len(parts) != 2 {
		return "", "", false
	}
	objectAPIName := strings.TrimSpace(parts[0])
	fieldAPIName := strings.TrimSpace(parts[1])
	if objectAPIName == "" || fieldAPIName == "" {
		return "", "", false
	}
	if !legacyStandardActionFieldAPINamePattern.MatchString(objectAPIName) || !legacyStandardActionFieldAPINamePattern.MatchString(fieldAPIName) {
		return "", "", false
	}
	return objectAPIName, fieldAPIName, true
}

func (d *legacyStandardActionDeps) dispatchChangeOwnerEmail(newOwner contract.Record, record contract.Record, objectAPIName string, actingUser *models.UserSession) {
	defer func() {
		if r := recover(); r != nil {
			log.Printf("dispatchChangeOwnerEmail panic: %v", r)
		}
	}()
	if d.notifications == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	toEmail, _ := newOwner["Email"].(string)
	if toEmail == "" {
		return
	}
	newOwnerName, _ := newOwner["Name"].(string)
	objectLabel := objectAPIName
	var schema *models.ObjectMetadata
	if d.metadata != nil {
		if resolved, err := d.metadata.Describe(ctx, objectAPIName); err == nil && resolved != nil {
			schema = resolved
			if resolved.Label != "" {
				objectLabel = resolved.Label
			}
		}
	}
	recordName := legacyRecordDisplayName(record, schema)
	recordID, _ := record["Id"].(string)
	actingName := ""
	if actingUser != nil {
		actingName = actingUser.Name
	}

	link := d.notifications.FrontendBaseURL(ctx)
	if link != "" && recordID != "" {
		link = strings.TrimRight(link, "/") + "/object/" + strings.ToLower(objectAPIName) + "/" + recordID
	}

	subject := fmt.Sprintf("You are the new owner of %s: %s", objectLabel, recordName)
	var body strings.Builder
	if newOwnerName != "" {
		body.WriteString("Hi " + newOwnerName + ",\n\n")
	}
	body.WriteString(fmt.Sprintf("%s has assigned the %s \"%s\" to you.\n", legacyOrFallback(actingName, "A colleague"), objectLabel, recordName))
	if link != "" {
		body.WriteString("\nOpen it here: " + link + "\n")
	}
	body.WriteString("\n\u2014 NexusCRM\n")

	if _, err := d.notifications.SendEmail(ctx, &contract.OutboundEmailMessage{
		To:      []string{toEmail},
		Subject: subject,
		Body:    body.String(),
	}); err != nil {
		log.Printf("change-owner notification to %s failed: %v", toEmail, err)
	}
}

func legacyRecordDisplayName(record contract.Record, schema *models.ObjectMetadata) string {
	if schema != nil {
		for _, field := range schema.Fields {
			if !field.IsNameField {
				continue
			}
			if name, ok := record[field.APIName].(string); ok && name != "" {
				return name
			}
			break
		}
	}
	if name, ok := record["Name"].(string); ok && name != "" {
		return name
	}
	if id, ok := record["Id"].(string); ok {
		return id
	}
	return "(unknown)"
}

func legacyOrFallback(primary, fallback string) string {
	if primary == "" {
		return fallback
	}
	return primary
}

func legacyStandardActionTruthy(v interface{}) bool {
	switch x := v.(type) {
	case bool:
		return x
	case int64:
		return x != 0
	case int:
		return x != 0
	case float64:
		return x != 0
	case []byte:
		return legacyStandardActionTruthy(string(x))
	case string:
		return x == "1" || strings.EqualFold(x, "true")
	default:
		return false
	}
}

func cloneLegacyRecord(src contract.Record) contract.Record {
	if src == nil {
		return nil
	}
	dst := make(contract.Record, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst
}

func legacyStandardActionErrorCode(err error) string {
	msg := strings.ToLower(err.Error())
	switch {
	case strings.Contains(msg, "permission"), strings.Contains(msg, "forbidden"):
		return "INSUFFICIENT_ACCESS"
	case strings.Contains(msg, "not found"), strings.Contains(msg, "accessible"):
		return "NOT_FOUND"
	case strings.Contains(msg, "inactive"), strings.Contains(msg, "required"), strings.Contains(msg, "invalid"):
		return "FIELD_INTEGRITY_EXCEPTION"
	default:
		return "UNKNOWN_EXCEPTION"
	}
}
