package rest

import (
	"context"
	"encoding/json"
	stderrors "errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/features/files"
	"github.com/nexuscrm/backend/internal/features/mru"
	"github.com/nexuscrm/backend/internal/features/uiapi"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

func generateUUID() string {
	return utils.GenerateID()
}

type UIAPIHandler struct {
	uiService    *uiapi.Service
	mruService   *mru.Service
	hrefResolver *files.HrefFieldResolver
}

type layoutAssignmentResponse struct {
	ID               string    `json:"id"`
	ObjectAPIName    string    `json:"objectApiName"`
	LayoutID         string    `json:"layoutId"`
	RoleID           *string   `json:"roleId,omitempty"`
	ProfileID        *string   `json:"profileId,omitempty"`
	RecordTypeID     *string   `json:"recordTypeId,omitempty"`
	Specificity      int       `json:"specificity"`
	CreatedDate      time.Time `json:"createdDate,omitempty"`
	LastModifiedDate time.Time `json:"lastModifiedDate,omitempty"`
	CreatedByID      *string   `json:"createdById,omitempty"`
}

func newLayoutAssignmentResponse(assignment *models.LayoutAssignment) layoutAssignmentResponse {
	if assignment == nil {
		return layoutAssignmentResponse{}
	}
	return layoutAssignmentResponse{
		ID:               assignment.ID,
		ObjectAPIName:    assignment.ObjectAPIName,
		LayoutID:         assignment.LayoutID,
		RoleID:           assignment.RoleID,
		ProfileID:        assignment.ProfileID,
		RecordTypeID:     assignment.RecordTypeID,
		Specificity:      assignment.Specificity,
		CreatedDate:      assignment.CreatedDate,
		LastModifiedDate: assignment.LastModifiedDate,
		CreatedByID:      assignment.CreatedByID,
	}
}

func NewUIAPIHandler(uiService *uiapi.Service, mruService *mru.Service, hrefResolver *files.HrefFieldResolver) *UIAPIHandler {
	return &UIAPIHandler{uiService: uiService, mruService: mruService, hrefResolver: hrefResolver}
}

func writeUIAPIError(c *gin.Context, err error) {
	var apiErr *apierr.Error
	if stderrors.As(err, &apiErr) {
		errorCode := apierr.SFCodeInternalError
		switch apiErr.Status {
		case http.StatusBadRequest:
			errorCode = apierr.SFCodeInvalidRequest
		case http.StatusUnauthorized:
			errorCode = apierr.SFCodeUnauthorized
		case http.StatusForbidden:
			errorCode = apierr.SFCodeForbidden
		case http.StatusNotFound:
			errorCode = apierr.SFCodeNotFound
		}
		c.JSON(apiErr.Status, apierr.SFErrorResponse(errorCode, apiErr.Error()))
		return
	}
	if errors.IsNotFound(err) {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, err.Error()))
		return
	}
	if errors.IsPermission(err) || errors.GetHTTPStatus(err) == http.StatusForbidden {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, err.Error()))
		return
	}
	if errors.IsValidation(err) {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, err.Error()))
		return
	}
	c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
}

