package automation

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ActionService handles execution of metadata-driven actions
type ActionService struct {
	metadata    *metadata.MetadataService
	persistence PersistenceProvider
	permissions *permissions.PermissionService
	txManager   ports.TransactionManager
	emailSender ports.EmailSender
	formula     *formula.Engine
	logger      *logging.Logger
}

// NewActionService creates a new ActionService
func NewActionService(metadata *metadata.MetadataService, persistence PersistenceProvider, permissions *permissions.PermissionService, txManager ports.TransactionManager, emailSender ports.EmailSender, logger *logging.Logger) *ActionService {
	return &ActionService{
		metadata:    metadata,
		persistence: persistence,
		permissions: permissions,
		txManager:   txManager,
		emailSender: emailSender,
		formula:     formula.NewEngine(),
		logger:      logger,
	}
}

// ActionContext holds the state of an action execution, including results from previous steps
type ActionContext struct {
	Record  models.SObject
	User    *models.UserSession
	Results map[string]map[string]interface{}
}

// ExecuteAction executes an action by ID with the given context record and user
func (as *ActionService) ExecuteAction(ctx context.Context, actionID string, contextRecord models.SObject, user *models.UserSession) error {
	// Find the action in metadata cache
	action := as.findAction(ctx, actionID)
	if action == nil {
		return apperrors.NewNotFoundError("action", actionID)
	}

	// Create context
	actionCtx := &ActionContext{
		Record:  contextRecord,
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	return as.executeActionFromMetadata(ctx, action, actionCtx)
}

// executeActionFromMetadata executes an action definition with the given context
func (as *ActionService) executeActionFromMetadata(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// Execute based on action type
	switch action.Type {
	case constants.ActionTypeCreateRecord:
		return as.executeCreateRecord(ctx, action, actionCtx)
	case constants.ActionTypeUpdateRecord:
		return as.executeUpdateRecord(ctx, action, actionCtx)
	case constants.ActionTypeSendEmail:
		return as.executeSendEmail(ctx, action, actionCtx)
	case constants.ActionTypeCallWebhook:
		return as.executeCallWebhook(ctx, action, actionCtx)
	case constants.ActionTypeComposite:
		return as.executeComposite(ctx, action, actionCtx)
	default:
		return apperrors.NewValidationError("action.Type", fmt.Sprintf("unsupported action type: %s", action.Type))
	}
}

// ExecuteActionDirect executes an action metadata directly without ID lookup (used by FlowExecutor)
func (as *ActionService) ExecuteActionDirect(ctx context.Context, action *models.ActionMetadata, record models.SObject, user *models.UserSession) error {
	actionCtx := &ActionContext{
		Record:  record,
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}
	return as.executeActionFromMetadata(ctx, action, actionCtx)
}

// findAction searches for an action by ID using MetadataService
func (as *ActionService) findAction(ctx context.Context, actionID string) *models.ActionMetadata {
	return as.metadata.GetActionByID(ctx, actionID)
}

// executeCreateRecord creates a new record based on action configuration
func (as *ActionService) executeCreateRecord(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// Extract configuration
	targetObject, err := utils.GetConfigStringRequired(action.Config, constants.ConfigTargetObject)
	if err != nil {
		return err
	}

	fieldMappings, ok := utils.GetConfigMap(action.Config, constants.ConfigFieldMappings)
	if !ok {
		return apperrors.NewValidationError("field_mappings", "field_mappings not specified in action config")
	}

	// Build record from field mappings
	record := make(models.SObject)
	for fieldName, formulaOrValue := range fieldMappings {
		value, err := as.evaluateRef(ctx, formulaOrValue, actionCtx, action.ObjectAPIName)
		if err != nil {
			return apperrors.NewInternalError(fmt.Sprintf("failed to evaluate field %s", fieldName), err)
		}
		record[fieldName] = value
	}

	// Insert the record (context propagates the transaction)
	result, err := as.persistence.Insert(ctx, targetObject, record, actionCtx.User)
	if err != nil {
		return err
	}

	// Store result for future steps
	actionCtx.Results[action.ID] = result

	return nil
}

// executeUpdateRecord updates an existing record based on action configuration
func (as *ActionService) executeUpdateRecord(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// Extract configuration
	targetObjectName, err := utils.GetConfigStringRequired(action.Config, constants.ConfigTargetObject)
	if err != nil {
		return err
	}

	recordIDVal, err := as.getConfigValue(ctx, action.Config, constants.ConfigRecordID, actionCtx, action.ObjectAPIName)
	if err != nil {
		return apperrors.NewInternalError("failed to get record_id", err)
	}
	recordID := fmt.Sprintf("%v", recordIDVal)

	fieldMappings, ok := utils.GetConfigMap(action.Config, constants.ConfigFieldMappings)
	if !ok {
		return apperrors.NewValidationError("field_mappings", "field_mappings not specified in action config")
	}

	// Build updates from field mappings
	updates := make(models.SObject)
	for fieldName, formulaOrValue := range fieldMappings {
		value, err := as.evaluateRef(ctx, formulaOrValue, actionCtx, action.ObjectAPIName)
		if err != nil {
			return apperrors.NewInternalError(fmt.Sprintf("failed to evaluate field %s", fieldName), err)
		}
		updates[fieldName] = value
	}

	// Update the record
	return as.persistence.Update(ctx, targetObjectName, recordID, updates, actionCtx.User)
}

// executeComposite executes a sequence of actions within a transaction
func (as *ActionService) executeComposite(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// composite action config should have a "steps" array
	stepsInterface, ok := action.Config[constants.ConfigKeySteps]
	if !ok {
		return apperrors.NewValidationError("steps", "steps not specified in composite action")
	}

	stepsList, ok := stepsInterface.([]interface{})
	if !ok {
		return apperrors.NewValidationError("steps", "steps must be an array")
	}

	// EXECUTE WITHIN TRANSACTION
	return as.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
		// Execute steps using the transactional context
		return as.executeSteps(txCtx, stepsList, actionCtx, action.ObjectAPIName)
	})
}

