package rest

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

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/features/records"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// fieldAPINamePattern matches a well-formed Salesforce-style field API name:
// standard (e.g. AccountId), custom (Foo__c), or relationship (Foo__r).
// Used to validate operator-supplied field names before embedding them in
// filter expressions — rejects anything with whitespace, quotes, or operators.
var fieldAPINamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*(__c|__r)?$`)

func isValidFieldAPIName(s string) bool {
	return fieldAPINamePattern.MatchString(s)
}

// StandardActionsHandler is the legacy code-native implementation path for
// standard actions that have not yet migrated to the registry-driven action
// dispatcher. Remaining cases: changeOwner and createRecord. convertLead is
// dispatched through StandardActionDispatcher.
type StandardActionsHandler struct {
	persistence *records.PersistenceService
	permissions *permissions.PermissionService
	metadata    *metadata.MetadataService
	querySvc    ports.QueryService
	emailSender ports.EmailSender
	frontendURL string
}

// NewStandardActionsHandler creates a new handler. permissions, metadata, and
// emailSender may be nil (the ChangeOwner path will soft-degrade: permission
// check defaults to "deny", object label falls back to apiName, and email
// delivery is skipped).
func NewStandardActionsHandler(
	persistence *records.PersistenceService,
	perms *permissions.PermissionService,
	meta *metadata.MetadataService,
	querySvc ports.QueryService,
	emailSender ports.EmailSender,
	frontendURL string,
) *StandardActionsHandler {
	return &StandardActionsHandler{
		persistence: persistence,
		permissions: perms,
		metadata:    meta,
		querySvc:    querySvc,
		emailSender: emailSender,
		frontendURL: frontendURL,
	}
}

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

type StandardCreateRecordResponse struct {
	IsSuccess    bool                   `json:"isSuccess"`
	OutputValues map[string]interface{} `json:"outputValues"`
	Errors       []any                  `json:"errors"`
}

type createRecordActionConfig struct {
	LookupField    string                      `json:"lookupField"`
	ConfirmTitle   string                      `json:"confirmTitle"`
	ConfirmMessage string                      `json:"confirmMessage"`
	Workflow       *createRecordWorkflowConfig `json:"workflow"`
}

// createRecordWorkflowConfig models the Config.workflow subtree consumed
// by the CreateRecord action flow. Child cloning is NOT represented here
// — that authority moved to _System_Object.ChildCloneRules and is
// executed by PersistenceService.Insert via applyChildCloneRules
// (features/records/child_clone.go). Any workflow.childCopy keys present
// in legacy Config JSON are inert: the runtime simply doesn't read them.
// See data-specs/2026-04-24-clear-residual-workflow-childcopy.md for
// optional hygiene cleanup of those inert keys.
type createRecordWorkflowConfig struct {
	FieldMappings map[string]interface{} `json:"fieldMappings"`
	DefaultValues map[string]interface{} `json:"defaultValues"`
}

// CreateRecord handles POST /actions/standard/createRecord.
func (h *StandardActionsHandler) CreateRecord(c *gin.Context) {
	user := MustGetUser(c)

	var body struct {
		Inputs []StandardCreateRecordRequest `json:"inputs"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || len(body.Inputs) == 0 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "inputs array is required"))
		return
	}

	results := make([]StandardCreateRecordResponse, 0, len(body.Inputs))
	for _, input := range body.Inputs {
		record, err := h.createSingleRecord(c.Request.Context(), input, user)
		if err != nil {
			results = append(results, StandardCreateRecordResponse{
				IsSuccess:    false,
				OutputValues: map[string]interface{}{"recordId": input.RecordID, "actionId": input.ActionID},
				Errors:       []any{map[string]string{"statusCode": "UNKNOWN_EXCEPTION", "message": err.Error()}},
			})
			continue
		}

		results = append(results, StandardCreateRecordResponse{
			IsSuccess: true,
			OutputValues: map[string]interface{}{
				"record": record,
			},
			Errors: []any{},
		})
	}

	c.JSON(http.StatusOK, results)
}