// HandleRecordUI handles GET /ui-api/record-ui/:recordIds
// Salesforce v63.0 compatible - auto-resolves object type from record ID prefix
func (h *UIAPIHandler) HandleRecordUI(c *gin.Context) {
	recordIdsParam, ok := RequireParam(c, "recordIds")
	if !ok {
		return
	}

	ids := strings.Split(recordIdsParam, ",")
	if len(ids) > 50 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeLimitExceeded, "Too many record IDs (max 50)"))
		return
	}

	// Salesforce v63.0: Auto-resolve type from record ID prefix
	// Optional type query parameter as fallback for custom objects
	typeHint := c.Query("type")
	if typeHint == "" {
		// Try to resolve type from first record ID using keyPrefix
		if len(ids) > 0 && len(ids[0]) >= 3 {
			if resolvedType, ok := mru.ResolveTypeFromID(ids[0]); ok {
				typeHint = resolvedType
			}
		}
	}

	// If still no type, return error
	if typeHint == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidField, "Unable to resolve object type from record ID. Use 'type' query parameter for custom objects."))
		return
	}

	// Preserve original casing for response keys before normalizing for DB queries.
	// The aggregator prefers schema.APIName for response keys, but falls back to
	// this when schema lookup fails (e.g. cache not yet warm).
	originalTypeHint := typeHint
	typeHint = strings.ToLower(typeHint)

	user := MustGetUser(c)

	// Call Aggregator
	recordUI, err := h.uiService.GetRecordUI(c.Request.Context(), ids, typeHint, user, originalTypeHint)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	// Rewrite legacy SF ContentDistribution hrefs in the four registered
	// Contract/Order columns to point at matching `_System_File` rows on the
	// same record. The frontend's NexusFileLinkField renderer keys off the
	// `/services/data/v63.0/files/...` prefix to decide between the resolved
	// (file chip) and unresolved (warning) display states.
	h.rewriteHrefFieldsInRecordUI(c.Request.Context(), recordUI)

	// Async MRU Tracking - spawn goroutines BEFORE response to avoid context lifecycle issues
	// Capture user.ID and typeHint before spawning goroutines

	if h.mruService != nil {
		userID := user.ID
		objectType := typeHint
		for _, id := range ids {
			recordID := id // Capture loop variable
			go h.mruService.TrackRecordAccess(context.Background(), userID, objectType, recordID)
		}
	}

	// Direct JSON response (complex structure)
	c.JSON(http.StatusOK, recordUI)
}

// rewriteHrefFieldsInRecordUI applies HrefFieldResolver to every record in
// the UI-API response envelope. The resolver mutates Field.Value in place;
// when DisplayValue mirrors Value (the legacy SF column case where the raw
// HTML is also the formatted display), it is updated to match. Errors are
// swallowed and logged so a resolver hiccup never blocks the page render —
// the unrewritten value still renders, just as a warning chip on the
// frontend.
func (h *UIAPIHandler) rewriteHrefFieldsInRecordUI(ctx context.Context, recordUI *uiapi.RecordUI) {
	if h == nil || h.hrefResolver == nil || recordUI == nil {
		return
	}
	for _, record := range recordUI.Records {
		fieldNames := files.FieldsToRewriteForObject(record.ApiName)
		if len(fieldNames) == 0 {
			continue
		}
		for _, fieldName := range fieldNames {
			fv, ok := record.Fields[fieldName]
			if !ok {
				continue
			}
			rawValue, ok := fv.Value.(string)
			if !ok || strings.TrimSpace(rawValue) == "" {
				continue
			}
			rewritten, changed, err := h.hrefResolver.RewriteValue(ctx, record.Id, rawValue)
			if err != nil {
				log.Warn().Err(err).
					Str("object", record.ApiName).
					Str("id", record.Id).
					Str("field", fieldName).
					Msg("href resolver: serving record with raw legacy href value")
				continue
			}
			if !changed {
				continue
			}
			fv.Value = rewritten
			if fv.DisplayValue != nil && *fv.DisplayValue == rawValue {
				fv.DisplayValue = &rewritten
			}
			record.Fields[fieldName] = fv
		}
	}
}

