package rest

import (
	"context"
	"fmt"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/pkg/apierr"
	appErrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/fieldtypes"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

// SearchSOSL handles GET /services/data/vXX.X/search?q=FIND...
// @Summary      Execute SOSL Search
// @Description  Execute a SOSL search query using FIND syntax
// @Tags         sosl
// @Produce      json
// @Param        q query string true "SOSL Query (e.g. FIND {term} IN ALL FIELDS RETURNING Account)"
// @Success      200  {object}  map[string]interface{}
// @Success      200  {object}  map[string]interface{}
// @Failure      400  {object}  map[string]interface{}
// @Router       /search [get]
// @Security     BearerAuth
func (h *SearchHandler) SearchSOSL(c *gin.Context) {
	query := c.Query("q")
	user := MustGetUser(c)

	if query == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedSearch, "q parameter is required"))
		return
	}

	// Parse SOSL query: FIND {term} [IN {scope}] [RETURNING {objects}]
	searchTerm, scope, objectConfigs, err := parseSOSL(query)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedSearch, err.Error()))
		return
	}

	// If no RETURNING clause, use searchable objects from metadata (dynamic, no hardcoding)
	if len(objectConfigs) == 0 {
		defaults := h.svcMgr.Metadata.GetSearchableObjects(c.Request.Context())
		for _, obj := range defaults {
			objectConfigs = append(objectConfigs, searchObjectConfig{Name: obj, Limit: constants.DefaultSOSLLimit})
		}
	}

	// Execute Search using unified helper (Strict Limit Support)
	// SOSL Global Limit is not yet parsed, defaulting max to protocol limit
	searchResults := h.executeSearchWithLimits(c, searchTerm, scope, objectConfigs, constants.MaxSOSLLimit, user)

	c.JSON(http.StatusOK, gin.H{
		"searchRecords": searchResults,
	})
}

// parseSOSL parses a SOSL query and extracts search term and object-to-fields mapping
// SOSL format: FIND {term} [IN ALL FIELDS] [RETURNING Object1(Field1, Field2 LIMIT n), Object2, ...]
// Returns: searchTerm, scope, []searchObjectConfig, error
func parseSOSL(query string) (string, string, []searchObjectConfig, error) {
	query = strings.TrimSpace(query)

	// Check for FIND keyword (case-insensitive) before parsing braces so malformed
	// non-SOSL input returns the more specific protocol error.
	upperQuery := strings.ToUpper(query)
	if !strings.HasPrefix(upperQuery, "FIND") {
		return "", "", nil, appErrors.NewValidationError("query", "SOSL query must start with FIND")
	}

	// Extract search term between braces: FIND {term}
	start := strings.Index(query, "{")
	end := strings.Index(query, "}")
	if start == -1 || end == -1 || end <= start {
		return "", "", nil, appErrors.NewValidationError("query", "Search term must be enclosed in braces: FIND {term}")
	}

	searchTerm := strings.TrimSpace(query[start+1 : end])
	if searchTerm == "" {
		return "", "", nil, appErrors.NewValidationError("query", "Search term cannot be empty")
	}

	// Sanitize search term
	searchTerm = strings.ReplaceAll(searchTerm, "'", "")
	searchTerm = strings.ReplaceAll(searchTerm, "\"", "")
	searchTerm = strings.ReplaceAll(searchTerm, ";", "")
	searchTerm = strings.ReplaceAll(searchTerm, "\\", "")

	remaining := query[end+1:]
	scope := "ALL" // Default scope

	// Parse IN ... FIELDS
	upperRemaining := strings.ToUpper(remaining)
	inIdx := strings.Index(upperRemaining, " IN ")
	returningIdx := strings.Index(upperRemaining, " RETURNING ")

	// If IN clause exists and is before RETURNING (or RETURNING doesn't exist)
	if inIdx != -1 && (returningIdx == -1 || inIdx < returningIdx) {
		// Extract content between IN and FIELDS
		// e.g. " IN ALL FIELDS" or " IN NAME FIELDS "
		scopePart := upperRemaining[inIdx+4:] // After " IN "
		fieldsIdx := strings.Index(scopePart, " FIELDS")
		if fieldsIdx != -1 {
			extractedScope := strings.TrimSpace(scopePart[:fieldsIdx])
			if extractedScope == "ALL" || extractedScope == "NAME" || extractedScope == "EMAIL" || extractedScope == "PHONE" {
				scope = extractedScope
			}
			// Update remaining to strip the IN clause for RETURNING parser
			// We need to be careful with indices since we looked at upperRemaining
			// The length of " IN " + scope + " FIELDS"
			clauseLen := 4 + fieldsIdx + 7
			// Slice remaining from where FIELDS ends
			remaining = remaining[inIdx+clauseLen:]
		}
	}

	// Default objects if RETURNING not specified - will be populated by caller with searchable objects
	// Note: parseSOSL returns nil configs if no RETURNING, caller should use GetSearchableObjects()
	configs := []searchObjectConfig{}

	// Look for RETURNING clause
	returningIdx = strings.Index(strings.ToUpper(remaining), "RETURNING")
	if returningIdx != -1 {
		objectsPart := remaining[returningIdx+9:] // After "RETURNING"
		objectsPart = strings.TrimSpace(objectsPart)

		// Reset to build from RETURNING clause
		configs = make([]searchObjectConfig, 0)

		// Parse RETURNING clause: Account(Id, Name LIMIT 10), Contact(Id)
		currentObj := ""
		inParens := false
		fieldBuffer := ""
		objBuffer := ""

		for i := 0; i < len(objectsPart); i++ {
			c := objectsPart[i]

			if c == '(' {
				inParens = true
				currentObj = strings.TrimSpace(objBuffer)
				objBuffer = ""
				fieldBuffer = ""
			} else if c == ')' {
				inParens = false
				if currentObj != "" {
					var fields []string
					var limit, offset int
					var filter, orderBy, orderDir string
					fields, limit, offset, orderBy, orderDir, filter = parseFieldListWithOptions(fieldBuffer)
					configs = append(configs, searchObjectConfig{
						Name:     currentObj,
						Fields:   fields,
						Limit:    limit,
						Offset:   offset,
						OrderBy:  orderBy,
						OrderDir: orderDir,
						Filter:   filter,
					})
				}
				currentObj = ""
				fieldBuffer = ""
			} else if c == ',' && !inParens {
				if objBuffer != "" {
					objName := strings.TrimSpace(objBuffer)
					if objName != "" {
						// Comma ends previous object when no parens were used
						configs = append(configs, searchObjectConfig{
							Name:  objName,
							Limit: constants.DefaultSOSLLimit, // Default
						})
					}
				}
				objBuffer = ""
			} else if inParens {
				fieldBuffer += string(c)
			} else {
				objBuffer += string(c)
			}
		}

		// Handle trailing object
		if objBuffer != "" {
			objName := strings.TrimSpace(objBuffer)
			if objName != "" {
				configs = append(configs, searchObjectConfig{
					Name:  objName,
					Limit: constants.DefaultSOSLLimit,
				})
			}
		}
	}

	// Note: Empty configs is acceptable - caller will populate from metadata if needed
	return searchTerm, scope, configs, nil
}

