/*
 * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
 * SPDX-License-Identifier: Apache-2.0
 */

package modusdb

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/hypermodeinc/modusdb/api/apiutils"
	"github.com/hypermodeinc/modusdb/api/querygen"
	"github.com/hypermodeinc/modusdb/api/structreflect"
)

func getByGid[T any](ctx context.Context, ns *Namespace, gid uint64) (uint64, T, error) {
	return executeGet[T](ctx, ns, gid)
}

func getByGidWithObject[T any](ctx context.Context, ns *Namespace, gid uint64, obj T) (uint64, T, error) {
	return executeGetWithObject[T](ctx, ns, obj, false, gid)
}

func getByConstrainedField[T any](ctx context.Context, ns *Namespace, cf ConstrainedField) (uint64, T, error) {
	return executeGet[T](ctx, ns, cf)
}

func getByConstrainedFieldWithObject[T any](ctx context.Context, ns *Namespace,
	cf ConstrainedField, obj T) (uint64, T, error) {

	return executeGetWithObject[T](ctx, ns, obj, false, cf)
}

func executeGet[T any, R UniqueField](ctx context.Context, ns *Namespace, args ...R) (uint64, T, error) {
	var obj T
	if len(args) != 1 {
		return 0, obj, fmt.Errorf("expected 1 argument, got %ds", len(args))
	}

	return executeGetWithObject(ctx, ns, obj, true, args...)
}

func executeGetWithObject[T any, R UniqueField](ctx context.Context, ns *Namespace,
	obj T, withReverse bool, args ...R) (uint64, T, error) {
	t := reflect.TypeOf(obj)

	tagMaps, err := structreflect.GetFieldTags(t)
	if err != nil {
		return 0, obj, err
	}
	readFromQuery := ""
	if withReverse {
		for jsonTag, reverseEdgeTag := range tagMaps.JsonToReverseEdge {
			readFromQuery += fmt.Sprintf(querygen.ReverseEdgeQuery,
				apiutils.GetPredicateName(t.Name(), jsonTag), reverseEdgeTag)
		}
	}

	var cf ConstrainedField
	var query string
	gid, ok := any(args[0]).(uint64)
	if ok {
		query = querygen.FormatObjQuery(querygen.BuildUidQuery(gid), readFromQuery)
	} else if cf, ok = any(args[0]).(ConstrainedField); ok {
		query = querygen.FormatObjQuery(querygen.BuildEqQuery(apiutils.GetPredicateName(t.Name(),
			cf.Key), cf.Value), readFromQuery)
	} else {
		return 0, obj, fmt.Errorf("invalid unique field type")
	}

	if tagMaps.JsonToDb[cf.Key] != nil && tagMaps.JsonToDb[cf.Key].Constraint == "" {
		return 0, obj, fmt.Errorf("constraint not defined for field %s", cf.Key)
	}

	resp, err := ns.engine.queryWithLock(ctx, ns, query)
	if err != nil {
		return 0, obj, err
	}

	dynamicType := structreflect.CreateDynamicStruct(t, tagMaps.FieldToJson, 1)

	dynamicInstance := reflect.New(dynamicType).Interface()

	var result struct {
		Obj []any `json:"obj"`
	}

	result.Obj = append(result.Obj, dynamicInstance)

	// Unmarshal the JSON response into the dynamic struct
	if err := json.Unmarshal(resp.Json, &result); err != nil {
		return 0, obj, err
	}

	// Check if we have at least one object in the response
	if len(result.Obj) == 0 {
		return 0, obj, apiutils.ErrNoObjFound
	}

	return structreflect.ConvertDynamicToTyped[T](result.Obj[0], t)
}

func executeQuery[T any](ctx context.Context, ns *Namespace, queryParams QueryParams,
	withReverse bool) ([]uint64, []T, error) {
	var obj T
	t := reflect.TypeOf(obj)
	tagMaps, err := structreflect.GetFieldTags(t)
	if err != nil {
		return nil, nil, err
	}

	var filterQueryFunc querygen.QueryFunc = func() string {
		return ""
	}
	var paginationAndSorting string
	if queryParams.Filter != nil {
		filterQueryFunc = filtersToQueryFunc(t.Name(), *queryParams.Filter)
	}
	if queryParams.Pagination != nil || queryParams.Sorting != nil {
		var pagination, sorting string
		if queryParams.Pagination != nil {
			pagination = paginationToQueryString(*queryParams.Pagination)
		}
		if queryParams.Sorting != nil {
			sorting = sortingToQueryString(t.Name(), *queryParams.Sorting)
		}
		paginationAndSorting = fmt.Sprintf("%s %s", pagination, sorting)
	}

	readFromQuery := ""
	if withReverse {
		for jsonTag, reverseEdgeTag := range tagMaps.JsonToReverseEdge {
			readFromQuery += fmt.Sprintf(querygen.ReverseEdgeQuery, apiutils.GetPredicateName(t.Name(), jsonTag), reverseEdgeTag)
		}
	}

	query := querygen.FormatObjsQuery(t.Name(), filterQueryFunc, paginationAndSorting, readFromQuery)

	resp, err := ns.engine.queryWithLock(ctx, ns, query)
	if err != nil {
		return nil, nil, err
	}

	dynamicType := structreflect.CreateDynamicStruct(t, tagMaps.FieldToJson, 1)

	var result struct {
		Objs []any `json:"objs"`
	}

	var tempMap map[string][]any
	if err := json.Unmarshal(resp.Json, &tempMap); err != nil {
		return nil, nil, err
	}

	// Determine the number of elements
	numElements := len(tempMap["objs"])

	// Append the interface the correct number of times
	for i := 0; i < numElements; i++ {
		result.Objs = append(result.Objs, reflect.New(dynamicType).Interface())
	}

	// Unmarshal the JSON response into the dynamic struct
	if err := json.Unmarshal(resp.Json, &result); err != nil {
		return nil, nil, err
	}

	gids := make([]uint64, len(result.Objs))
	objs := make([]T, len(result.Objs))
	for i, obj := range result.Objs {
		gid, typedObj, err := structreflect.ConvertDynamicToTyped[T](obj, t)
		if err != nil {
			return nil, nil, err
		}
		gids[i] = gid
		objs[i] = typedObj
	}

	return gids, objs, nil
}

func getExistingObject[T any](ctx context.Context, ns *Namespace, gid uint64, cf *ConstrainedField,
	object T) (uint64, error) {
	var err error
	if gid != 0 {
		gid, _, err = getByGidWithObject[T](ctx, ns, gid, object)
	} else if cf != nil {
		gid, _, err = getByConstrainedFieldWithObject[T](ctx, ns, *cf, object)
	}
	if err != nil {
		return 0, err
	}
	return gid, nil
}

func getSchema(ctx context.Context, ns *Namespace) (*querygen.SchemaResponse, error) {
	resp, err := ns.engine.queryWithLock(ctx, ns, querygen.SchemaQuery)
	if err != nil {
		return nil, err
	}

	var schema querygen.SchemaResponse
	if err := json.Unmarshal(resp.Json, &schema); err != nil {
		return nil, err
	}
	return &schema, nil
}