// HandleBatchRecordAccess handles POST /ui-api/record-access
// Returns record-level access (hasReadAccess, hasEditAccess, hasDeleteAccess) for a batch of record IDs.
func (h *UIAPIHandler) HandleBatchRecordAccess(c *gin.Context) {
	var req struct {
		ObjectApiName string   `json:"objectApiName"`
		RecordIds     []string `json:"recordIds"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid request body"))
		return
	}
	if req.ObjectApiName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName is required"))
		return
	}
	if len(req.RecordIds) == 0 {
		c.JSON(http.StatusOK, gin.H{"recordAccess": map[string]interface{}{}})
		return
	}
	if len(req.RecordIds) > 200 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeLimitExceeded, "Too many record IDs (max 200)"))
		return
	}

	user := MustGetUser(c)
	objectName := strings.ToLower(req.ObjectApiName)

	// Check object-level Read permission
	if err := h.uiService.CheckReadPermission(c.Request.Context(), objectName, user); err != nil {
		writeUIAPIError(c, err)
		return
	}

	// Retrieve records and compute access
	accessMap, err := h.uiService.GetBatchRecordAccess(c.Request.Context(), objectName, req.RecordIds, user)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"recordAccess": accessMap})
}

// HandleObjectInfo handles GET /ui-api/object-info/:objectApiName
func (h *UIAPIHandler) HandleObjectInfo(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	user := MustGetUser(c)

	info, err := h.uiService.GetObjectInfo(c.Request.Context(), objectName, user)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	if info == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Object not found"))
		return
	}

	c.JSON(http.StatusOK, info)
}

// HandleListLayouts handles GET /ui-api/layouts/:objectApiName
// Returns all layouts for an object (summary metadata for list views)
func (h *UIAPIHandler) HandleListLayouts(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	layouts, err := h.uiService.ListLayouts(c.Request.Context(), objectName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	type layoutSummary struct {
		ID               string `json:"id"`
		LayoutName       string `json:"layoutName"`
		ObjectAPIName    string `json:"objectApiName"`
		Type             string `json:"type"`
		IsDefault        bool   `json:"isDefault"`
		CreatedDate      string `json:"createdDate,omitempty"`
		LastModifiedDate string `json:"lastModifiedDate,omitempty"`
	}

	result := make([]layoutSummary, 0, len(layouts))
	for _, l := range layouts {
		result = append(result, layoutSummary{
			ID:               l.ID,
			LayoutName:       l.LayoutName,
			ObjectAPIName:    l.ObjectAPIName,
			Type:             l.Type,
			IsDefault:        l.IsDefault,
			CreatedDate:      l.CreatedDate.Format("2006-01-02T15:04:05Z"),
			LastModifiedDate: l.LastModifiedDate.Format("2006-01-02T15:04:05Z"),
		})
	}

	c.JSON(http.StatusOK, gin.H{"layouts": result, "totalSize": len(result)})
}

// HandleListApprovalLayouts handles GET /ui-api/approval-layouts/:objectApiName
// Returns all approval layouts for an object (summary metadata for list views)
func (h *UIAPIHandler) HandleListApprovalLayouts(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	approvallayouts, err := h.uiService.GetApprovalLayouts(c.Request.Context(), objectName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	type approvalLayoutSummary struct {
		ID               string `json:"id"`
		LayoutName       string `json:"layoutName"`
		ObjectAPIName    string `json:"objectApiName"`
		Type             string `json:"type"`
		IsDefault        bool   `json:"isDefault"`
		CreatedDate      string `json:"createdDate,omitempty"`
		LastModifiedDate string `json:"lastModifiedDate,omitempty"`
	}

	result := make([]approvalLayoutSummary, 0, len(approvallayouts))
	for _, l := range approvallayouts {
		result = append(result, approvalLayoutSummary{
			ID:               l.ID,
			LayoutName:       l.LayoutName,
			ObjectAPIName:    l.ObjectAPIName,
			Type:             l.Type,
			IsDefault:        l.IsDefault,
			CreatedDate:      l.CreatedDate.Format("2006-01-02T15:04:05Z"),
			LastModifiedDate: l.LastModifiedDate.Format("2006-01-02T15:04:05Z"),
		})
	}

	c.JSON(http.StatusOK, gin.H{"approvalLayouts": result, "totalSize": len(result)})
}

// HandleSaveApprovalLayout handles POST /ui-api/approval-layout/Global
func (h *UIAPIHandler) HandleSaveApprovalLayout(c *gin.Context) {
	var layout models.ApprovalPageLayout
	if err := c.ShouldBindJSON(&layout); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid approval layout JSON: "+err.Error()))
		return
	}

	if layout.ObjectAPIName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName is required"))
		return
	}

	if layout.LayoutName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "layoutName is required"))
		return
	}

	isCreate := layout.ID == ""
	if isCreate {
		layout.ID = generateUUID()
	}

	// Set default type if not specified
	if layout.Type == "" {
		layout.Type = "approval_response"
	}

	// Mark as admin-created
	layout.Source = "admin_created"
	layout.IsCustomized = true

	if err := h.uiService.SaveApprovalLayout(c.Request.Context(), &layout); err != nil {
		errMsg := err.Error()
		if isCreate && strings.Contains(errMsg, "Duplicate entry") {
			c.JSON(http.StatusConflict, apierr.SFErrorResponse("DUPLICATE_VALUE",
				"An approval layout named '"+layout.LayoutName+"' already exists for this object"))
			return
		}
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, errMsg))
		return
	}

	status := http.StatusOK
	if isCreate {
		status = http.StatusCreated
	}
	c.JSON(status, layout)
}

// HandleSetDefaultApprovalLayout handles PATCH /ui-api/approval-layout/:layoutId/default
func (h *UIAPIHandler) HandleSetDefaultApprovalLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	var body struct {
		ObjectAPIName string `json:"objectApiName"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.ObjectAPIName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName is required"))
		return
	}

	layout, err := h.uiService.GetApprovalLayoutMetadata(c.Request.Context(), layoutID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if layout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Approval layout not found"))
		return
	}
	if layout.ObjectAPIName != body.ObjectAPIName {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest,
			"Approval layout does not belong to object '"+body.ObjectAPIName+"'"))
		return
	}

	if err := h.uiService.SetDefaultApprovalLayout(c.Request.Context(), body.ObjectAPIName, layoutID); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.JSON(http.StatusOK, gin.H{"success": true})
}