// parseFieldListWithOptions parses fields, optional WHERE, ORDER BY, LIMIT, OFFSET
func parseFieldListWithOptions(content string) ([]string, int, int, string, string, string) {
	content = strings.TrimSpace(content)
	limit := constants.DefaultSOSLLimit // Default
	offset := 0
	orderBy := ""
	orderDir := ""
	filter := ""

	upperContent := strings.ToUpper(content)

	// 1. Extract OFFSET (Last clause typically)
	// Pattern: OFFSET <number>
	offsetRegex := regexp.MustCompile(`(?i)\s+OFFSET\s+(\d+)$`)
	if match := offsetRegex.FindStringSubmatch(content); match != nil {
		if val, err := strconv.Atoi(match[1]); err == nil {
			offset = val
		}
		// Remove offset from content
		loc := offsetRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	// 2. Extract LIMIT (After ORDER BY usually)
	// Pattern: LIMIT <number>
	limitRegex := regexp.MustCompile(`(?i)\s+LIMIT\s+(\d+)$`)
	if match := limitRegex.FindStringSubmatch(content); match != nil {
		if val, err := strconv.Atoi(match[1]); err == nil {
			limit = val
			if limit <= 0 {
				limit = constants.DefaultSOSLLimit
			}
		}
		loc := limitRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	// 3. Extract ORDER BY
	// Pattern: ORDER BY <field> [ASC|DESC]
	orderByRegex := regexp.MustCompile(`(?i)\s+ORDER\s+BY\s+([a-zA-Z0-9_\.]+)(\s+(ASC|DESC))?$`)
	if match := orderByRegex.FindStringSubmatch(content); match != nil {
		orderBy = match[1]
		if len(match) > 3 && match[3] != "" {
			orderDir = match[3]
		}
		loc := orderByRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	// 4. Extract WHERE (Before ORDER BY usually)
	// Pattern: WHERE <expr>
	// Since WHERE can contain keywords, we assume WHERE starts the clause and goes until end (which is now stripped of sort/limit)
	whereIdx := strings.LastIndex(upperContent, " WHERE ")
	if whereIdx != -1 {
		filter = strings.TrimSpace(content[whereIdx+7:])
		content = content[:whereIdx]
	}

	fields := []string{}
	if content != "" {
		for _, f := range strings.Split(content, ",") {
			field := strings.TrimSpace(f)
			if field != "" {
				fields = append(fields, field)
			}
		}
	}
	return fields, limit, offset, orderBy, orderDir, filter
}

// executeSearchWithLimits is a helper that respects per-object and overall limits.
// Returns a flat array of SObjects with attributes (Salesforce SOSL wire format).
func (h *SearchHandler) executeSearchWithLimits(c *gin.Context, term string, scope string, objectConfigs []searchObjectConfig, overallLimit int, user *models.UserSession) []models.SObject {
	flatResults := make([]models.SObject, 0)
	totalCount := 0

	// Get API version from path (dynamic)
	version := c.Param("version")
	if version == "" {
		version = "v63.0"
	}

	for _, config := range objectConfigs {
		if totalCount >= overallLimit {
			break
		}

		schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), config.Name)
		if schema == nil {
			log.Warn().Str("object", config.Name).Msg("[Search Debug] Skipping object due to missing schema")
			continue
		}
		if !schema.Searchable {
			log.Debug().Str("object", config.Name).Msg("[Search Debug] Skipping non-searchable object")
			continue
		}

		canRead := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), config.Name, constants.PermRead, user)
		if !canRead {
			log.Warn().Str("object", config.Name).Msg("[Search Debug] Skipping object due to permissions")
			continue
		}

		// Execute search on this object

		// Resolve Search Fields based on Scope (Strict Mode)
		searchFields := buildSOSLSearchFields(schema, scope)

		// Apply field-level read permissions: compound-name expansion adds
		// member fields (e.g. Salutation/FirstName/LastName) from metadata,
		// and FieldPermissions is the SF-modeled SSoT for read access. An
		// admin who hides FirstName must not have it returned by a Name-scope
		// search (Ian P0 on PR #1963 / issue #1455).
		searchFields = filterFieldsByVisibility(c.Request.Context(), h.svcMgr, config.Name, searchFields, user)

		if len(searchFields) == 0 {
			continue
		}

		// Determine fields to Select
		// If empty, QueryService defaults to Id + NameField.
		// We want to ensure we get the Name Field value for normalization.
		returnFields := config.Fields

		// Execute Search
		records, err := h.svcMgr.QuerySvc.SearchSingleObject(
			c.Request.Context(),
			config.Name,
			term,
			searchFields,
			returnFields,
			config.Limit,
			config.Offset,
			config.OrderBy,
			config.OrderDir,
			config.Filter,
			user,
		)
		if err != nil {
			log.Warn().Err(err).Str("object", config.Name).Msg("Search failed for object")
			continue
		}

		// Apply per-object limit
		objectLimit := config.Limit
		if objectLimit <= 0 {
			objectLimit = constants.DefaultSOSLLimit
		}

		// Collect records into flat array (Salesforce wire format)
		for i, record := range records {
			if i >= objectLimit || totalCount >= overallLimit {
				break
			}

			normalizeNameField(record, schema)

			// Get record ID
			recordID := record["Id"]

			record["attributes"] = map[string]interface{}{
				"type": config.Name,
				"url":  fmt.Sprintf("/services/data/%s/sobjects/%s/%v", version, config.Name, recordID),
			}
			flatResults = append(flatResults, record)
			totalCount++
		}
	}
	return flatResults
}

