package funcspec

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/bitechdev/ResolveSpec/pkg/common"
	"github.com/bitechdev/ResolveSpec/pkg/logger"
	"github.com/bitechdev/ResolveSpec/pkg/reflection"
	"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
	"github.com/bitechdev/ResolveSpec/pkg/security"
)

// Handler handles function-based SQL API requests
type Handler struct {
	db                common.Database
	hooks             *HookRegistry
	variablesCallback func(r *http.Request) map[string]interface{}
}

type SqlQueryOptions struct {
	NoCount     bool
	BlankParams bool
	AllowFilter bool
}

func NewSqlQueryOptions() SqlQueryOptions {
	return SqlQueryOptions{
		NoCount:     false,
		BlankParams: true,
		AllowFilter: true,
	}
}

// NewHandler creates a new function API handler
func NewHandler(db common.Database) *Handler {
	return &Handler{
		db:    db,
		hooks: NewHookRegistry(),
	}
}

// GetDatabase returns the underlying database connection
// Implements common.SpecHandler interface
func (h *Handler) GetDatabase() common.Database {
	return h.db
}

func (h *Handler) SetVariablesCallback(callback func(r *http.Request) map[string]interface{}) {
	h.variablesCallback = callback
}

func (h *Handler) GetVariablesCallback() func(r *http.Request) map[string]interface{} {
	return h.variablesCallback
}

// Hooks returns the hook registry for this handler
// Use this to register custom hooks for operations
func (h *Handler) Hooks() *HookRegistry {
	return h.hooks
}

// HTTPFuncType is a function type for HTTP handlers
type HTTPFuncType func(http.ResponseWriter, *http.Request)