// HandleDeleteApprovalLayout handles DELETE /ui-api/approval-layout/:layoutId
func (h *UIAPIHandler) HandleDeleteApprovalLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	layout, err := h.uiService.GetApprovalLayoutMetadata(c.Request.Context(), layoutID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if layout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Approval layout not found"))
		return
	}
	if layout.IsDefault {
		c.JSON(http.StatusConflict, apierr.SFErrorResponse("CANNOT_DELETE_DEFAULT",
			"Cannot delete the default approval layout. Set another approval layout as default first."))
		return
	}

	if err := h.uiService.DeleteApprovalLayout(c.Request.Context(), layoutID); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.Status(http.StatusNoContent)
}

// HandleCloneApprovalLayout handles POST /ui-api/approval-layout/:layoutId/clone
func (h *UIAPIHandler) HandleCloneApprovalLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	var req struct {
		LayoutName string `json:"layoutName"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || req.LayoutName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "layoutName is required"))
		return
	}

	// Get the source layout to clone
	sourceLayout, err := h.uiService.GetApprovalLayoutMetadata(c.Request.Context(), layoutID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if sourceLayout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Source approval layout not found"))
		return
	}

	// Deep copy the layout with new name and ID
	clonedLayout := *sourceLayout
	clonedLayout.ID = generateUUID()
	clonedLayout.LayoutName = req.LayoutName
	clonedLayout.IsDefault = false
	clonedLayout.IsCustomized = false
	clonedLayout.Source = "cloned"
	clonedLayout.GenerationVersion = 0
	clonedLayout.CreatedDate = time.Now()
	clonedLayout.LastModifiedDate = time.Now()

	// Deep copy the config
	if sourceLayout.Config != nil {
		// Create a deep copy by marshaling/unmarshaling
		configBytes, _ := json.Marshal(sourceLayout.Config)
		json.Unmarshal(configBytes, &clonedLayout.Config)
	}

	if err := h.uiService.SaveApprovalLayout(c.Request.Context(), &clonedLayout); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.JSON(http.StatusCreated, &clonedLayout)
}

// HandleLayout handles GET /ui-api/layout/:objectApiName
// Supports optional ?layoutId= query param to load a specific layout by ID
func (h *UIAPIHandler) HandleLayout(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	// Read mode early — needed by both layoutId and default paths
	layoutType := c.Query("type")
	switch layoutType {
	case "", "Detail", "Create", "Edit":
		// valid
	default:
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid layout type. Must be one of: Create, Edit, Detail"))
		return
	}

	layoutID := c.Query("layoutId")

	if layoutID != "" {
		mode := "View"
		switch layoutType {
		case "Create":
			mode = "Create"
		case "Edit":
			mode = "Edit"
		}
		layout, err := h.uiService.GetLayoutByID(c.Request.Context(), layoutID, objectName, mode)
		if err != nil {
			c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
			return
		}
		if layout == nil {
			c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Layout not found"))
			return
		}
		c.JSON(http.StatusOK, layout)
		return
	}

	user := MustGetUser(c)
	// layoutType already validated above (before layoutId branch)
	recordTypeID := c.Query("recordTypeId")
	layout, err := h.uiService.GetLayoutWithOptions(c.Request.Context(), objectName, user, recordTypeID, layoutType)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	if layout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Layout not found"))
		return
	}

	c.JSON(http.StatusOK, layout)
}

// HandleSaveLayout handles POST /ui-api/layout/Global
func (h *UIAPIHandler) HandleSaveLayout(c *gin.Context) {
	var layout models.PageLayout
	if err := c.ShouldBindJSON(&layout); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid layout JSON: "+err.Error()))
		return
	}

	if layout.ObjectAPIName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName is required"))
		return
	}

	if layout.LayoutName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "layoutName is required"))
		return
	}

	isCreate := layout.ID == ""
	if isCreate {
		layout.ID = generateUUID()
	}

	// Mark as admin-created (Source replaces IsCustomized; both set for backward compat)
	layout.Source = "admin_created"
	layout.IsCustomized = true

	if err := h.uiService.SaveLayout(c.Request.Context(), &layout); err != nil {
		errMsg := err.Error()
		if isCreate && strings.Contains(errMsg, "Duplicate entry") {
			c.JSON(http.StatusConflict, apierr.SFErrorResponse("DUPLICATE_VALUE",
				"A layout named '"+layout.LayoutName+"' already exists for this object"))
			return
		}
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, errMsg))
		return
	}

	// MarkLayoutCustomized kept for backward compat (redundant with Source but safe)
	h.uiService.MarkLayoutCustomized(c.Request.Context(), layout.ID)

	// Write version history for undo/rollback
	reason := "admin_edit"
	if isCreate {
		reason = "admin_create"
	}
	user := MustGetUser(c)
	userID := &user.ID
	h.uiService.WriteLayoutHistory(c.Request.Context(), layout.ID, reason, userID)

	status := http.StatusOK
	if isCreate {
		status = http.StatusCreated
	}
	c.JSON(status, layout)
}

// HandleSetDefaultLayout handles PATCH /ui-api/layout/:layoutId/default
func (h *UIAPIHandler) HandleSetDefaultLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	var body struct {
		ObjectAPIName string `json:"objectApiName"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.ObjectAPIName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName is required"))
		return
	}

	layout, err := h.uiService.GetLayoutMetadata(c.Request.Context(), layoutID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if layout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Layout not found"))
		return
	}
	if layout.ObjectAPIName != body.ObjectAPIName {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest,
			"Layout does not belong to object '"+body.ObjectAPIName+"'"))
		return
	}

	user := MustGetUser(c)
	if err := h.uiService.SetDefaultLayout(c.Request.Context(), body.ObjectAPIName, layoutID, &user.ID); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.JSON(http.StatusOK, gin.H{"success": true})
}