func (h *StandardActionsHandler) createSingleRecord(
	ctx context.Context,
	input StandardCreateRecordRequest,
	user *models.UserSession,
) (models.SObject, error) {
	if input.ActionID == "" {
		return nil, fmt.Errorf("actionId is required")
	}
	if input.RecordID == "" {
		return nil, fmt.Errorf("recordId is required")
	}

	action := h.metadata.GetActionByID(ctx, input.ActionID)
	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 := decodeCreateRecordActionConfig(action.Config)
	if err != nil {
		return nil, fmt.Errorf("invalid create action config: %w", err)
	}

	targetObject := *action.TargetObject
	var createdRecord models.SObject
	err = h.persistence.RunInTransaction(ctx, func(txCtx context.Context) error {
		sourceRecord, retrieveErr := h.persistence.Retrieve(txCtx, action.ObjectAPIName, input.RecordID, user)
		if retrieveErr != nil {
			return fmt.Errorf("failed to retrieve source %s: %w", action.ObjectAPIName, retrieveErr)
		}

		recordData := make(models.SObject)
		applyDefaultValues(recordData, config.Workflow)
		relatedResolver := h.createRelatedFieldResolver(txCtx, sourceRecord, user)
		applyConfiguredFieldMappingsWithResolver(recordData, config.Workflow, sourceRecord, relatedResolver)

		if config.LookupField != "" {
			recordData[config.LookupField] = input.RecordID
		}

		inserted, insertErr := h.persistence.Insert(txCtx, targetObject, recordData, user)
		if insertErr != nil {
			return fmt.Errorf("failed to create %s: %w", targetObject, insertErr)
		}
		createdRecord = inserted

		// Child cloning is handled by PersistenceService.Insert via
		// _System_Object.ChildCloneRules (features/records/child_clone.go).
		// The old action-level workflow.childCopy executor has been
		// retired — this handler no longer runs child-copy loops. Any
		// workflow.childCopy keys that persist in legacy _System_Action.Config
		// rows are inert; the hygiene spec
		// data-specs/2026-04-24-clear-residual-workflow-childcopy.md
		// removes them on request but is not required for correctness.

		return nil
	})
	if err != nil {
		return nil, err
	}

	return createdRecord, nil
}

func decodeCreateRecordActionConfig(raw map[string]interface{}) (*createRecordActionConfig, error) {
	if len(raw) == 0 {
		return &createRecordActionConfig{}, nil
	}

	encoded, err := json.Marshal(raw)
	if err != nil {
		return nil, err
	}

	var config createRecordActionConfig
	if err := json.Unmarshal(encoded, &config); err != nil {
		return nil, err
	}
	return &config, nil
}

// todaySentinel, when used as a string value in workflow.defaultValues, is
// substituted with today's date (UTC, YYYY-MM-DD) at action execution time.
// Lets action configs express "default to today" without a runtime hook.
const todaySentinel = "__TODAY__"

func applyDefaultValues(target models.SObject, workflow *createRecordWorkflowConfig) {
	if workflow == nil {
		return
	}
	for fieldName, value := range workflow.DefaultValues {
		if str, ok := value.(string); ok && str == todaySentinel {
			target[fieldName] = time.Now().UTC().Format("2006-01-02")
			continue
		}
		target[fieldName] = value
	}
}

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

func applyConfiguredFieldMappingsWithResolver(
	target models.SObject,
	workflow *createRecordWorkflowConfig,
	source models.SObject,
	resolver mappedSourceResolver,
) {
	if workflow == nil {
		return
	}
	for targetField, rawMapping := range workflow.FieldMappings {
		applyFieldMapping(target, targetField, rawMapping, source, resolver)
	}
}

func applyFieldMapping(
	target models.SObject,
	targetField string,
	rawMapping interface{},
	source models.SObject,
	resolver mappedSourceResolver,
) {
	switch mapping := rawMapping.(type) {
	case string:
		if value, ok := resolveMappedSourceValue(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 := resolveMappedSourceValue(sourceField, source, resolver); ok && value != nil {
				target[targetField] = value
				return
			}
		}
		if hasDefault {
			target[targetField] = defaultValue
		}
	}
}