// SqlQueryList creates an HTTP handler that executes a SQL query and returns a list with pagination
func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFuncType {
	return func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				stack := debug.Stack()
				logger.Error("Panic in SqlQueryList: %v\nStack trace:\n%s", err, string(stack))
				http.Error(w, fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError)
			}
		}()

		// Create local copy to avoid modifying the captured parameter across requests
		sqlquery := sqlquery

		ctx, cancel := context.WithTimeout(r.Context(), 15*time.Minute)
		defer cancel()

		var dbobjlist []map[string]interface{}
		var total int64
		propQry := make(map[string]string)
		inputvars := make([]string, 0)
		metainfo := make(map[string]interface{})
		variables := make(map[string]interface{})

		complexAPI := false

		// Get user context from security package
		userCtx, ok := security.GetUserContext(ctx)
		if !ok {
			logger.Warn("No user context found in request")
			userCtx = &security.UserContext{UserID: 0, UserName: "anonymous"}
		}

		w.Header().Set("Content-Type", "application/json")

		// Initialize hook context
		hookCtx := &HookContext{
			Context:     ctx,
			Handler:     h,
			Request:     r,
			Writer:      w,
			SQLQuery:    sqlquery,
			Variables:   variables,
			InputVars:   inputvars,
			MetaInfo:    metainfo,
			PropQry:     propQry,
			UserContext: userCtx,
			NoCount:     options.NoCount,
			BlankParams: options.BlankParams,
			AllowFilter: options.AllowFilter,
			ComplexAPI:  complexAPI,
		}

		// Extract input variables from SQL query (placeholders like [variable])
		sqlquery = h.extractInputVariables(sqlquery, &inputvars)

		// Merge URL path parameters
		sqlquery = h.mergePathParams(r, sqlquery, variables)

		// Parse comprehensive parameters from headers and query string
		reqParams := h.ParseParameters(r)
		complexAPI = reqParams.ComplexAPI

		// Merge query string parameters
		sqlquery = h.mergeQueryParams(r, sqlquery, variables, options.AllowFilter, propQry)

		// Merge header parameters
		sqlquery = h.mergeHeaderParams(r, sqlquery, variables, propQry, &complexAPI)

		// Apply filters from parsed parameters (if not already applied by pAllowFilter)
		if !options.AllowFilter {
			sqlquery = h.ApplyFilters(sqlquery, reqParams)
		}

		// Apply field selection
		sqlquery = h.ApplyFieldSelection(sqlquery, reqParams)

		// Apply DISTINCT if requested
		sqlquery = h.ApplyDistinct(sqlquery, reqParams)

		// Override pNoCount if skipcount is specified
		if reqParams.SkipCount {
			options.NoCount = true
		}

		// Build metainfo
		metainfo["ipaddress"] = getIPAddress(r)
		metainfo["url"] = r.RequestURI
		metainfo["user"] = userCtx.UserName
		metainfo["rid_user"] = fmt.Sprintf("%d", userCtx.UserID)
		metainfo["method"] = r.Method
		metainfo["variables"] = variables

		// Replace meta variables in SQL
		sqlquery = h.replaceMetaVariables(sqlquery, r, userCtx, metainfo, variables)

		// Replace variables from provided values, then blank any remaining unused ones
		for _, kw := range inputvars {
			varName := kw[1 : len(kw)-1] // strip [ and ]
			if val, ok := variables[varName]; ok {
				if strVal := fmt.Sprintf("%v", val); strVal != "" {
					sqlquery = strings.ReplaceAll(sqlquery, kw, safeSubstituteVar(sqlquery, kw, strVal))
					continue
				}
			}
			if options.BlankParams {
				replacement := getReplacementForBlankParam(sqlquery, kw)
				sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
				logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
			}
		}

		// Update hook context with latest SQL query and variables
		hookCtx.SQLQuery = sqlquery
		hookCtx.Variables = variables
		hookCtx.InputVars = inputvars

		// Execute query within transaction
		err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
			// Set transaction in hook context for hooks to use
			hookCtx.Tx = tx

			// Execute BeforeQueryList hook (inside transaction)
			if err := h.hooks.ExecuteBeforeOp(BeforeQueryList, hookCtx); err != nil {
				logger.Error("BeforeQueryList hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}

			// Check if hook aborted the operation
			if hookCtx.Abort {
				if hookCtx.AbortCode == 0 {
					hookCtx.AbortCode = http.StatusBadRequest
				}
				sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
				return fmt.Errorf("operation aborted: %s", hookCtx.AbortMessage)
			}

			// Use potentially modified SQL query from hook
			sqlquery = hookCtx.SQLQuery
			sqlqueryCnt := sqlquery

			// Parse sorting and pagination parameters
			sortcols, limit, offset := h.parsePaginationParams(r)

			// Override with parsed parameters if available
			if reqParams.SortColumns != "" {
				sortcols = reqParams.SortColumns
			}
			if reqParams.Limit > 0 {
				limit = reqParams.Limit
			}
			if reqParams.Offset > 0 {
				offset = reqParams.Offset
			}

			hookCtx.SortColumns = sortcols
			hookCtx.Limit = limit
			hookCtx.Offset = offset
			fromPos := strings.Index(strings.ToLower(sqlquery), "from ")
			orderbyPos := strings.Index(strings.ToLower(sqlquery), "order by")

			if len(sortcols) > 0 && (orderbyPos < 0 || (orderbyPos > 0 && orderbyPos < fromPos)) {
				sqlquery = fmt.Sprintf("%s \nORDER BY %s", sqlquery, ValidSQL(sortcols, "select"))
			}

			if !options.NoCount {
				if limit > 0 && offset > 0 {
					sqlquery = fmt.Sprintf("%s \nLIMIT %d OFFSET %d", sqlquery, limit, offset)
				} else if limit > 0 {
					sqlquery = fmt.Sprintf("%s \nLIMIT %d", sqlquery, limit)
				} else {
					sqlquery = fmt.Sprintf("%s \nLIMIT %d", sqlquery, 20000)
				}

				// Get total count
				countQuery := fmt.Sprintf("SELECT COUNT(1) FROM (%s) cnts", sqlqueryCnt)
				var countResult struct{ Count int64 }
				if err := tx.Query(ctx, &countResult, countQuery); err != nil {
					sendError(w, http.StatusBadRequest, "count_failed", "Failed to retrieve record count", err)
					return err
				}
				total = countResult.Count
			}

			// Execute BeforeSQLExec hook
			hookCtx.SQLQuery = sqlquery
			if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
				logger.Error("BeforeSQLExec hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified SQL query from hook
			sqlquery = hookCtx.SQLQuery

			// Execute main query
			rows := make([]map[string]interface{}, 0)
			if err := tx.Query(ctx, &rows, sqlquery); err != nil {
				sendError(w, http.StatusBadRequest, "query_failed", "Failed to retrieve records", err)
				return err
			}

			// Normalize PostgreSQL types for proper JSON marshaling
			dbobjlist = normalizePostgresTypesList(rows)

			if options.NoCount {
				total = int64(len(dbobjlist))
			}

			// Execute AfterSQLExec hook
			hookCtx.Result = dbobjlist
			hookCtx.Total = total
			if err := h.hooks.Execute(AfterSQLExec, hookCtx); err != nil {
				logger.Error("AfterSQLExec hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified result from hook
			if modifiedResult, ok := hookCtx.Result.([]map[string]interface{}); ok {
				dbobjlist = modifiedResult
			}
			total = hookCtx.Total

			// Execute AfterQueryList hook (inside transaction)
			hookCtx.Result = dbobjlist
			hookCtx.Total = total
			hookCtx.Error = nil
			if err := h.hooks.Execute(AfterQueryList, hookCtx); err != nil {
				logger.Error("AfterQueryList hook failed: %v", err)
				sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified result from hook
			if modifiedResult, ok := hookCtx.Result.([]map[string]interface{}); ok {
				dbobjlist = modifiedResult
			}
			total = hookCtx.Total

			return nil
		})

		if err != nil {
			logger.Error("Transaction failed: %v", err)
			return
		}

		// Set response headers
		respOffset := 0
		if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
			if o, err := strconv.Atoi(offsetStr); err == nil {
				respOffset = o
			}
		}

		w.Header().Set("Content-Range", fmt.Sprintf("items %d-%d/%d", respOffset, respOffset+len(dbobjlist), total))
		logger.Info("Serving: Records %d of %d", len(dbobjlist), total)

		// Execute BeforeResponse hook. The transaction has already committed by
		// this point, so hooks must use the pooled connection rather than the
		// now-dead tx.
		hookCtx.Tx = h.db
		hookCtx.Result = dbobjlist
		hookCtx.Total = total
		if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
			logger.Error("BeforeResponse hook failed: %v", err)
			sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
			return
		}
		// Use potentially modified result from hook
		if modifiedResult, ok := hookCtx.Result.([]map[string]interface{}); ok {
			dbobjlist = modifiedResult
		}

		if len(dbobjlist) == 0 {
			_, _ = w.Write([]byte("[]"))
			return
		}

		// Format response based on response format
		switch reqParams.ResponseFormat {
		case "syncfusion":
			// Syncfusion format: { result: data, count: total }
			response := map[string]interface{}{
				"result": dbobjlist,
				"count":  total,
			}
			data, err := json.Marshal(response)
			if err != nil {
				sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
			} else {
				if int64(len(dbobjlist)) < total {
					w.WriteHeader(http.StatusPartialContent)
				}
				_, _ = w.Write(data)
			}

		case "detail":
			// Detail format: { count, fields, items, tablename, tableprefix, total }
			tableName := r.URL.Path
			tablePrefix := reflection.ExtractTableNameOnly(tableName)
			fields := buildDetailFieldsFromRows(dbobjlist)
			metaobj := map[string]interface{}{
				"count":       fmt.Sprintf("%d", len(dbobjlist)),
				"fields":      fields,
				"items":       dbobjlist,
				"tablename":   tableName,
				"tableprefix": tablePrefix,
				"total":       fmt.Sprintf("%d", total),
			}
			data, err := json.Marshal(metaobj)
			if err != nil {
				sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
			} else {
				if int64(len(dbobjlist)) < total {
					w.WriteHeader(http.StatusPartialContent)
				}
				_, _ = w.Write(data)
			}

		default:
			// Simple format: just return the data array (or complex API if requested)
			if complexAPI {
				metaobj := map[string]interface{}{
					"items":       dbobjlist,
					"count":       fmt.Sprintf("%d", len(dbobjlist)),
					"total":       fmt.Sprintf("%d", total),
					"tablename":   r.URL.Path,
					"tableprefix": "gsql",
				}
				data, err := json.Marshal(metaobj)
				if err != nil {
					sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
				} else {
					if int64(len(dbobjlist)) < total {
						w.WriteHeader(http.StatusPartialContent)
					}
					_, _ = w.Write(data)
				}
			} else {
				data, err := json.Marshal(dbobjlist)
				if err != nil {
					sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
				} else {
					if int64(len(dbobjlist)) < total {
						w.WriteHeader(http.StatusPartialContent)
					}
					_, _ = w.Write(data)
				}
			}
		}
	}
}