// HandleDeleteLayout handles DELETE /ui-api/layout/:layoutId
func (h *UIAPIHandler) HandleDeleteLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	layout, err := h.uiService.GetLayoutMetadata(c.Request.Context(), layoutID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if layout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Layout not found"))
		return
	}
	if layout.IsDefault {
		c.JSON(http.StatusConflict, apierr.SFErrorResponse("CANNOT_DELETE_DEFAULT",
			"Cannot delete the default layout. Set another layout as default first."))
		return
	}

	// Block delete if layout has non-wildcard (targeted) assignments.
	// Admin must reassign those audiences to another layout first.
	assignments, err := h.uiService.GetAssignmentsForObject(c.Request.Context(), layout.ObjectAPIName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "failed to check assignments: "+err.Error()))
		return
	}
	for _, a := range assignments {
		if a.LayoutID == layoutID && a.Specificity > 0 {
			c.JSON(http.StatusConflict, apierr.SFErrorResponse("LAYOUT_IN_USE",
				"Cannot delete layout with active assignments. Remove assignments first."))
			return
		}
	}

	if err := h.uiService.DeleteLayout(c.Request.Context(), layoutID); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.Status(http.StatusNoContent)
}

// HandleCloneLayout handles POST /ui-api/layout/:layoutId/clone
func (h *UIAPIHandler) HandleCloneLayout(c *gin.Context) {
	layoutID, ok := RequireParam(c, "layoutId")
	if !ok {
		return
	}

	var req struct {
		LayoutName string `json:"layoutName"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || req.LayoutName == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "layoutName is required"))
		return
	}

	cloned, err := h.uiService.CloneLayout(c.Request.Context(), layoutID, req.LayoutName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if cloned == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Source layout not found"))
		return
	}

	c.JSON(http.StatusCreated, cloned)
}

// HandleRestoreLayout handles POST /ui-api/layout/:layoutId/restore/:versionNumber
func (h *UIAPIHandler) HandleRestoreLayout(c *gin.Context) {
	layoutID := c.Param("layoutId")
	versionStr := c.Param("versionNumber")
	if layoutID == "" || versionStr == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "layoutId and versionNumber are required"))
		return
	}

	versionNumber, err := strconv.Atoi(versionStr)
	if err != nil || versionNumber < 1 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "versionNumber must be a positive integer"))
		return
	}

	if err := h.uiService.RestoreLayout(c.Request.Context(), layoutID, versionNumber); err != nil {
		if strings.Contains(err.Error(), "not found") {
			c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, err.Error()))
			return
		}
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.JSON(http.StatusOK, gin.H{"success": true, "restoredVersion": versionNumber})
}

// --- Layout Assignment CRUD ---

// HandleListAssignments handles GET /ui-api/layout-assignments/:objectApiName
func (h *UIAPIHandler) HandleListAssignments(c *gin.Context) {
	objectApiName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	assignments, err := h.uiService.GetAssignmentsForObject(c.Request.Context(), objectApiName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if assignments == nil {
		assignments = []*models.LayoutAssignment{}
	}
	response := make([]layoutAssignmentResponse, 0, len(assignments))
	for _, assignment := range assignments {
		response = append(response, newLayoutAssignmentResponse(assignment))
	}
	c.JSON(http.StatusOK, response)
}

// HandleCreateAssignment handles POST /ui-api/layout-assignment
func (h *UIAPIHandler) HandleCreateAssignment(c *gin.Context) {
	var req struct {
		ObjectAPIName string  `json:"objectApiName"`
		LayoutID      string  `json:"layoutId"`
		RoleID        *string `json:"roleId,omitempty"`
		ProfileID     *string `json:"profileId,omitempty"`
		RecordTypeID  *string `json:"recordTypeId,omitempty"`
		RouteSource   *string `json:"routeSource,omitempty"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid assignment JSON"))
		return
	}

	if req.RouteSource != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "routeSource is internal and may not be supplied"))
		return
	}

	assignment := models.LayoutAssignment{
		ObjectAPIName: req.ObjectAPIName,
		LayoutID:      req.LayoutID,
		RoleID:        req.RoleID,
		ProfileID:     req.ProfileID,
		RecordTypeID:  req.RecordTypeID,
	}

	if assignment.ObjectAPIName == "" || assignment.LayoutID == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "objectApiName and layoutId are required"))
		return
	}

	// Normalize empty strings to nil (Finding 7: prevent phantom assignments)
	assignment.Normalize()

	// Validate that referenced layout exists (Finding 8)
	layout, err := h.uiService.GetLayoutMetadata(c.Request.Context(), assignment.LayoutID)
	if err != nil || layout == nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Referenced layoutId does not exist"))
		return
	}

	// Set CreatedByID from auth context (Finding 10: prevent spoofing)
	user := MustGetUser(c)
	assignment.CreatedByID = &user.ID

	if err := h.uiService.CreateAssignment(c.Request.Context(), &assignment); err != nil {
		if strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "duplicate") {
			c.JSON(http.StatusConflict, apierr.SFErrorResponse("DUPLICATE_VALUE",
				"An assignment with these dimensions already exists"))
			return
		}
		var sfErr *apierr.SalesforceAPIError
		if stderrors.As(err, &sfErr) {
			c.JSON(sfErr.Status, []apierr.SFError{sfErr.Err})
			return
		}
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to create assignment"))
		return
	}

	c.JSON(http.StatusCreated, newLayoutAssignmentResponse(&assignment))
}

