package rest

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/audit"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/backend/pkg/soql"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

type QueryHandler struct {
	svcMgr *services.ServiceManager
}

func NewQueryHandler(svcMgr *services.ServiceManager) *QueryHandler {
	return &QueryHandler{svcMgr: svcMgr}
}

// ExecuteQuery handles GET /query
func (h *QueryHandler) ExecuteQuery(c *gin.Context) {
	h.executeQueryInternal(c, false)
}

// ExecuteQueryAll handles GET /queryAll (Include Deleted)
func (h *QueryHandler) ExecuteQueryAll(c *gin.Context) {
	h.executeQueryInternal(c, true)
}

func (h *QueryHandler) executeQueryInternal(c *gin.Context, scanAll bool) {
	log.Debug().Str("url", c.Request.URL.String()).Str("raw_query", c.Request.URL.RawQuery).Msg("executeQueryInternal ENTER")
	q := c.Query("q")
	log.Debug().Str("query_param_q", q).Msg("Captured q")
	if q == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedQuery, "query string has to be specified"))
		return
	}

	// Parse SOQL
	req, err := soql.ParseSOQL(q)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedQuery, normalizeSOQLParseError(err)))
		return
	}

	// C11 §5.1: <Object>History is a virtual view over _System_AuditLog.
	// Short-circuit the normal query pipeline so the dispatcher never
	// tries to read the non-existent AccountHistory / LeadHistory table.
	if audit.IsHistoryObject(req.ObjectAPIName) {
		h.executeHistoryQuery(c, req)
		return
	}

	// Set ScanAll
	req.ScanAll = scanAll

	// Override params
	offsetParam := c.Query("offset")
	if offsetParam != "" {
		var off int
		fmt.Sscanf(offsetParam, "%d", &off)
		req.Offset = off
	}
	limitParam := c.Query("limit")
	if limitParam != "" {
		var lim int
		fmt.Sscanf(limitParam, "%d", &lim)
		req.Limit = lim
	}

	// Execute via QueryService
	apiReq := api.QueryRequest{
		ObjectAPIName: req.ObjectAPIName,
		FilterExpr:    req.FilterExpr,
		SortField:     req.SortField,
		SortDirection: req.SortDirection,
		Limit:         req.Limit,
		Offset:        req.Offset,
		ScanAll:       req.ScanAll,
		SelectFields:  req.SelectFields,
		GroupBy:       req.GroupBy,
		Having:        req.Having,
	}
	// Map SubQueries from parsed SOQL to API DTO
	if len(req.SubQueries) > 0 {
		apiReq.SubQueries = make([]api.SubQueryRequest, len(req.SubQueries))
		for i, sq := range req.SubQueries {
			apiSQ := api.SubQueryRequest{
				RelationshipName: sq.RelationshipName,
				SelectFields:     sq.SelectFields,
				FilterExpr:       sq.FilterExpr,
				Limit:            sq.Limit,
			}
			if len(sq.OrderBy) > 0 {
				apiSQ.OrderBy = make([]api.SortCriterion, len(sq.OrderBy))
				for j, o := range sq.OrderBy {
					apiSQ.OrderBy[j] = api.SortCriterion{Field: o.Field, Direction: o.Direction}
				}
			}
			apiReq.SubQueries[i] = apiSQ
		}
	}

	user := MustGetUser(c)

	queryResult, err := h.svcMgr.QuerySvc.Query(c.Request.Context(), apiReq, user)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidQuery, err.Error()))
		return
	}

	// Format Response
	formattedRecords := make([]interface{}, len(queryResult.Records))
	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), req.ObjectAPIName)
	h.hydrateQueryLookupsBatch(c.Request.Context(), schema, queryResult.Records, user)
	if strings.EqualFold(req.ObjectAPIName, "CampaignMember") {
		enrichCampaignMemberRecords(c.Request.Context(), h.svcMgr, queryResult.Records, user)
	}
	for i, r := range queryResult.Records {
		nestRelationshipFields(schema, r)
		attrs := gin.H{
			"type": req.ObjectAPIName,
			"url":  fmt.Sprintf("/services/data/v63.0/sobjects/%s/%s", req.ObjectAPIName, r["Id"]),
		}
		r["attributes"] = attrs
		formattedRecords[i] = r
	}

	response := gin.H{
		"totalSize": queryResult.TotalSize,
		"done":      queryResult.Done,
		"records":   formattedRecords,
	}

	if !queryResult.Done {
		// Generate nextRecordsUrl using base64 locator
		limit := req.Limit
		if limit == 0 {
			limit = 500 // Default batch size
		}
		nextOffset := req.Offset + limit

		// Create locator: base64 encoded JSON with query state
		locatorData := fmt.Sprintf(`{"q":"%s","offset":%d,"limit":%d,"scanAll":%t}`,
			url.QueryEscape(q), nextOffset, limit, scanAll)
		locator := base64.URLEncoding.EncodeToString([]byte(locatorData))

		ver := c.Param("version")
		if ver == "" {
			ver = "v63.0"
		}
		response["nextRecordsUrl"] = fmt.Sprintf("/services/data/%s/query/%s", ver, locator)
	}

	c.JSON(http.StatusOK, response)
}

