package dialectsfcrm

import (
	"context"
	"log"
	"net/http"
	"strings"

	soql "github.com/nexuscrm/dialect-sf-crm-soql"
	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

var toolingTableAliases = map[string]string{
	"ValidationRule":    "_System_ValidationRule",
	"CustomApplication": "_System_App",
}

var toolingFieldAliases = map[string]string{
	"validationrule.EntityDefinition.DeveloperName": "ObjectApiName",
	"validationrule.ValidationName":                 "Name",
}

func ToolingQueryNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	q := queryValue(req, "q")
	if q == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidQuery, "q parameter is required")}, nil
	}

	parsed, err := soql.ParseSOQL(q)
	if err != nil {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeMalformedQuery, err.Error())}, nil
	}
	toolingTable := translateToolingQueryAliases(parsed)

	query := deps.Contracts.Query
	if query == nil {
		log.Printf("tooling query: query contract unavailable")
		return toolingQueryInternalErrorResponse(), nil
	}
	queryResult, err := query.Query(ctx, querySpecFromParsedSOQL(parsed), req.User)
	if err != nil {
		if sharederrors.IsInternal(err) {
			log.Printf("tooling query: query execution failed: %v", err)
		}
		if resp := respondServiceError(req, err); resp != nil {
			return resp, nil
		}
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidQuery, err.Error())}, nil
	}

	formattedRecords, attrErr := toolingQueryRecordsToV63(queryResult.Records, toolingTable, req.Version, queryResult.IsAggregate)
	if attrErr != nil {
		log.Printf("tooling query: record attribute projection failed: %v", attrErr)
		return toolingQueryInternalErrorResponse(), nil
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"totalSize": queryResult.TotalSize,
		"done":      queryResult.Done,
		"records":   formattedRecords,
	}), nil
}

func toolingQueryInternalErrorResponse() *dialect.Response {
	return queryErrorResponse(http.StatusInternalServerError, wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator.")
}

func toolingQueryRecordsToV63(records []contract.Record, toolingTable string, apiVersion string, isAggregate bool) ([]any, error) {
	toolingBase := versionedToolingSObjectsBasePath(apiVersion)
	wireRecords := recordsToV63SObjects(records)
	formattedRecords := make([]any, len(wireRecords))
	for i, r := range wireRecords {
		attrs, attrErr := recordAttributes(toolingBase, toolingTable, r, isAggregate)
		if attrErr != nil {
			return nil, attrErr
		}
		r["attributes"] = attrs
		formattedRecords[i] = r
	}
	return formattedRecords, nil
}

func translateToolingQueryAliases(req *models.QueryRequest) string {
	if req == nil {
		return ""
	}
	toolingTable := req.ObjectAPIName
	tableLower := strings.ToLower(req.ObjectAPIName)
	if alias, ok := toolingTableAliases[req.ObjectAPIName]; ok {
		req.ObjectAPIName = alias
	}
	for i, f := range req.SelectFields {
		key := tableLower + "." + f
		if mapped, ok := toolingFieldAliases[key]; ok {
			req.SelectFields[i] = mapped
		}
	}
	if req.FilterExpr != "" {
		req.FilterExpr = translateToolingFilterExpression(tableLower, req.FilterExpr)
	}
	if req.Filter.Kind != queryspec.PredicateNone {
		req.Filter = translateToolingPredicate(tableLower, req.Filter)
	}
	if req.Having != "" {
		req.Having = translateToolingFilterExpression(tableLower, req.Having)
	}
	if req.HavingPredicate.Kind != queryspec.PredicateNone {
		req.HavingPredicate = translateToolingPredicate(tableLower, req.HavingPredicate)
	}
	for i := range req.SubQueries {
		if req.SubQueries[i].FilterExpr != "" {
			req.SubQueries[i].FilterExpr = translateToolingFilterExpression(tableLower, req.SubQueries[i].FilterExpr)
		}
		if req.SubQueries[i].Filter.Kind != queryspec.PredicateNone {
			req.SubQueries[i].Filter = translateToolingPredicate(tableLower, req.SubQueries[i].Filter)
		}
	}
	return toolingTable
}

func translateToolingFilterExpression(tableLower string, expression string) string {
	for _, suffix := range []string{"EntityDefinition.DeveloperName", "ValidationName"} {
		if mapped, ok := toolingFieldAlias(tableLower, suffix); ok {
			expression = strings.ReplaceAll(expression, suffix, mapped)
		}
	}
	return expression
}

func toolingFieldAlias(tableLower string, field string) (string, bool) {
	mapped, ok := toolingFieldAliases[tableLower+"."+field]
	return mapped, ok
}

func translateToolingPredicate(tableLower string, predicate queryspec.Predicate) queryspec.Predicate {
	switch predicate.Kind {
	case queryspec.PredicateFieldComparison:
		predicate.Field = translateToolingFieldName(tableLower, predicate.Field)
	case queryspec.PredicateComparison:
		predicate.Left = translateToolingOperand(tableLower, predicate.Left)
		predicate.Right = translateToolingOperand(tableLower, predicate.Right)
	case queryspec.PredicateExpression:
		predicate.Operand = translateToolingOperand(tableLower, predicate.Operand)
	case queryspec.PredicateFormulaExpression:
		predicate.Expression = translateToolingFilterExpression(tableLower, predicate.Expression)
	case queryspec.PredicateAnd, queryspec.PredicateOr, queryspec.PredicateNot:
		for i := range predicate.Children {
			predicate.Children[i] = translateToolingPredicate(tableLower, predicate.Children[i])
		}
	}
	return predicate
}

func translateToolingOperand(tableLower string, operand queryspec.Operand) queryspec.Operand {
	switch operand.Kind {
	case queryspec.OperandField:
		operand.Field = translateToolingFieldName(tableLower, operand.Field)
	case queryspec.OperandFunction:
		for i := range operand.Args {
			operand.Args[i] = translateToolingOperand(tableLower, operand.Args[i])
		}
	case queryspec.OperandList:
		for i := range operand.Elements {
			operand.Elements[i] = translateToolingOperand(tableLower, operand.Elements[i])
		}
	}
	return operand
}

func translateToolingFieldName(tableLower string, field string) string {
	if mapped, ok := toolingFieldAlias(tableLower, field); ok {
		return mapped
	}
	return field
}

func versionedToolingSObjectsBasePath(version string) string {
	return "/services/data/" + apiVersionOrDefault(version) + "/tooling/sobjects"
}
