package dialectsfcrm

import (
	"context"
	"errors"
	"net/http"
	"net/url"

	"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"
	querycodec "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/querycodec"
)

// serveQueryWithCodec is the transitional in-process adapter for the same
// L1-owned query codec used by the independently deployed sfcrm AppService.
// It translates only between the codec's product model and the current neutral
// in-process Query seam; it does not own parsing, pagination, locators, CRM
// projection, or public error grammar.
func serveQueryWithCodec(ctx context.Context, req *dialect.Request, deps dialect.Deps, kind querycodec.RouteKind) (*dialect.Response, error) {
	if req == nil {
		return &dialect.Response{Status: http.StatusInternalServerError, Body: wire.SFGenericInternalErrorResponse()}, nil
	}
	actors := &historyActorNames{}
	executor := legacyQueryCodecExecutor{
		query: deps.Contracts.Query, metadata: deps.Contracts.Metadata,
		user: req.User, version: req.Version, historyActors: actors,
	}
	handler, err := querycodec.NewHandler(querycodec.Executor{Query: executor.queryRecords, History: executor.queryHistory, Dates: executor.resolveDates})
	if err != nil {
		return nil, err
	}
	response := handler.Handle(ctx, querycodec.Request{
		Route:  querycodec.Route{Kind: kind, Version: req.Version, Locator: req.Params["locator"]},
		Query:  url.Values(req.Query),
		Header: http.Header(req.Header),
	})
	// Enrich <Object>History rows with the resolved CreatedBy relationship. The
	// released codec owns the neutral projection and only emits the raw actor id;
	// resolving it to User.Name is a Salesforce-shaped concern kept here until the
	// codec is next released (see query_history_actor.go).
	if response.Status == http.StatusOK {
		injectHistoryActorRelationships(response.Body, actors, req.Version)
	}
	headers := make(map[string]string, len(response.Header))
	for key, values := range response.Header {
		if len(values) > 0 {
			headers[key] = values[0]
		}
	}
	return &dialect.Response{Status: response.Status, Body: restoreLegacyQueryResponseTypes(response.Body), Headers: headers}, nil
}

// restoreLegacyQueryResponseTypes keeps the transitional in-process dialect
// seam source-compatible while the independently deployed AppService consumes
// the codec's stdlib-only map representation. Only the top-level query
// envelope authoritatively identifies its records here. Relationship and child
// records retain their models.SObject identity from legacyQueryCodecExecutor;
// recursively guessing from field names such as "attributes" would corrupt
// valid ordinary JSON objects.
func restoreLegacyQueryResponseTypes(value any) any {
	envelope, ok := value.(map[string]any)
	if !ok {
		return value
	}
	records, ok := envelope["records"].([]any)
	if !ok {
		return value
	}
	for index, record := range records {
		if fields, isCodecMap := record.(map[string]any); isCodecMap {
			records[index] = models.SObject(fields)
		}
	}
	return envelope
}

type legacyQueryCodecExecutor struct {
	query    contract.Query
	metadata contract.Metadata
	user     *models.UserSession
	version  string
	// historyActors is a shared holder the history executor uses to publish
	// resolved <Object>History actor names (CreatedById -> User.Name) back to
	// serveQueryWithCodec after the released codec has run. See query_history_actor.go.
	historyActors *historyActorNames
}

func (e legacyQueryCodecExecutor) queryRecords(ctx context.Context, request querycodec.QueryRequest, aggregateFetchLimit int) (querycodec.Result, error) {
	if e.query == nil {
		return querycodec.Result{}, &querycodec.Failure{Category: querycodec.FailureInternal}
	}
	if aggregateFetchLimit > 0 {
		ctx = queryspec.WithAggregateFetchLimit(ctx, aggregateFetchLimit)
	}
	result, err := e.query.Query(ctx, legacyQuerySpec(request), e.user)
	if err != nil {
		return querycodec.Result{}, legacyQueryFailure(e.version, err)
	}
	if result == nil {
		return querycodec.Result{Done: true}, nil
	}
	mapped := querycodec.Result{
		TotalSize: result.TotalSize, TotalSizeUnknown: result.TotalSizeUnknown,
		Done: result.Done, Aggregate: result.IsAggregate,
		Records: make([]querycodec.Record, 0, len(result.Records)),
	}
	var schema *models.ObjectMetadata
	if e.metadata != nil {
		schema, _ = e.metadata.Describe(ctx, request.ObjectAPIName)
	}
	for _, record := range result.Records {
		row := projectRecordToV63(record, v63RecordProjectionFull)
		nestRelationshipFields(schema, row)
		normalizeCurrentEngineChildSubqueries(row, request.SubQueries)
		// The codec is the sole owner of the top-level CRM attributes envelope.
		delete(row, "attributes")
		mapped.Records = append(mapped.Records, querycodec.Record{
			ObjectType: models.RecordObjectType(record),
			Fields:     map[string]any(row),
		})
	}
	return mapped, nil
}