func buildSOSLSearchFields(schema *models.ObjectMetadata, scope string) []string {
	if scope != "ALL" {
		fields := make([]string, 0)
		seen := make(map[string]struct{})
		addScoped := func(apiName string) {
			if apiName == "" {
				return
			}
			if _, ok := seen[apiName]; ok {
				return
			}
			seen[apiName] = struct{}{}
			fields = append(fields, apiName)
		}
		for _, field := range schema.Fields {
			switch scope {
			case "NAME":
				if field.IsNameField || strings.EqualFold(field.APIName, "Name") || strings.EqualFold(field.APIName, "CaseNumber") {
					addScoped(field.APIName)
					addCompoundMembers(schema, field.APIName, addScoped)
				}
			case "EMAIL":
				if strings.EqualFold(string(field.Type), string(constants.FieldTypeEmail)) {
					addScoped(field.APIName)
				}
			case "PHONE":
				if strings.EqualFold(string(field.Type), string(constants.FieldTypePhone)) {
					addScoped(field.APIName)
				}
			}
		}
		return fields
	}

	fields := make([]string, 0)
	seen := make(map[string]struct{})
	add := func(apiName string) {
		if apiName == "" {
			return
		}
		field, ok := findSchemaField(schema, apiName)
		if !ok || !isMetadataSearchableField(field) {
			return
		}
		if _, ok := seen[apiName]; ok {
			return
		}
		seen[apiName] = struct{}{}
		fields = append(fields, apiName)
	}

	for _, field := range schema.Fields {
		if field.IsNameField || strings.EqualFold(field.APIName, constants.FieldName) {
			add(field.APIName)
			addCompoundMembers(schema, field.APIName, add)
		}
	}

	for _, apiName := range schema.ListFields {
		add(apiName)
	}

	if len(fields) == 0 {
		for _, col := range buildSearchColumns(schema) {
			add(col.Name)
		}
	}

	if len(fields) == 0 {
		for _, field := range schema.Fields {
			add(field.APIName)
		}
	}

	return fields
}