func (as *ActionService) executeSteps(ctx context.Context, steps []interface{}, actionCtx *ActionContext, sourceObjectName string) error {
	for _, stepInterface := range steps {
		stepConfig, ok := stepInterface.(map[string]interface{})
		if !ok {
			return apperrors.NewValidationError("step", "invalid step config format")
		}

		stepID, _ := stepConfig[constants.FieldID].(string)
		stepType, _ := stepConfig[constants.FieldSysAction_Type].(string)

		if stepID == "" || stepType == "" {
			return apperrors.NewValidationError("step", "step must have id and type")
		}

		stepAction := &models.ActionMetadata{
			ID:            stepID,
			ObjectAPIName: sourceObjectName, // Inherit source object context
			Type:          stepType,
			Config:        stepConfig, // Pass full config
		}

		// Recursively execute
		if err := as.executeActionFromMetadata(ctx, stepAction, actionCtx); err != nil {
			return apperrors.NewInternalError(fmt.Sprintf("step %s failed", stepID), err)
		}
	}
	return nil
}

// executeSendEmail sends an email based on action configuration.
// Template mode: config contains emailTemplate (ID) + recipient (address or formula).
// The template is looked up from _System_EmailTemplate and merge fields are resolved.
func (as *ActionService) executeSendEmail(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// Extract email template ID
	templateIDRaw, err := as.getConfigValue(ctx, action.Config, constants.ConfigEmailTemplate, actionCtx, action.ObjectAPIName)
	if err != nil {
		return apperrors.NewInternalError("failed to get emailTemplate from action config", err)
	}
	templateID := fmt.Sprintf("%v", templateIDRaw)

	// Extract recipient (email address or formula like {!User.Id})
	recipientRaw, err := as.getConfigValue(ctx, action.Config, constants.ConfigRecipient, actionCtx, action.ObjectAPIName)
	if err != nil {
		return apperrors.NewInternalError("failed to get recipient from action config", err)
	}
	recipientStr := fmt.Sprintf("%v", recipientRaw)

	// Load and render the email template
	subject, htmlBody, plainBody, err := as.LoadAndRenderTemplate(ctx, templateID, actionCtx, action.ObjectAPIName)
	if err != nil {
		return err
	}

	// Resolve recipient to an email address
	toEmail, err := as.ResolveRecipient(ctx, recipientStr)
	if err != nil {
		return apperrors.NewInternalError("failed to resolve email recipient", err)
	}

	// Log the email action
	userName := constants.DefaultUserName
	if actionCtx.User != nil {
		userName = actionCtx.User.Name
	}
	as.logger.Info(fmt.Sprintf("📧 EMAIL ACTION: To=%s Subject=%q Template=%s TriggeredBy=%s", toEmail, subject, templateID, userName))

	// Send the email
	msg := &ports.EmailMessage{
		To:      []string{toEmail},
		Subject: subject,
		Body:    plainBody,
		HTML:    htmlBody,
	}
	if err := as.emailSender.Send(ctx, msg); err != nil {
		return apperrors.NewInternalError(fmt.Sprintf("failed to send email to %s", toEmail), err)
	}

	return nil
}