// normalizeCurrentEngineChildSubqueries removes the current in-process
// engine's historical CRM envelope before the single query codec projects it.
// A future neutral engine slice follows the same path, so the compatibility
// adapter does not become a second response-envelope owner.
func normalizeCurrentEngineChildSubqueries(row models.SObject, subqueries []querycodec.SubQueryRequest) {
	for _, subquery := range subqueries {
		value, ok := row[subquery.RelationshipName]
		if !ok {
			continue
		}
		if envelope, wrapped := value.(map[string]any); wrapped {
			value = envelope["records"]
		}
		if records, isRecordSlice := childRecordsAsAny(value); isRecordSlice {
			row[subquery.RelationshipName] = records
		}
	}
}

func childRecordsAsAny(value any) ([]any, bool) {
	switch records := value.(type) {
	case []any:
		return records, true
	case []models.SObject:
		result := make([]any, len(records))
		for i := range records {
			result[i] = records[i]
		}
		return result, true
	default:
		return nil, false
	}
}

func (e legacyQueryCodecExecutor) queryHistory(ctx context.Context, request querycodec.HistoryRequest) (querycodec.HistoryResult, error) {
	if e.query == nil {
		return querycodec.HistoryResult{}, &querycodec.Failure{Category: querycodec.FailureUnavailable}
	}
	result, err := e.query.QueryHistory(ctx, contract.HistoryQueryRequest{
		ParentObjectAPIName: request.ParentObjectAPIName,
		SelectFields:        append([]string(nil), request.SelectFields...),
		ParentID:            request.ParentID,
		Filter:              legacyQueryPredicate(request.Filter),
		Limit:               request.Limit,
		Offset:              request.Offset,
		SortDesc:            request.SortDescending,
		ScanAll:             request.IncludeDeleted,
		PageSize:            request.PageSize,
		CursorOffset:        request.CursorOffset,
	}, e.user)
	if err != nil {
		return querycodec.HistoryResult{}, legacyHistoryFailure(err)
	}
	if result == nil {
		return querycodec.HistoryResult{Done: true}, nil
	}
	// Resolve CreatedById -> User.Name only when the projection selected the
	// CreatedBy relationship, so bare-CreatedById callers pay no extra query.
	if e.historyActors != nil && historyActorRelationshipRequested(request.SelectFields) {
		e.historyActors.byID = e.resolveHistoryActorNames(ctx, result.Rows)
	}
	mapped := querycodec.HistoryResult{TotalSize: result.TotalSize, Done: result.Done, Rows: make([]querycodec.HistoryRecord, 0, len(result.Rows))}
	for _, row := range result.Rows {
		mapped.Rows = append(mapped.Rows, querycodec.HistoryRecord{
			ID: row.ID, ParentID: row.ParentID, Field: row.Field,
			OldValue: row.OldValue, NewValue: row.NewValue, DataType: row.DataType,
			CreatedDate: row.CreatedDate, CreatedByID: row.CreatedByID,
		})
	}
	return mapped, nil
}