// SqlQuery creates an HTTP handler that executes a SQL query and returns a single record
func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncType {
	return func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				stack := debug.Stack()
				logger.Error("Panic in SqlQuery: %v\nStack trace:\n%s", err, string(stack))
				http.Error(w, fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError)
			}
		}()

		// Create local copy to avoid modifying the captured parameter across requests
		sqlquery := sqlquery

		ctx, cancel := context.WithTimeout(r.Context(), 15*time.Minute)
		defer cancel()

		propQry := make(map[string]string)
		inputvars := make([]string, 0)
		metainfo := make(map[string]interface{})
		variables := make(map[string]interface{})

		dbobj := make(map[string]interface{})
		complexAPI := false

		// Get user context from security package
		userCtx, ok := security.GetUserContext(ctx)
		if !ok {
			logger.Warn("No user context found in request")
			userCtx = &security.UserContext{UserID: 0, UserName: "anonymous"}
		}

		w.Header().Set("Content-Type", "application/json")

		// Initialize hook context
		hookCtx := &HookContext{
			Context:     ctx,
			Handler:     h,
			Request:     r,
			Writer:      w,
			SQLQuery:    sqlquery,
			Variables:   variables,
			InputVars:   inputvars,
			MetaInfo:    metainfo,
			PropQry:     propQry,
			UserContext: userCtx,
			BlankParams: options.BlankParams,
			ComplexAPI:  complexAPI,
		}

		// Extract input variables from SQL query
		sqlquery = h.extractInputVariables(sqlquery, &inputvars)

		// Merge URL path parameters
		sqlquery = h.mergePathParams(r, sqlquery, variables)

		// Parse comprehensive parameters from headers and query string
		reqParams := h.ParseParameters(r)
		complexAPI = reqParams.ComplexAPI

		// Merge query string parameters
		sqlquery = h.mergeQueryParams(r, sqlquery, variables, false, propQry)

		// Merge header parameters
		sqlquery = h.mergeHeaderParams(r, sqlquery, variables, propQry, &complexAPI)
		hookCtx.ComplexAPI = complexAPI

		// Apply filters from parsed parameters
		sqlquery = h.ApplyFilters(sqlquery, reqParams)

		// Apply field selection
		sqlquery = h.ApplyFieldSelection(sqlquery, reqParams)

		// Apply DISTINCT if requested
		sqlquery = h.ApplyDistinct(sqlquery, reqParams)

		// Build metainfo
		metainfo["ipaddress"] = getIPAddress(r)
		metainfo["url"] = r.RequestURI
		metainfo["user"] = userCtx.UserName
		metainfo["rid_user"] = fmt.Sprintf("%d", userCtx.UserID)
		metainfo["method"] = r.Method
		metainfo["variables"] = variables

		// Replace meta variables in SQL
		sqlquery = h.replaceMetaVariables(sqlquery, r, userCtx, metainfo, variables)

		// Apply field filters from headers
		for k, val := range propQry {
			kLower := strings.ToLower(k)
			if strings.HasPrefix(kLower, "x-fieldfilter-") {
				colname := strings.ReplaceAll(kLower, "x-fieldfilter-", "")
				if strings.Contains(strings.ToLower(sqlquery), colname) {
					switch val {
					case "0":
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = 0", ValidSQL(colname, "colname")))
					case "":
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(colname, "colname")))
					default:
						if IsNumeric(val) {
							sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
						} else {
							sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = '%s'", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
						}
					}
				}
			}
		}

		// Replace variables from provided values, then blank any remaining unused ones
		for _, kw := range inputvars {
			varName := kw[1 : len(kw)-1] // strip [ and ]
			if val, ok := variables[varName]; ok {
				if strVal := fmt.Sprintf("%v", val); strVal != "" {
					sqlquery = strings.ReplaceAll(sqlquery, kw, safeSubstituteVar(sqlquery, kw, strVal))
					continue
				}
			}
			if options.BlankParams {
				replacement := getReplacementForBlankParam(sqlquery, kw)
				sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
				logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
			}
		}

		// Update hook context with latest SQL query and variables
		hookCtx.SQLQuery = sqlquery
		hookCtx.Variables = variables
		hookCtx.InputVars = inputvars

		// Execute query within transaction
		err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
			// Set transaction in hook context for hooks to use
			hookCtx.Tx = tx

			// Execute BeforeQuery hook (inside transaction)
			if err := h.hooks.ExecuteBeforeOp(BeforeQuery, hookCtx); err != nil {
				logger.Error("BeforeQuery hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}

			// Check if hook aborted the operation
			if hookCtx.Abort {
				if hookCtx.AbortCode == 0 {
					hookCtx.AbortCode = http.StatusBadRequest
				}
				sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
				return fmt.Errorf("operation aborted: %s", hookCtx.AbortMessage)
			}

			// Use potentially modified SQL query from hook
			sqlquery = hookCtx.SQLQuery

			// Execute BeforeSQLExec hook
			if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
				logger.Error("BeforeSQLExec hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified SQL query from hook
			sqlquery = hookCtx.SQLQuery

			// Execute main query
			rows := make([]map[string]interface{}, 0)
			if err := tx.Query(ctx, &rows, sqlquery); err != nil {
				sendError(w, http.StatusBadRequest, "query_failed", "Failed to retrieve records", err)
				return err
			}

			if len(rows) > 0 {
				dbobj = normalizePostgresTypes(rows[0])
			}

			// Execute AfterSQLExec hook
			hookCtx.Result = dbobj
			if err := h.hooks.Execute(AfterSQLExec, hookCtx); err != nil {
				logger.Error("AfterSQLExec hook failed: %v", err)
				sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified result from hook
			if modifiedResult, ok := hookCtx.Result.(map[string]interface{}); ok {
				dbobj = modifiedResult
			}

			// Execute AfterQuery hook (inside transaction)
			hookCtx.Result = dbobj
			hookCtx.Error = nil
			if err := h.hooks.Execute(AfterQuery, hookCtx); err != nil {
				logger.Error("AfterQuery hook failed: %v", err)
				sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
				return err
			}
			// Use potentially modified result from hook
			if modifiedResult, ok := hookCtx.Result.(map[string]interface{}); ok {
				dbobj = modifiedResult
			}

			return nil
		})

		if err != nil {
			logger.Error("Transaction failed: %v", err)
			return
		}

		// Execute BeforeResponse hook. The transaction has already committed by
		// this point, so hooks must use the pooled connection rather than the
		// now-dead tx.
		hookCtx.Tx = h.db
		hookCtx.Result = dbobj
		if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
			logger.Error("BeforeResponse hook failed: %v", err)
			sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
			return
		}
		// Use potentially modified result from hook
		if modifiedResult, ok := hookCtx.Result.(map[string]interface{}); ok {
			dbobj = modifiedResult
		}

		// Check if response should be root-level data
		if val, ok := dbobj["root_as_data"]; ok {
			data, err := json.Marshal(val)
			if err != nil {
				sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
			} else {
				_, _ = w.Write(data)
			}
			return
		}

		// Marshal and send response
		data, err := json.Marshal(dbobj)
		if err != nil {
			sendError(w, http.StatusInternalServerError, "json_error", "Could not marshal response", err)
		} else {
			_, _ = w.Write(data)
		}
	}
}