// QueryMore handles GET /query/:locator - continues a paginated query
func (h *QueryHandler) QueryMore(c *gin.Context) {
	locator := c.Param("locator")
	if locator == "" {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidQueryLocator, "invalid query locator"))
		return
	}

	// Decode locator
	locatorBytes, err := base64.URLEncoding.DecodeString(locator)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidQueryLocator, "bad id"))
		return
	}

	// Parse locator JSON
	var locatorData struct {
		Q       string `json:"q"`
		Offset  int    `json:"offset"`
		Limit   int    `json:"limit"`
		ScanAll bool   `json:"scanAll"`
	}
	if err := json.Unmarshal(locatorBytes, &locatorData); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidQueryLocator, "invalid query locator"))
		return
	}

	// URL decode the query
	q, err := url.QueryUnescape(locatorData.Q)
	if err != nil {
		q = locatorData.Q // Fall back to original
	}

	// Parse SOQL
	req, err := soql.ParseSOQL(q)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeMalformedQuery, normalizeSOQLParseError(err)))
		return
	}

	// Apply locator state
	req.Offset = locatorData.Offset
	req.Limit = locatorData.Limit
	req.ScanAll = locatorData.ScanAll

	// Execute via QueryService
	apiReq := api.QueryRequest{
		ObjectAPIName: req.ObjectAPIName,
		SelectFields:  req.SelectFields,
		FilterExpr:    req.FilterExpr,
		GroupBy:       req.GroupBy,
		Having:        req.Having,
		SortField:     req.SortField,
		SortDirection: req.SortDirection,
		Limit:         req.Limit,
		Offset:        req.Offset,
		ScanAll:       req.ScanAll,
	}
	// Map SubQueries from parsed SOQL to API DTO
	if len(req.SubQueries) > 0 {
		apiReq.SubQueries = make([]api.SubQueryRequest, len(req.SubQueries))
		for i, sq := range req.SubQueries {
			apiSQ := api.SubQueryRequest{
				RelationshipName: sq.RelationshipName,
				SelectFields:     sq.SelectFields,
				FilterExpr:       sq.FilterExpr,
				Limit:            sq.Limit,
			}
			if len(sq.OrderBy) > 0 {
				apiSQ.OrderBy = make([]api.SortCriterion, len(sq.OrderBy))
				for j, o := range sq.OrderBy {
					apiSQ.OrderBy[j] = api.SortCriterion{Field: o.Field, Direction: o.Direction}
				}
			}
			apiReq.SubQueries[i] = apiSQ
		}
	}

	user := MustGetUser(c)

	queryResult, err := h.svcMgr.QuerySvc.Query(c.Request.Context(), apiReq, user)
	if err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidQuery, err.Error()))
		return
	}

	// Format Response
	formattedRecords := make([]interface{}, len(queryResult.Records))
	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), req.ObjectAPIName)
	h.hydrateQueryLookupsBatch(c.Request.Context(), schema, queryResult.Records, user)
	if strings.EqualFold(req.ObjectAPIName, "CampaignMember") {
		enrichCampaignMemberRecords(c.Request.Context(), h.svcMgr, queryResult.Records, user)
	}
	for i, r := range queryResult.Records {
		nestRelationshipFields(schema, r)
		attrs := gin.H{
			"type": req.ObjectAPIName,
			"url":  fmt.Sprintf("/services/data/v63.0/sobjects/%s/%s", req.ObjectAPIName, r["Id"]),
		}
		r["attributes"] = attrs
		formattedRecords[i] = r
	}

	response := gin.H{
		"totalSize": queryResult.TotalSize,
		"done":      queryResult.Done,
		"records":   formattedRecords,
	}

	if !queryResult.Done {
		// Generate next locator
		limit := req.Limit
		if limit == 0 {
			limit = 500
		}
		nextOffset := req.Offset + limit

		nextLocatorData := fmt.Sprintf(`{"q":"%s","offset":%d,"limit":%d,"scanAll":%t}`,
			url.QueryEscape(q), nextOffset, limit, req.ScanAll)
		nextLocator := base64.URLEncoding.EncodeToString([]byte(nextLocatorData))

		ver := c.Param("version")
		if ver == "" {
			ver = "v63.0"
		}
		response["nextRecordsUrl"] = fmt.Sprintf("/services/data/%s/query/%s", ver, nextLocator)
	}

	c.JSON(http.StatusOK, response)
}