func resolveMappedSourceValue(mapping string, source models.SObject, resolver mappedSourceResolver) (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 (h *StandardActionsHandler) createRelatedFieldResolver(
	ctx context.Context,
	source models.SObject,
	user *models.UserSession,
) mappedSourceResolver {
	relatedCache := make(map[string]models.SObject)
	return func(mapping string) (interface{}, bool) {
		objectAPIName, fieldAPIName, ok := parseOneHopFieldPath(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 {
			if h == nil || h.persistence == nil {
				return nil, false
			}
			record, err := h.persistence.Retrieve(ctx, objectAPIName, relatedID, user)
			if err != nil {
				return nil, false
			}
			related = record
			relatedCache[cacheKey] = related
		}

		value, ok := related[fieldAPIName]
		if !ok || value == nil {
			return nil, false
		}
		return value, true
	}
}

func parseOneHopFieldPath(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 !isValidFieldAPIName(objectAPIName) || !isValidFieldAPIName(fieldAPIName) {
		return "", "", false
	}
	return objectAPIName, fieldAPIName, true
}

// ---- ChangeOwner ---------------------------------------------------------

// ChangeOwnerInput is a single record owner-change request.
type ChangeOwnerInput struct {
	RecordID         string `json:"recordId"`
	ObjectAPIName    string `json:"objectApiName"`
	NewOwnerID       string `json:"newOwnerId"`
	SendNotification bool   `json:"sendNotification"`
}

// ChangeOwnerResponse matches Salesforce Invocable Action response format.
type ChangeOwnerResponse struct {
	IsSuccess    bool                   `json:"isSuccess"`
	OutputValues map[string]interface{} `json:"outputValues"`
	Errors       []any                  `json:"errors"`
}

// ChangeOwner handles POST /actions/standard/changeOwner.
// Reassigns the OwnerId field of one or more records and optionally sends a
// notification email to the new owner.
func (h *StandardActionsHandler) ChangeOwner(c *gin.Context) {
	user := MustGetUser(c)

	var body struct {
		Inputs []ChangeOwnerInput `json:"inputs"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || len(body.Inputs) == 0 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "inputs array is required"))
		return
	}

	results := make([]ChangeOwnerResponse, 0, len(body.Inputs))
	for _, input := range body.Inputs {
		result, err := h.changeSingleOwner(c.Request.Context(), input, user)
		if err != nil {
			results = append(results, ChangeOwnerResponse{
				IsSuccess: false,
				OutputValues: map[string]interface{}{
					"recordId": input.RecordID,
				},
				Errors: []any{map[string]string{
					"statusCode": sfErrorCode(err),
					"message":    err.Error(),
				}},
			})
			continue
		}
		results = append(results, *result)
	}

	c.JSON(http.StatusOK, results)
}

// changeSingleOwner executes one owner change inside a transaction. On
// success, it optionally dispatches a notification email asynchronously.
func (h *StandardActionsHandler) changeSingleOwner(
	ctx context.Context,
	input ChangeOwnerInput,
	user *models.UserSession,
) (*ChangeOwnerResponse, 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")
	}

	// 1. Fetch the target record (permission & FLS enforced by persistence).
	record, err := h.persistence.Retrieve(ctx, input.ObjectAPIName, input.RecordID, user)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve %s: %w", input.ObjectAPIName, err)
	}
	previousOwnerID, _ := record["OwnerId"].(string)

	// 2. Explicit TransferAnyEntity permission check.
	// Users may always transfer records they own; otherwise require the
	// system permission. System admins pass via CheckSystemPermission.
	if previousOwnerID != user.ID {
		if h.permissions == nil || !h.permissions.CheckSystemPermission(ctx, user, string(constants.PermTransferAnyEntity)) {
			return nil, fmt.Errorf("permission denied: you do not have permission to transfer records you do not own")
		}
	}

	// 3. Validate the new owner exists and is an active User. Queue/Group
	// targets are not yet supported in this release.
	newOwner, err := h.persistence.Retrieve(ctx, constants.TableUser, input.NewOwnerID, user)
	if err != nil || len(newOwner) == 0 {
		return nil, fmt.Errorf("newOwnerId does not refer to an accessible User record")
	}
	if newOwnerIDField, _ := newOwner["Id"].(string); newOwnerIDField == "" {
		return nil, fmt.Errorf("newOwnerId does not refer to an accessible User record")
	}
	if !isTruthy(newOwner[constants.FieldIsActive]) {
		return nil, fmt.Errorf("newOwnerId refers to an inactive user")
	}

	// 4. Update the record inside a transaction. PersistenceService.Update
	// runs queue assignment validation, approval-lock checks, FLS, immutable
	// field enforcement, and writes the audit log for the OwnerId change.
	txErr := h.persistence.RunInTransaction(ctx, func(txCtx context.Context) error {
		return h.persistence.Update(txCtx, input.ObjectAPIName, input.RecordID, models.SObject{
			"OwnerId": input.NewOwnerID,
		}, user)
	})
	if txErr != nil {
		return nil, fmt.Errorf("failed to update owner: %w", txErr)
	}

	// 5. Fire-and-forget notification email (best effort). The goroutine
	// survives the request lifetime, so we must not hand it map references
	// that callers can still mutate — shallow-copy everything it reads.
	if input.SendNotification && h.emailSender != nil {
		go h.dispatchChangeOwnerEmail(cloneSObject(newOwner), cloneSObject(record), input.ObjectAPIName, user)
	}

	return &ChangeOwnerResponse{
		IsSuccess: true,
		OutputValues: map[string]interface{}{
			"recordId":        input.RecordID,
			"previousOwnerId": previousOwnerID,
			"newOwnerId":      input.NewOwnerID,
		},
		Errors: []any{},
	}, nil
}

// dispatchChangeOwnerEmail sends the notification email. It runs in a
// background goroutine with its own timeout context so SMTP latency never
// blocks the action response. Failures are logged but do not surface to
// the caller — the owner change has already been persisted.
//
// Uses context.Background() (not the request context) because the HTTP
// handler returns before the goroutine finishes, which would otherwise
// cancel the email send mid-flight. The 30s timeout bounds goroutine
// lifetime.
func (h *StandardActionsHandler) dispatchChangeOwnerEmail(
	newOwner models.SObject,
	record models.SObject,
	objectAPIName string,
	actingUser *models.UserSession,
) {
	defer func() {
		if r := recover(); r != nil {
			// Never let a notification panic crash the server, but log it
			// so a misconfiguration doesn't silently swallow every email.
			log.Printf("dispatchChangeOwnerEmail panic: %v", r)
		}
	}()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	toEmail, _ := newOwner["Email"].(string)
	if toEmail == "" {
		return
	}
	newOwnerName, _ := newOwner["Name"].(string)

	objectLabel := objectAPIName
	if h.metadata != nil {
		if schema := h.metadata.GetSchema(ctx, objectAPIName); schema != nil && schema.Label != "" {
			objectLabel = schema.Label
		}
	}

	recordName := recordDisplayName(record)
	recordID, _ := record["Id"].(string)
	actingName := ""
	if actingUser != nil {
		actingName = actingUser.Name
	}

	link := h.frontendURL
	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", orFallback(actingName, "A colleague"), objectLabel, recordName))
	if link != "" {
		body.WriteString("\nOpen it here: " + link + "\n")
	}
	body.WriteString("\n— NexusCRM\n")

	msg := &ports.EmailMessage{
		To:      []string{toEmail},
		Subject: subject,
		Body:    body.String(),
	}
	if err := h.emailSender.Send(ctx, msg); err != nil {
		log.Printf("change-owner notification to %s failed: %v", toEmail, err)
	}
}

// recordDisplayName returns a friendly label for a record, falling back to
// common name fields and finally to the Id.
func recordDisplayName(record models.SObject) string {
	for _, key := range []string{"Name", "CaseNumber", "Subject", "Title"} {
		if v, ok := record[key].(string); ok && v != "" {
			return v
		}
	}
	if id, ok := record["Id"].(string); ok {
		return id
	}
	return "(unknown)"
}

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

// isTruthy handles the variety of representations TiDB/MySQL drivers produce
// for BOOLEAN columns: a native Go bool, an int64 (0/1), or a float64.
func isTruthy(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:
		// Route through the string branch so "false" / "true" / "0" / "1"
		// all land on the same interpretation — avoids a latent trap where
		// []byte("false") would be read as truthy because the first byte is 'f'.
		return isTruthy(string(x))
	case string:
		return x == "1" || strings.EqualFold(x, "true")
	}
	return false
}

// cloneSObject makes a shallow copy of an SObject map so callers can hand
// it to a goroutine without sharing a mutable reference with the request path.
func cloneSObject(src models.SObject) models.SObject {
	if src == nil {
		return nil
	}
	dst := make(models.SObject, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst
}

// sfErrorCode maps internal errors to Salesforce-style statusCode strings.
func sfErrorCode(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"
	}
}