// Helper functions

// extractInputVariables extracts placeholders like [variable] from the SQL query
func (h *Handler) extractInputVariables(sqlquery string, inputvars *[]string) string {

	testsqlquery := sqlquery
	for i := 0; i <= strings.Count(sqlquery, "[")*4; i++ {
		iStart := strings.Index(testsqlquery, "[")
		if iStart < 0 {
			break
		}
		iEnd := strings.Index(testsqlquery, "]")
		if iEnd < 0 {
			break
		}
		*inputvars = append(*inputvars, testsqlquery[iStart:iEnd+1])
		testsqlquery = testsqlquery[iEnd+1:]
	}
	return sqlquery
}

// mergePathParams merges URL path parameters into the SQL query
func (h *Handler) mergePathParams(r *http.Request, sqlquery string, variables map[string]interface{}) string {

	if h.GetVariablesCallback() != nil {
		pathVars := h.GetVariablesCallback()(r)
		for k, v := range pathVars {
			kword := fmt.Sprintf("[%s]", k)
			if strings.Contains(sqlquery, kword) {
				// Sanitize the value before replacing
				vStr := fmt.Sprintf("%v", v)
				sanitized := ValidSQL(vStr, "colvalue")
				sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
			}
			variables[k] = v

		}
	}
	return sqlquery
}