// hydrateQueryLookupsBatch resolves lookup fields for all records in batch,
// using one GetMany call per referenced object type instead of one Get per field per record.
func (h *QueryHandler) hydrateQueryLookupsBatch(ctx context.Context, schema *models.ObjectMetadata, records []models.SObject, user *models.UserSession) {
	if schema == nil || len(records) == 0 {
		return
	}

	// Collect all unique (targetObject, id) pairs across all records
	// Key: target object name, Value: set of IDs to fetch
	idsByObject := make(map[string]map[string]struct{})

	type lookupRef struct {
		fieldAPIName string
		targetObj    string
		recordIdx    int
		idStr        string
	}
	var refs []lookupRef

	for i, record := range records {
		if record == nil {
			continue
		}
		for _, f := range schema.Fields {
			if !constants.IsReferenceType(f.Type) || len(f.ReferenceTo) == 0 {
				continue
			}

			rawID, ok := record[f.APIName]
			if !ok || rawID == nil {
				continue
			}

			idStr, ok := rawID.(string)
			if !ok || idStr == "" {
				continue
			}

			if _, already := record[f.APIName+"_Name"]; already {
				continue
			}

			targetObj := f.ReferenceTo[0]
			if f.IsPolymorphic && f.APIName == constants.FieldOwnerID {
				targetObj = constants.TableUser
			}

			if idsByObject[targetObj] == nil {
				idsByObject[targetObj] = make(map[string]struct{})
			}
			idsByObject[targetObj][idStr] = struct{}{}

			refs = append(refs, lookupRef{
				fieldAPIName: f.APIName,
				targetObj:    targetObj,
				recordIdx:    i,
				idStr:        idStr,
			})
		}
	}

	// Batch fetch: one GetMany call per object type
	fetchedByObject := make(map[string]map[string]models.SObject)
	for objName, idSet := range idsByObject {
		ids := make([]string, 0, len(idSet))
		for id := range idSet {
			ids = append(ids, id)
		}
		result, err := h.svcMgr.Persistence.RetrieveMany(ctx, objName, ids, user)
		if err != nil {
			continue
		}
		fetchedByObject[objName] = result
	}

	// Distribute results back to records
	for _, ref := range refs {
		objResults, ok := fetchedByObject[ref.targetObj]
		if !ok {
			continue
		}
		targetRecord, ok := objResults[ref.idStr]
		if !ok || targetRecord == nil {
			continue
		}

		record := records[ref.recordIdx]
		if displayName := auth.ResolveLookupDisplayName(ref.targetObj, targetRecord); displayName != "" {
			record[ref.fieldAPIName+"_Name"] = displayName
		}
	}
}

func normalizeSOQLParseError(err error) string {
	if err == nil {
		return ""
	}

	message := err.Error()
	if strings.Contains(strings.ToLower(message), "unexpected keyword") {
		return "unexpected token"
	}

	return message
}