// filterFieldsByVisibility drops fields the user cannot read per
// FieldPermissions. Used after compound-name expansion to ensure admin-hidden
// member fields (e.g. FirstName/LastName when only the compound `Name` is
// permitted) are not searched or returned. SystemContext / scoped internal
// reads bypass this — they intentionally see all fields except
// security-sensitive ones, mirroring `analytics.QueryService.Query`'s
// `bypassUserVisibility` policy.
func filterFieldsByVisibility(ctx context.Context, svc *services.ServiceManager, objectAPIName string, fields []string, user *models.UserSession) []string {
	if svc == nil || svc.Permissions == nil || len(fields) == 0 {
		return fields
	}
	bypass := constants.GetSecurityContext(ctx) >= constants.SystemContext ||
		constants.IsScopedInternalAccess(ctx, objectAPIName, constants.PermRead)
	out := fields[:0]
	for _, f := range fields {
		if bypass {
			out = append(out, f)
			continue
		}
		if svc.Permissions.CheckFieldVisibilityWithUser(ctx, objectAPIName, f, user) {
			out = append(out, f)
		}
	}
	return out
}

func addCompoundMembers(schema *models.ObjectMetadata, apiName string, add func(string)) {
	if schema == nil || add == nil {
		return
	}
	// Single SSoT for compound-shape expansion across REST search and
	// analytics return-field expansion (Ian's R10/R11 P0 directive on
	// PR #1963 / issue #1455). The helper reads
	// `fieldprovenance.ClassifyCompound` first, falls back to the
	// metadata-driven `packspec.CompoundFieldComponents`, and filters to
	// schema-existing members.
	isCompound, members := metadata.ExpandCompoundFieldMembers(schema, apiName)
	if !isCompound {
		return
	}
	for _, member := range members {
		add(member)
	}
}

func findSchemaField(schema *models.ObjectMetadata, apiName string) (models.FieldMetadata, bool) {
	if schema == nil {
		return models.FieldMetadata{}, false
	}
	for _, field := range schema.Fields {
		if strings.EqualFold(field.APIName, apiName) {
			return field, true
		}
	}
	return models.FieldMetadata{}, false
}

func isMetadataSearchableField(field models.FieldMetadata) bool {
	effectiveType := field.Type
	if field.LogicalType != nil {
		effectiveType = *field.LogicalType
	}
	if effectiveType == constants.FieldTypeFormula || effectiveType == constants.FieldTypeRollupSummary {
		return false
	}
	return fieldtypes.GetRegistry().IsSearchable(string(effectiveType))
}

// normalizeNameField populates the "Name" key on a search result record
// using the schema's IsNameField markers. Objects like User have no physical
// Name column — their name fields are Username/Email. When the RETURNING
// clause only selects a subset of fields, the first IsNameField may not be
// present in the record. We iterate ALL IsNameField entries to find one
// that exists in the record, falling back to Id.
//
// For objects with FirstName/LastName (User, Contact, Lead), a compound
// name is synthesized when both parts are available.
func normalizeNameField(record models.SObject, schema *models.ObjectMetadata) {
	// Try compound name from FirstName + LastName first
	first, _ := record["FirstName"].(string)
	last, _ := record["LastName"].(string)
	if first != "" || last != "" {
		record["Name"] = strings.TrimSpace(first + " " + last)
		return
	}

	// Look for IsNameField markers in schema
	for _, f := range schema.Fields {
		if f.IsNameField {
			if val, ok := record[f.APIName]; ok {
				record["Name"] = val
				return
			}
		}
	}

	// For objects like Product2, also check for a field named "Name"
	for _, f := range schema.Fields {
		if strings.EqualFold(f.APIName, "Name") {
			if val, ok := record[f.APIName]; ok {
				record["Name"] = val
				return
			}
		}
	}

	// If no proper name field was found, fall back to Id
	// Only set Name to Id if it doesn't already exist (avoid overwriting existing Name values)
	if _, hasName := record["Name"]; !hasName {
		record["Name"] = record["Id"]
	}
}