// mergeQueryParams merges query string parameters into the SQL query
func (h *Handler) mergeQueryParams(r *http.Request, sqlquery string, variables map[string]interface{}, allowFilter bool, propQry map[string]string) string {
	for parmk, parmv := range r.URL.Query() {
		if len(parmk) == 0 || len(parmv) == 0 {
			continue
		}

		val := parmv[0]
		dec, err := restheadspec.DecodeParam(val)
		if err == nil {
			val = dec
		}

		kword := fmt.Sprintf("[%s]", parmk)
		variables[parmk] = val

		// Replace in SQL if placeholder exists
		if strings.Contains(sqlquery, kword) && len(val) > 0 {
			if strings.HasPrefix(parmk, "p-") {
				// Sanitize the parameter value before replacing
				sanitized := ValidSQL(val, "colvalue")
				sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
			}
		}

		// Add to propQry for x- prefixed params
		if strings.HasPrefix(parmk, "x-") {
			propQry[parmk] = val
		}

		// Apply filters if allowed — check only the SELECT list to avoid matching function
		// parameters in the FROM clause (e.g. [p_rid_doctype] in a set-returning function call)
		// or names inside quoted string arguments.
		if allowFilter && len(parmk) > 1 && strings.Contains(sqlSelectList(sqlStripStringLiterals(sqlquery)), strings.ToLower(parmk)) {
			if len(parmv) > 1 {
				// Sanitize each value in the IN clause with appropriate quoting
				sanitizedValues := make([]string, len(parmv))
				for i, v := range parmv {
					if IsNumeric(v) {
						// Numeric values don't need quotes
						sanitizedValues[i] = ValidSQL(v, "colvalue")
					} else {
						// String values need quotes
						sanitized := ValidSQL(v, "colvalue")
						sanitizedValues[i] = fmt.Sprintf("'%s'", sanitized)
					}
				}
				sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s IN (%s)", ValidSQL(parmk, "colname"), strings.Join(sanitizedValues, ",")))
			} else {
				if strings.Contains(val, "match=") {
					colval := strings.ReplaceAll(val, "match=", "")
					// Escape single quotes and backslashes for LIKE patterns
					// But don't escape wildcards % and _ which are intentional
					colval = strings.ReplaceAll(colval, "\\", "\\\\")
					colval = strings.ReplaceAll(colval, "'", "''")
					if colval != "*" {
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", ValidSQL(parmk, "colname"), colval))
					}
				} else if val == "" || val == "0" {
					// For empty/zero values, treat as literal 0 or empty string with quotes
					if val == "0" {
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = 0 OR %[1]s IS NULL)", ValidSQL(parmk, "colname")))
					} else {
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(parmk, "colname")))
					}
				} else {
					if IsNumeric(val) {
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(parmk, "colname"), ValidSQL(val, "colvalue")))
					} else {
						sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = '%s'", ValidSQL(parmk, "colname"), ValidSQL(val, "colvalue")))
					}
				}
			}
		}
	}
	return sqlquery
}