// nestRelationshipFields converts flat _Name suffix fields into Salesforce-style
// nested relationship objects. E.g., OwnerId="005xx" + OwnerId_Name="John" becomes
// Owner: { Id: "005xx", Name: "John", attributes: { type: "User" } }.
// This matches the Salesforce SOQL response format that the frontend expects.
func nestRelationshipFields(schema *models.ObjectMetadata, record models.SObject) {
	if schema == nil || record == nil {
		return
	}

	for _, f := range schema.Fields {
		if !constants.IsReferenceType(f.Type) || len(f.ReferenceTo) == 0 {
			continue
		}

		nameKey := f.APIName + "_Name"
		nameVal, hasName := record[nameKey]
		if !hasName {
			continue
		}

		// Derive the relationship name: strip "Id" suffix from field name
		// E.g., "OwnerId" → "Owner", "AccountId" → "Account", "CreatedById" → "CreatedBy"
		relName := f.APIName
		if f.RelationshipName != nil && *f.RelationshipName != "" {
			relName = *f.RelationshipName
		} else if strings.HasSuffix(relName, "Id") {
			relName = strings.TrimSuffix(relName, "Id")
		}

		// Build nested object
		nested := map[string]interface{}{
			"attributes": map[string]interface{}{
				"type": f.ReferenceTo[0],
			},
		}
		if id, ok := record[f.APIName].(string); ok {
			nested["Id"] = id
		}
		if name, ok := nameVal.(string); ok {
			nested["Name"] = name
		}

		record[relName] = nested

		// Keep the flat _Name key for backward compatibility — some frontend
		// consumers (usePermissionAnalyzer, UIRegistry LookupRenderer) read
		// _Name keys directly from raw SOQL responses.
	}
}

// BatchRelatedListCounts handles POST /ui-api/related-list-counts
// Accepts an array of {objectApiName, lookupField, parentRecordId} and returns
// counts in a single HTTP round-trip instead of N independent count queries.
func (h *QueryHandler) BatchRelatedListCounts(c *gin.Context) {
	type countRequest struct {
		ObjectApiName    string `json:"objectApiName"`
		LookupField      string `json:"lookupField"`
		ParentRecordId   string `json:"parentRecordId"`
		ParentFieldValue string `json:"parentFieldValue"`
	}
	type countResponse struct {
		ObjectApiName string `json:"objectApiName"`
		LookupField   string `json:"lookupField"`
		Count         int    `json:"count"`
		Error         string `json:"error,omitempty"`
	}

	var requests []countRequest
	if err := c.ShouldBindJSON(&requests); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}

	if len(requests) > 50 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "max 50 count requests per batch"})
		return
	}

	user := MustGetUser(c)

	// Execute all counts in parallel with bounded concurrency (max 8 workers)
	results := make([]countResponse, len(requests))
	type indexedResult struct {
		idx   int
		count int
		err   error
	}
	ch := make(chan indexedResult, len(requests))
	sem := make(chan struct{}, 8) // limit concurrent DB queries per batch request

	for i, req := range requests {
		go func(idx int, r countRequest) {
			sem <- struct{}{}        // acquire
			defer func() { <-sem }() // release
			defer func() {
				if p := recover(); p != nil {
					ch <- indexedResult{idx: idx, err: fmt.Errorf("internal error")}
				}
			}()

			filterValue := r.ParentFieldValue
			if filterValue == "" {
				filterValue = r.ParentRecordId
			}

			apiReq := api.QueryRequest{
				ObjectAPIName: r.ObjectApiName,
				FilterExpr:    fmt.Sprintf("%s = '%s'", r.LookupField, strings.ReplaceAll(filterValue, "'", "\\'")),
				SelectFields:  []string{"Id"},
				Limit:         1,
			}

			queryResult, err := h.svcMgr.QuerySvc.Query(c.Request.Context(), apiReq, user)
			if err != nil {
				ch <- indexedResult{idx: idx, err: err}
				return
			}
			ch <- indexedResult{idx: idx, count: queryResult.TotalSize}
		}(i, req)
	}

	for range requests {
		r := <-ch
		results[r.idx] = countResponse{
			ObjectApiName: requests[r.idx].ObjectApiName,
			LookupField:   requests[r.idx].LookupField,
			Count:         r.count,
		}
		if r.err != nil {
			results[r.idx].Error = r.err.Error()
		}
	}

	c.JSON(http.StatusOK, gin.H{"counts": results})
}