// LoadAndRenderTemplate loads an email template by ID, validates it is active,
// and resolves merge fields in subject/body. Returns the rendered subject, HTML body,
// and plain text body. If actionCtx is nil, merge field resolution is skipped.
func (as *ActionService) LoadAndRenderTemplate(ctx context.Context, templateID string, actionCtx *ActionContext, objectAPIName string) (subject, htmlBody, plainBody string, err error) {
	template, lookupErr := as.persistence.GetRaw(ctx, constants.TableEmailTemplate, templateID)
	if lookupErr != nil {
		return "", "", "", apperrors.NewInternalError(fmt.Sprintf("failed to look up email template %s", templateID), lookupErr)
	}
	if template == nil {
		return "", "", "", apperrors.NewNotFoundError("EmailTemplate", templateID)
	}

	// Validate template is active
	if isActive, ok := template[constants.FieldSysEmailTemplate_IsActive]; ok {
		active := false
		switch v := isActive.(type) {
		case bool:
			active = v
		case int64:
			active = v == 1
		case float64:
			active = v == 1
		}
		if !active {
			return "", "", "", apperrors.NewValidationError("emailTemplate", fmt.Sprintf("email template %s is inactive", templateID))
		}
	}

	subject = utils.GetConfigString(template, constants.FieldSysEmailTemplate_Subject)
	htmlBody = utils.GetConfigString(template, constants.FieldSysEmailTemplate_HTMLValue)
	plainBody = utils.GetConfigString(template, constants.FieldSysEmailTemplate_Body)

	// Resolve merge fields if action context is available
	if actionCtx != nil {
		subject = as.resolveTemplateText(ctx, subject, actionCtx, objectAPIName)
		htmlBody = as.resolveTemplateText(ctx, htmlBody, actionCtx, objectAPIName)
		plainBody = as.resolveTemplateText(ctx, plainBody, actionCtx, objectAPIName)
	}

	return subject, htmlBody, plainBody, nil
}

// ResolveRecipient converts a recipient value to an email address.
// If the value contains '@', it's treated as a literal email address.
// Otherwise, it's treated as a User ID and the user's email is looked up.
func (as *ActionService) ResolveRecipient(ctx context.Context, recipient string) (string, error) {
	recipient = strings.TrimSpace(recipient)
	if recipient == "" {
		return "", fmt.Errorf("recipient is empty")
	}

	// If it looks like an email address, use directly
	if strings.Contains(recipient, "@") {
		return recipient, nil
	}

	// Treat as a User ID — look up the user's email
	user, err := as.persistence.GetRaw(ctx, constants.TableUser, recipient)
	if err != nil {
		return "", fmt.Errorf("failed to look up user %s: %w", recipient, err)
	}
	if user == nil {
		return "", fmt.Errorf("user %s not found", recipient)
	}

	email := utils.GetConfigString(user, constants.FieldEmail)
	if email == "" {
		return "", fmt.Errorf("user %s has no email address", recipient)
	}
	return email, nil
}

// resolveTemplateText replaces merge field placeholders ({!...}) in template text
// with their resolved values from the action context.
func (as *ActionService) resolveTemplateText(ctx context.Context, text string, actionCtx *ActionContext, objectName string) string {
	if text == "" {
		return text
	}

	text = as.resolveTemplateTextWithDelimiter(ctx, text, "{!", "}", actionCtx, objectName, func(raw string) string {
		return raw
	})
	return as.resolveTemplateTextWithDelimiter(ctx, text, "{{", "}}", actionCtx, objectName, normalizeDoubleBraceMergeField)
}