// HandleDeleteAssignment handles DELETE /ui-api/layout-assignment/:assignmentId
func (h *UIAPIHandler) HandleDeleteAssignment(c *gin.Context) {
	assignmentID, ok := RequireParam(c, "assignmentId")
	if !ok {
		return
	}

	if err := h.uiService.DeleteAssignment(c.Request.Context(), assignmentID); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to delete assignment"))
		return
	}

	c.Status(http.StatusNoContent)
}

// HandleRecordDefaultsCreate handles GET /ui-api/record-defaults/create/:objectApiName
// Returns default field values for creating a new record (Salesforce v63.0)
func (h *UIAPIHandler) HandleRecordDefaultsCreate(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}

	user := MustGetUser(c)

	// Optional record type ID
	recordTypeId := c.Query("recordTypeId")

	defaults, err := h.uiService.GetRecordDefaults(c.Request.Context(), objectName, recordTypeId, user)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	c.JSON(http.StatusOK, defaults)
}

// HandleRecordDefaultsClone handles GET /ui-api/record-defaults/clone/:recordId
// Returns default field values for cloning an existing record (Salesforce v63.0)
func (h *UIAPIHandler) HandleRecordDefaultsClone(c *gin.Context) {
	recordId, ok := RequireParam(c, "recordId")
	if !ok {
		return
	}

	user := MustGetUser(c)
	typeHint := c.Query("type")

	defaults, err := h.uiService.GetCloneDefaults(c.Request.Context(), recordId, typeHint, user)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	c.JSON(http.StatusOK, defaults)
}

// HandleListInfo handles GET /ui-api/list-info/:objectApiName/:listViewApiName
// Returns list view metadata including columns and filters (Salesforce v63.0)
func (h *UIAPIHandler) HandleListInfo(c *gin.Context) {
	objectName, ok := RequireParam(c, "objectApiName")
	if !ok {
		return
	}
	listViewName, ok := RequireParam(c, "listViewApiName")
	if !ok {
		return
	}

	user := MustGetUser(c)

	listInfo, err := h.uiService.GetListInfo(c.Request.Context(), objectName, listViewName, user)
	if err != nil {
		writeUIAPIError(c, err)
		return
	}

	c.JSON(http.StatusOK, listInfo)
}