func legacyQuerySpec(request querycodec.QueryRequest) queryspec.QuerySpec {
	result := queryspec.QuerySpec{
		ObjectAPIName: request.ObjectAPIName,
		SelectFields:  append([]string(nil), request.SelectFields...),
		Filter:        legacyQueryPredicate(request.Filter),
		SortField:     request.SortField,
		SortDirection: request.SortDirection,
		Limit:         request.Limit,
		Offset:        request.Offset,
		SkipTotalSize: request.SkipTotalSize,
		ScanAll:       request.ScanAll,
		GroupBy:       request.GroupBy,
		Having:        legacyQueryPredicate(request.HavingPredicate),
	}
	for _, order := range request.OrderBy {
		result.OrderBy = append(result.OrderBy, queryspec.SortCriterion{Field: order.Field, Direction: order.Direction})
	}
	for _, subquery := range request.SubQueries {
		mapped := queryspec.SubQuerySpec{
			RelationshipName: subquery.RelationshipName,
			SelectFields:     append([]string(nil), subquery.SelectFields...),
			Filter:           legacyQueryPredicate(subquery.Filter),
			Limit:            subquery.Limit,
		}
		for _, order := range subquery.OrderBy {
			mapped.OrderBy = append(mapped.OrderBy, queryspec.SortCriterion{Field: order.Field, Direction: order.Direction})
		}
		result.SubQueries = append(result.SubQueries, mapped)
	}
	return result
}

func legacyQueryPredicate(value querycodec.Predicate) queryspec.Predicate {
	result := queryspec.Predicate{Field: value.Field, Op: value.Op, Value: legacyQueryScalar(value.Value)}
	switch value.Kind {
	case querycodec.PredicateNone:
		result.Kind = queryspec.PredicateNone
	case querycodec.PredicateAnd:
		result.Kind = queryspec.PredicateAnd
	case querycodec.PredicateOr:
		result.Kind = queryspec.PredicateOr
	case querycodec.PredicateNot:
		result.Kind = queryspec.PredicateNot
	case querycodec.PredicateExpression:
		result.Kind = queryspec.PredicateExpression
	case querycodec.PredicateComparison:
		result.Kind = queryspec.PredicateComparison
	case querycodec.PredicateFieldComparison:
		result.Kind = queryspec.PredicateFieldComparison
	case querycodec.PredicateCorrelatedMembership:
		result.Kind = queryspec.PredicateCorrelatedMembership
	}
	result.Left = legacyQueryOperand(value.Left)
	result.Right = legacyQueryOperand(value.Right)
	result.Operand = legacyQueryOperand(value.Operand)
	for _, child := range value.Children {
		result.Children = append(result.Children, legacyQueryPredicate(child))
	}
	if value.CorrelatedMembership != nil {
		membership := value.CorrelatedMembership
		result.CorrelatedMembership = &queryspec.CorrelatedMembership{
			ID: membership.ID, OuterField: membership.OuterField, Negate: membership.Negate,
			InnerObjectAPIName: membership.InnerObjectAPIName,
			InnerSelectField:   membership.InnerSelectField,
			InnerFilter:        legacyQueryPredicate(membership.InnerFilter),
			InnerLimit:         membership.InnerLimit, InnerScanAll: membership.InnerScanAll,
		}
	}
	return result
}

func legacyQueryOperand(value querycodec.Operand) queryspec.Operand {
	result := queryspec.Operand{Field: value.Field, Value: legacyQueryScalar(value.Value), Raw: value.Raw, Function: value.Function}
	switch value.Kind {
	case querycodec.OperandNone:
		result.Kind = queryspec.OperandNone
	case querycodec.OperandField:
		result.Kind = queryspec.OperandField
	case querycodec.OperandLiteral:
		result.Kind = queryspec.OperandLiteral
	case querycodec.OperandRaw:
		result.Kind = queryspec.OperandRaw
	case querycodec.OperandList:
		result.Kind = queryspec.OperandList
	case querycodec.OperandFunction:
		result.Kind = queryspec.OperandFunction
	}
	for _, argument := range value.Args {
		result.Args = append(result.Args, legacyQueryOperand(argument))
	}
	for _, element := range value.Elements {
		result.Elements = append(result.Elements, legacyQueryOperand(element))
	}
	return result
}

func legacyQueryScalar(value any) any {
	switch typed := value.(type) {
	case querycodec.NumericLiteral:
		return queryspec.NumericLiteral(typed)
	case querycodec.DateLiteralValue:
		return string(typed)
	case querycodec.DateTimeLiteralValue:
		return typed.FormulaText()
	default:
		return value
	}
}