// mergeHeaderParams merges HTTP header parameters into the SQL query
func (h *Handler) mergeHeaderParams(r *http.Request, sqlquery string, variables map[string]interface{}, propQry map[string]string, complexAPI *bool) string {
	for kc, v := range r.Header {
		k := strings.ToLower(kc)
		if !strings.HasPrefix(k, "x-") || len(v) == 0 {
			continue
		}

		val := v[0]
		dec, err := restheadspec.DecodeParam(val)
		if err == nil {
			val = dec
		}

		variables[k] = val
		propQry[k] = val

		kword := fmt.Sprintf("[%s]", k)
		if strings.Contains(sqlquery, kword) {
			// Sanitize the header value before replacing
			sanitized := ValidSQL(val, "colvalue")
			sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
		}

		// Handle special headers
		if strings.Contains(k, "x-fieldfilter-") {
			colname := strings.ReplaceAll(k, "x-fieldfilter-", "")
			switch val {
			case "0":
				sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = 0", ValidSQL(colname, "colname")))
			case "":
				sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(colname, "colname")))
			default:
				if IsNumeric(val) {
					sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
				} else {
					sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = '%s'", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
				}
			}
		}

		if strings.Contains(k, "x-searchfilter-") {
			colname := strings.ReplaceAll(k, "x-searchfilter-", "")
			sval := strings.ReplaceAll(val, "'", "")
			if sval != "" {
				sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", ValidSQL(colname, "colname"), ValidSQL(sval, "colvalue")))
			}
		}

		if strings.Contains(k, "x-custom-sql-w") {
			colval := ValidSQL(val, "select")
			if len(colval) > 0 {
				sqlquery = sqlQryWhere(sqlquery, colval)
			}
		}

		if strings.Contains(k, "x-simpleapi") {
			*complexAPI = !strings.EqualFold(val, "1") && !strings.EqualFold(val, "true")
		}
	}
	return sqlquery
}

// sqlStripStringLiterals removes the contents of single-quoted string literals from SQL,
// leaving the structural identifiers (column names, table names) intact.
// Used to check column presence without matching inside string arguments.
func sqlStripStringLiterals(sql string) string {
	re := regexp.MustCompile(`'(?:[^']|'')*'`)
	return re.ReplaceAllString(sql, "''")
}

// sqlSelectList returns the column list portion of a SELECT query (between SELECT and FROM).
// Returns the full query lowercased if no clear SELECT…FROM boundary is found.
func sqlSelectList(sql string) string {
	lower := strings.ToLower(sql)
	selectPos := strings.Index(lower, "select ")
	fromPos := strings.Index(lower, " from ")
	if selectPos < 0 || fromPos <= selectPos {
		return lower
	}
	return lower[selectPos+7 : fromPos]
}

// replaceMetaVariables replaces meta variables like [rid_user], [user], etc. in the SQL query
func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx *security.UserContext, metainfo map[string]interface{}, variables map[string]interface{}) string {
	if strings.Contains(sqlquery, "[p_meta_default]") {
		data, _ := json.Marshal(metainfo)
		dataStr := strings.ReplaceAll(string(data), "$META$", "/*META*/")
		sqlquery = strings.ReplaceAll(sqlquery, "[p_meta_default]", fmt.Sprintf("$META$%s$META$::jsonb", dataStr))
	}

	if strings.Contains(sqlquery, "[json_variables]") {
		data, _ := json.Marshal(variables)
		dataStr := strings.ReplaceAll(string(data), "$VAR$", "/*VAR*/")

		sqlquery = strings.ReplaceAll(sqlquery, "[json_variables]", fmt.Sprintf("$VAR$%s$VAR$::jsonb", dataStr))
	}

	if strings.Contains(sqlquery, "[rid_user]") {
		sqlquery = strings.ReplaceAll(sqlquery, "[rid_user]", fmt.Sprintf("%d", userCtx.UserID))
	}

	if strings.Contains(sqlquery, "[user]") {
		sqlquery = strings.ReplaceAll(sqlquery, "[user]", fmt.Sprintf("$USR$%s$USR$", strings.ReplaceAll(userCtx.UserName, "$USR$", "/*USR*/")))
	}

	if strings.Contains(sqlquery, "[rid_session]") {
		sqlquery = strings.ReplaceAll(sqlquery, "[rid_session]", fmt.Sprintf("%d", userCtx.SessionRID))
	}
	if strings.Contains(sqlquery, "[id_session]") {
		sqlquery = strings.ReplaceAll(sqlquery, "[id_session]", userCtx.SessionID)
	}

	if strings.Contains(sqlquery, "[method]") {
		sqlquery = strings.ReplaceAll(sqlquery, "[method]", fmt.Sprintf("$M$%s$M$", strings.ReplaceAll(r.Method, "$M$", "/*M*/")))
	}

	if strings.Contains(sqlquery, "[post_body]") {
		bodystr := ""
		if r.Method == "POST" || r.Method == "PUT" {
			if r.Body != nil {
				contents, err := io.ReadAll(r.Body)
				if err == nil {
					bodystr = string(contents)
				}
			}
		}
		sqlquery = strings.ReplaceAll(sqlquery, "[post_body]", fmt.Sprintf("$PBODY$%s$PBODY$", strings.ReplaceAll(bodystr, "$PBODY$", "/*PBODY*/")))
	}

	return sqlquery
}

// parsePaginationParams extracts sort, limit, and offset parameters from request
func (h *Handler) parsePaginationParams(r *http.Request) (sortcols string, limit, offset int) {
	limit = 20
	offset = 0

	if sortStr := r.URL.Query().Get("sort"); sortStr != "" {
		sortcols = sortStr
	}

	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
			limit = l
		}
	}

	if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
		if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
			offset = o
		}
	}

	return
}