func (as *ActionService) resolveTemplateTextWithDelimiter(
	ctx context.Context,
	text string,
	startDelimiter string,
	endDelimiter string,
	actionCtx *ActionContext,
	objectName string,
	normalize func(string) string,
) string {
	var result strings.Builder
	remaining := text

	for {
		start := strings.Index(remaining, startDelimiter)
		if start == -1 {
			result.WriteString(remaining)
			break
		}
		end := strings.Index(remaining[start:], endDelimiter)
		if end == -1 {
			result.WriteString(remaining)
			break
		}
		end += start // adjust to absolute position

		result.WriteString(remaining[:start])

		rawField := remaining[start+len(startDelimiter) : end]
		normalized := normalize(strings.TrimSpace(rawField))
		mergeField := "{!" + normalized + "}"
		resolved, err := as.evaluateRef(ctx, mergeField, actionCtx, objectName)
		if err != nil {
			result.WriteString(remaining[start : end+len(endDelimiter)])
		} else {
			result.WriteString(fmt.Sprintf("%v", resolved))
		}

		remaining = remaining[end+len(endDelimiter):]
	}

	return result.String()
}

func normalizeDoubleBraceMergeField(raw string) string {
	if raw == "" {
		return raw
	}
	if strings.HasPrefix(raw, "User.") || strings.HasPrefix(raw, "Record.") {
		return raw
	}

	parts := strings.Split(raw, ".")
	switch len(parts) {
	case 0:
		return raw
	case 1:
		return "Record." + parts[0]
	default:
		return "Record." + strings.Join(parts[1:], ".")
	}
}

// executeCallWebhook calls a webhook based on action configuration
func (as *ActionService) executeCallWebhook(ctx context.Context, action *models.ActionMetadata, actionCtx *ActionContext) error {
	// Extract webhook configuration
	urlValue, err := as.getConfigValue(ctx, action.Config, constants.ConfigURL, actionCtx, action.ObjectAPIName)
	if err != nil {
		return apperrors.NewInternalError("failed to get webhook URL", err)
	}
	url := fmt.Sprintf("%v", urlValue)

	methodValue, err := as.getConfigValue(ctx, action.Config, constants.ConfigMethod, actionCtx, action.ObjectAPIName)
	if err != nil {
		// Default to POST if not specified
		methodValue = "POST"
	}
	method := strings.ToUpper(fmt.Sprintf("%v", methodValue))

	// Validate method
	if method != "GET" && method != "POST" && method != "PUT" && method != "PATCH" && method != "DELETE" {
		return apperrors.NewValidationError("method", fmt.Sprintf("invalid HTTP method: %s", method))
	}

	// Build request body from payload config
	var bodyReader io.Reader
	if payload, err := as.getConfigValue(ctx, action.Config, constants.ConfigPayload, actionCtx, action.ObjectAPIName); err == nil && payload != nil {
		payloadBytes, err := json.Marshal(payload)
		if err != nil {
			return apperrors.NewInternalError("failed to serialize webhook payload", err)
		}
		bodyReader = bytes.NewReader(payloadBytes)
	}

	// SSRF prevention: reject loopback, private, and link-local targets
	if err := isAllowedWebhookURL(url); err != nil {
		return apperrors.NewValidationError("url", err.Error())
	}

	// Create HTTP request
	req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
	if err != nil {
		return apperrors.NewInternalError("failed to create webhook request", err)
	}

	// Set default content type
	req.Header.Set("Content-Type", "application/json")

	// Add custom headers if specified
	if headers, ok := utils.GetConfigMap(action.Config, constants.ConfigHeaders); ok {
		for headerName, headerValue := range headers {
			req.Header.Set(headerName, fmt.Sprintf("%v", headerValue))
		}
	}

	// Execute the webhook with a reasonable timeout
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		as.logger.Error(fmt.Sprintf("⚠️ WEBHOOK FAILED: URL=%s Method=%s", url, method), err)
		return apperrors.NewInternalError("webhook request failed", err)
	}
	defer func() { _ = resp.Body.Close() }()

	// Check response status
	if resp.StatusCode >= 400 {
		as.logger.Error(fmt.Sprintf("WEBHOOK ERROR RESPONSE: URL=%s Status=%d", url, resp.StatusCode), nil)
		return apperrors.NewInternalError(fmt.Sprintf("webhook returned error status: %d", resp.StatusCode), nil)
	}

	as.logger.Info(fmt.Sprintf("WEBHOOK SUCCESS: URL=%s Method=%s Status=%d", url, method, resp.StatusCode))
	return nil
}