func legacyHistoryFailure(err error) error {
	var denied *contract.FieldDeniedByFLSError
	if errors.As(err, &denied) {
		return &querycodec.Failure{Category: querycodec.FailureInvalidField, Message: denied.Error()}
	}
	var historyErr *contract.HistoryQueryError
	if !errors.As(err, &historyErr) {
		return &querycodec.Failure{Category: querycodec.FailureInternal}
	}
	switch historyErr.Kind {
	case contract.HistoryQueryErrorObjectPermissionDenied, contract.HistoryQueryErrorRecordVisibilityDenied:
		return &querycodec.Failure{Category: querycodec.FailureForbidden, Message: historyErr.Error()}
	case contract.HistoryQueryErrorMalformed:
		return &querycodec.Failure{Category: querycodec.FailureMalformed, Message: historyErr.Error()}
	case contract.HistoryQueryErrorUnavailable:
		return &querycodec.Failure{Category: querycodec.FailureUnavailable, Message: historyErr.Error()}
	default:
		return &querycodec.Failure{Category: querycodec.FailureInternal}
	}
}

func legacyQueryFailure(version string, err error) error {
	// Preserve lifecycle failures before converting errors to the codec's closed
	// categories; wrappers may contain private diagnostics.
	if errors.Is(err, context.Canceled) {
		return &querycodec.Failure{Category: querycodec.FailureCanceled}
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return &querycodec.Failure{Category: querycodec.FailureDeadline}
	}
	if field, ok := sharederrors.ValidationField(err); ok && field == "SelectFields" {
		return &querycodec.Failure{Category: querycodec.FailureInvalidField, Message: "Invalid field in query selection"}
	}
	if sharederrors.IsInternal(err) {
		return &querycodec.Failure{Category: querycodec.FailureInternal}
	}
	var statusErr httpStatusError
	if errors.As(err, &statusErr) {
		switch statusErr.HTTPStatus() {
		case http.StatusBadRequest:
			return &querycodec.Failure{Category: querycodec.FailureInvalid}
		case http.StatusUnauthorized:
			return &querycodec.Failure{Category: querycodec.FailureUnauthenticated}
		case http.StatusForbidden:
			return &querycodec.Failure{Category: querycodec.FailureForbidden}
		case http.StatusNotFound:
			return &querycodec.Failure{Category: querycodec.FailureMissing}
		case http.StatusConflict:
			return &querycodec.Failure{Category: querycodec.FailureConflict}
		case http.StatusTooManyRequests:
			return &querycodec.Failure{Category: querycodec.FailureExhausted}
		case 499:
			return &querycodec.Failure{Category: querycodec.FailureCanceled}
		case http.StatusGatewayTimeout:
			return &querycodec.Failure{Category: querycodec.FailureDeadline}
		case http.StatusServiceUnavailable:
			return &querycodec.Failure{Category: querycodec.FailureUnavailable}
		case http.StatusInternalServerError:
			return &querycodec.Failure{Category: querycodec.FailureInternal}
		}
	}
	sfError, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(version))
	if !ok {
		// An unclassified dependency error is not evidence of invalid user input.
		// Only the explicit domain wire contract below owns public error detail.
		return &querycodec.Failure{Category: querycodec.FailureInternal}
	}
	category := querycodec.FailureInternal
	switch sfError.ErrorCode {
	case wire.SFCodeInvalidCrossReferenceKey:
		category = querycodec.FailureInvalidReference
	case wire.SFCodeFieldIntegrityException:
		category = querycodec.FailureFieldIntegrity
	case wire.SFCodeInvalidOperation:
		category = querycodec.FailureInvalidOperation
	case wire.SFCodeInvalidField:
		category = querycodec.FailureInvalidField
	case wire.SFCodeNotFound:
		category = querycodec.FailureMissing
	case wire.SFCodeInvalidRequest:
		if status == http.StatusRequestEntityTooLarge {
			category = querycodec.FailureRequestTooLarge
		} else {
			category = querycodec.FailureInvalidRequest
		}
	}
	return &querycodec.Failure{Category: category, Message: sfError.Message, Fields: append([]string(nil), sfError.Fields...)}
}