// ValidSQL validates and sanitizes SQL input to prevent injection
// mode can be: "colname", "colvalue", "select"
func ValidSQL(input, mode string) string {
	// Remove dangerous characters based on mode
	switch mode {
	case "colname":
		// For column names, only allow alphanumeric, underscore, and dot
		reg := regexp.MustCompile(`[^a-zA-Z0-9_\.]`)
		return reg.ReplaceAllString(input, "")
	case "colvalue":
		// For column values, escape single quotes and backslashes
		// Note: Backslashes must be escaped first, then single quotes
		result := strings.ReplaceAll(input, "\\", "\\\\")
		result = strings.ReplaceAll(result, "'", "''")
		return result
	case "select":
		// For SELECT clauses, be more permissive but still safe
		// Remove semicolons and common SQL injection patterns (case-insensitive)
		dangerous := []string{
			";", "--", "/\\*", "\\*/", "xp_", "sp_",
			"drop ", "delete ", "truncate ", "update ", "insert ",
			"exec ", "execute ", "union ", "declare ", "alter ", "create ",
		}
		// Build a single regex pattern with all dangerous keywords
		pattern := "(?i)(" + strings.Join(dangerous, "|") + ")"
		re := regexp.MustCompile(pattern)
		return re.ReplaceAllString(input, "")
	default:
		return input
	}
}

// sqlQryWhere adds a WHERE clause to a SQL query or appends to existing WHERE with AND
func sqlQryWhere(sqlquery, condition string) string {
	lowerQuery := strings.ToLower(sqlquery)
	wherePos := strings.Index(lowerQuery, " where ")
	groupPos := strings.Index(lowerQuery, " group by")
	orderPos := strings.Index(lowerQuery, " order by")
	limitPos := strings.Index(lowerQuery, " limit ")

	// Find the insertion point (before GROUP BY, ORDER BY, or LIMIT)
	insertPos := len(sqlquery)
	if groupPos > 0 && groupPos < insertPos {
		insertPos = groupPos
	}
	if orderPos > 0 && orderPos < insertPos {
		insertPos = orderPos
	}
	if limitPos > 0 && limitPos < insertPos {
		insertPos = limitPos
	}

	if wherePos > 0 {
		// WHERE exists, add AND condition before GROUP BY / ORDER BY / LIMIT
		before := sqlquery[:insertPos]
		after := sqlquery[insertPos:]
		return fmt.Sprintf("%s AND %s %s", before, condition, after)
	} else {
		// No WHERE exists, add it before GROUP BY / ORDER BY / LIMIT
		before := sqlquery[:insertPos]
		after := sqlquery[insertPos:]
		return fmt.Sprintf("%s WHERE %s %s", before, condition, after)
	}
}

// IsNumeric checks if a string contains only numeric characters
func IsNumeric(s string) bool {
	_, err := strconv.ParseFloat(s, 64)
	return err == nil
}

// isInsideDollarQuote reports whether the first occurrence of placeholder in sqlquery
// is immediately surrounded by dollar-sign characters (i.e. inside a $...$-quoted string).
// Dollar-quoted strings pass content through literally — no backslash processing — so
// values placed there must NOT have their backslashes escaped.
func isInsideDollarQuote(sqlquery, placeholder string) bool {
	idx := strings.Index(sqlquery, placeholder)
	if idx < 0 {
		return false
	}
	endIdx := idx + len(placeholder)
	charBefore := byte(0)
	charAfter := byte(0)
	if idx > 0 {
		charBefore = sqlquery[idx-1]
	}
	if endIdx < len(sqlquery) {
		charAfter = sqlquery[endIdx]
	}
	return charBefore == '$' || charAfter == '$'
}

// safeSubstituteVar returns value sanitised for the quoting context that surrounds
// placeholder in sqlquery: raw (no backslash escaping) for dollar-quoted contexts,
// ValidSQL("colvalue") escaping for everything else.
func safeSubstituteVar(sqlquery, placeholder, value string) string {
	if isInsideDollarQuote(sqlquery, placeholder) {
		return value
	}
	return ValidSQL(value, "colvalue")
}

// getReplacementForBlankParam determines the replacement value for an unused parameter
// based on whether it appears within quotes in the SQL query.
// It checks for PostgreSQL quotes: single quotes (”) and dollar quotes ($...$)
func getReplacementForBlankParam(sqlquery, param string) string {
	// Find the parameter in the query
	idx := strings.Index(sqlquery, param)
	if idx < 0 {
		return ""
	}

	// Check characters immediately before and after the parameter
	var charBefore, charAfter byte

	if idx > 0 {
		charBefore = sqlquery[idx-1]
	}

	endIdx := idx + len(param)
	if endIdx < len(sqlquery) {
		charAfter = sqlquery[endIdx]
	}

	// Check if parameter is surrounded by quotes (single quote, dollar sign for PostgreSQL dollar-quoted strings, or double quote for JSON string values)
	if (charBefore == '\'' || charBefore == '$' || charBefore == '"') && (charAfter == '\'' || charAfter == '$' || charAfter == '"') {
		// Parameter is in quotes, return empty string
		return ""
	}

	// Parameter is not in quotes, return NULL
	return "NULL"
}