// getConfigValue extracts a value from action config and evaluates it if it's a formula
func (as *ActionService) getConfigValue(ctx context.Context, config map[string]interface{}, key string, actionCtx *ActionContext, sourceObjectName string) (interface{}, error) {
	value, exists := config[key]
	if !exists {
		return nil, apperrors.NewValidationError(key, fmt.Sprintf("%s not specified in action config", key))
	}

	// Evaluate if it's a formula
	return as.evaluateRef(ctx, value, actionCtx, sourceObjectName)
}

// evaluateRef evaluates a value using the metadata-driven Formula Engine
func (as *ActionService) evaluateRef(ctx context.Context, value interface{}, actionCtx *ActionContext, sourceObjectName string) (interface{}, error) {
	// If it's a string, check if it's a formula
	strValue, ok := value.(string)
	if !ok {
		// Return literal value as-is
		return value, nil
	}

	// Check if it's a formula (starts with {! and ends with })
	if !strings.HasPrefix(strValue, "{!") || !strings.HasSuffix(strValue, "}") {
		// Return literal string
		return strValue, nil
	}

	// Extract formula content
	expression := strings.TrimSpace(strValue[2 : len(strValue)-1])
	expression = normalizeActionFormulaExpression(expression)

	// Prepare Formula Engine Context from ActionContext
	formulaCtx := &formula.Context{
		Record: actionCtx.Record,
		Env:    make(map[string]interface{}), // Add Env if needed
	}

	// Add FLS check if user is present
	if actionCtx.User != nil && as.permissions != nil {
		formulaCtx.IsVisible = func(fieldName string) bool {
			// Use provided source object name
			if sourceObjectName != "" {
				return as.permissions.CheckFieldVisibilityWithUser(ctx, sourceObjectName, fieldName, actionCtx.User)
			}
			// Fallback: If we can't determine object, we default to hidden for safety
			return false
		}
	}

	// Add User context — use the shared helper so $Profile.Name, $User.Id,
	// and $UserRole.Name resolve identically here and in evaluateFlowFormulas
	// (Two-Path Consistency — CLAUDE.md R10 / #2259).
	formulaCtx.User = buildFlowUserFormulaScope(actionCtx.User)

	// Add Results from previous steps as custom fields.
	// Flatten Results: results.stepId.field -> accessing via formula context.
	// We convert map[string]map[string]interface{} to map[string]interface{} for the "results" key.
	resultsMap := make(map[string]interface{})
	for k, v := range actionCtx.Results {
		resultsMap[k] = v
	}

	if formulaCtx.Fields == nil {
		formulaCtx.Fields = make(map[string]interface{})
	}
	formulaCtx.Fields[constants.ConfigKeyResults] = resultsMap

	// Evaluate using shared engine
	return as.formula.Evaluate(expression, formulaCtx)
}

func normalizeActionFormulaExpression(expression string) string {
	switch {
	case strings.HasPrefix(expression, "Record."):
		return "record." + strings.TrimPrefix(expression, "Record.")
	case strings.HasPrefix(expression, "User."):
		return "user." + strings.TrimPrefix(expression, "User.")
	default:
		return expression
	}
}

// isAllowedWebhookURL validates that a webhook target URL is reachable from the
// internet and not an internal/loopback address. This prevents SSRF attacks where
// an automation rule could be used to probe internal infrastructure.
func isAllowedWebhookURL(rawURL string) error {
	u, err := url.Parse(rawURL)
	if err != nil {
		return fmt.Errorf("invalid URL: %w", err)
	}
	if u.Scheme != "https" && u.Scheme != "http" {
		return fmt.Errorf("webhook URLs must use HTTP(S)")
	}
	ips, err := net.LookupIP(u.Hostname())
	if err != nil {
		return fmt.Errorf("cannot resolve hostname: %w", err)
	}
	for _, ip := range ips {
		if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
			return fmt.Errorf("webhook URLs must not target internal addresses")
		}
	}
	return nil
}