// makeResultReceiver creates a slice of interface{} pointers for scanning SQL rows
// func makeResultReceiver(length int) []interface{} {
// 	result := make([]interface{}, length)
// 	for i := 0; i < length; i++ {
// 		var v interface{}
// 		result[i] = &v
// 	}
// 	return result
// }

// buildDetailFieldsFromRows builds a field metadata list from the column names and value types
// of a raw SQL result set. Used when no model struct is available (funcspec raw queries).
func buildDetailFieldsFromRows(rows []map[string]interface{}) []reflection.ModelFieldDetail {
	if len(rows) == 0 {
		return []reflection.ModelFieldDetail{}
	}
	first := rows[0]
	fields := make([]reflection.ModelFieldDetail, 0, len(first))
	for colName, val := range first {
		dataType := inferGoType(val)
		fields = append(fields, reflection.ModelFieldDetail{
			Name:        colName,
			DataType:    dataType,
			SQLName:     colName,
			SQLDataType: "",
			SQLKey:      "",
			Nullable:    val == nil,
		})
	}
	return fields
}

// inferGoType returns a simple type name for a value, used for detail field metadata.
func inferGoType(val interface{}) string {
	if val == nil {
		return "interface{}"
	}
	switch val.(type) {
	case bool:
		return "bool"
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		return "int64"
	case float32, float64:
		return "float64"
	case string:
		return "string"
	case []byte:
		return "[]byte"
	default:
		return "interface{}"
	}
}

// getIPAddress extracts the real IP address from the request
func getIPAddress(r *http.Request) string {
	if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
		// X-Forwarded-For can contain multiple IPs, take the first one
		ips := strings.Split(forwarded, ",")
		return strings.TrimSpace(ips[0])
	}
	if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
		return realIP
	}
	return r.RemoteAddr
}

// sendError sends a JSON error response
func sendError(w http.ResponseWriter, status int, code, message string, err error) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)

	errObj := common.APIError{
		Code:    code,
		Message: message,
	}
	if err != nil {
		errObj.Detail = err.Error()
		var sqlErr *common.SQLError
		if errors.As(err, &sqlErr) {
			errObj.SQL = sqlErr.SQL
		}
	}

	data, _ := json.Marshal(map[string]interface{}{
		"success": false,
		"error":   errObj,
	})
	_, _ = w.Write(data)
}

// normalizePostgresTypesList normalizes a list of result maps to handle PostgreSQL types correctly
func normalizePostgresTypesList(rows []map[string]interface{}) []map[string]interface{} {
	if len(rows) == 0 {
		return rows
	}

	normalized := make([]map[string]interface{}, len(rows))
	for i, row := range rows {
		normalized[i] = normalizePostgresTypes(row)
	}
	return normalized
}

// normalizePostgresTypes normalizes a result map to handle PostgreSQL types correctly for JSON marshaling
// This is necessary because when scanning into map[string]interface{}, PostgreSQL types like jsonb, bytea, etc.
// are scanned as []byte which would be base64-encoded when marshaled to JSON.
func normalizePostgresTypes(row map[string]interface{}) map[string]interface{} {
	if row == nil {
		return nil
	}

	normalized := make(map[string]interface{}, len(row))
	for key, value := range row {
		normalized[key] = normalizePostgresValue(value)
	}
	return normalized
}

// normalizePostgresValue normalizes a single value to the appropriate Go type for JSON marshaling
func normalizePostgresValue(value interface{}) interface{} {
	if value == nil {
		return nil
	}

	switch v := value.(type) {
	case []byte:
		// Check if it's valid JSON (jsonb type)
		// Try to unmarshal as JSON first
		var jsonObj interface{}
		if err := json.Unmarshal(v, &jsonObj); err == nil {
			// It's valid JSON, return as json.RawMessage so it's not double-encoded
			return json.RawMessage(v)
		}
		// Not valid JSON, could be bytea - keep as []byte for base64 encoding
		return v

	case []interface{}:
		// Recursively normalize array elements
		normalized := make([]interface{}, len(v))
		for i, elem := range v {
			normalized[i] = normalizePostgresValue(elem)
		}
		return normalized

	case map[string]interface{}:
		// Recursively normalize nested maps
		return normalizePostgresTypes(v)
	case string:
		var jsonObj interface{}
		if err := json.Unmarshal([]byte(v), &jsonObj); err == nil {
			// It's valid JSON, return as json.RawMessage so it's not double-encoded
			return json.RawMessage(v)
		}
		return v
	case uuid.UUID:
		return v.String()
	case time.Time:
		return v.Format(time.RFC3339)
	case bool, int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64:
		return v
	default:
		// For other types (int, float, bool, etc.), return as-is
		// Check stringers
		if str, ok := v.(fmt.Stringer); ok {
			return str.String()
		}
		return v
	}
}
