package apis

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

	"github.com/labstack/echo/v5"
	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/daos"
	"github.com/pocketbase/pocketbase/forms"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/resolvers"
	"github.com/pocketbase/pocketbase/tools/rest"
	"github.com/pocketbase/pocketbase/tools/search"
)

const expandQueryParam = "expand"

// BindRecordApi registers the record api endpoints and the corresponding handlers.
func BindRecordApi(app core.App, rg *echo.Group) {
	api := recordApi{app: app}

	subGroup := rg.Group(
		"/collections/:collection/records",
		ActivityLogger(app),
		LoadCollectionContext(app),
	)

	subGroup.GET("", api.list)
	subGroup.POST("", api.create)
	subGroup.GET("/:id", api.view)
	subGroup.PATCH("/:id", api.update)
	subGroup.DELETE("/:id", api.delete)
}

type recordApi struct {
	app core.App
}

func (api *recordApi) list(c echo.Context) error {
	collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
	if collection == nil {
		return rest.NewNotFoundError("", "Missing collection context.")
	}

	admin, _ := c.Get(ContextAdminKey).(*models.Admin)
	if admin == nil && collection.ListRule == nil {
		// only admins can access if the rule is nil
		return rest.NewForbiddenError("Only admins can perform this action.", nil)
	}

	// forbid user/guest defined non-relational joins (aka. @collection.*)
	queryStr := c.QueryString()
	if admin == nil && queryStr != "" && (strings.Contains(queryStr, "@collection") || strings.Contains(queryStr, "%40collection")) {
		return rest.NewForbiddenError("Only admins can filter by @collection.", nil)
	}

	requestData := api.exportRequestData(c)

	fieldsResolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)

	searchProvider := search.NewProvider(fieldsResolver).
		Query(api.app.Dao().RecordQuery(collection))

	if admin == nil && collection.ListRule != nil {
		searchProvider.AddFilter(search.FilterData(*collection.ListRule))
	}

	var rawRecords = []dbx.NullStringMap{}
	result, err := searchProvider.ParseAndExec(queryStr, &rawRecords)
	if err != nil {
		return rest.NewBadRequestError("Invalid filter parameters.", err)
	}

	records := models.NewRecordsFromNullStringMaps(collection, rawRecords)

	// expand records relations
	expands := strings.Split(c.QueryParam(expandQueryParam), ",")
	if len(expands) > 0 {
		expandErr := api.app.Dao().ExpandRecords(
			records,
			expands,
			api.expandFunc(c, requestData),
		)
		if expandErr != nil && api.app.IsDebug() {
			log.Println("Failed to expand relations: ", expandErr)
		}
	}

	result.Items = records

	event := &core.RecordsListEvent{
		HttpContext: c,
		Collection:  collection,
		Records:     records,
		Result:      result,
	}

	return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error {
		return e.HttpContext.JSON(http.StatusOK, e.Result)
	})
}

func (api *recordApi) view(c echo.Context) error {
	collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
	if collection == nil {
		return rest.NewNotFoundError("", "Missing collection context.")
	}

	admin, _ := c.Get(ContextAdminKey).(*models.Admin)
	if admin == nil && collection.ViewRule == nil {
		// only admins can access if the rule is nil
		return rest.NewForbiddenError("Only admins can perform this action.", nil)
	}

	recordId := c.PathParam("id")
	if recordId == "" {
		return rest.NewNotFoundError("", nil)
	}

	requestData := api.exportRequestData(c)

	ruleFunc := func(q *dbx.SelectQuery) error {
		if admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
			resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)
			expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
			if err != nil {
				return err
			}
			resolver.UpdateQuery(q)
			q.AndWhere(expr)
		}
		return nil
	}

	record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc)
	if fetchErr != nil || record == nil {
		return rest.NewNotFoundError("", fetchErr)
	}

	expands := strings.Split(c.QueryParam(expandQueryParam), ",")
	if len(expands) > 0 {
		expandErr := api.app.Dao().ExpandRecord(
			record,
			expands,
			api.expandFunc(c, requestData),
		)
		if expandErr != nil && api.app.IsDebug() {
			log.Println("Failed to expand relations: ", expandErr)
		}
	}

	event := &core.RecordViewEvent{
		HttpContext: c,
		Record:      record,
	}

	return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error {
		return e.HttpContext.JSON(http.StatusOK, e.Record)
	})
}

func (api *recordApi) create(c echo.Context) error {
	collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
	if collection == nil {
		return rest.NewNotFoundError("", "Missing collection context.")
	}

	admin, _ := c.Get(ContextAdminKey).(*models.Admin)
	if admin == nil && collection.CreateRule == nil {
		// only admins can access if the rule is nil
		return rest.NewForbiddenError("Only admins can perform this action.", nil)
	}

	requestData := api.exportRequestData(c)

	// temporary save the record and check it against the create rule
	if admin == nil && collection.CreateRule != nil && *collection.CreateRule != "" {
		ruleFunc := func(q *dbx.SelectQuery) error {
			resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)
			expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver)
			if err != nil {
				return err
			}
			resolver.UpdateQuery(q)
			q.AndWhere(expr)
			return nil
		}

		testRecord := models.NewRecord(collection)
		testForm := forms.NewRecordUpsert(api.app, testRecord)
		if err := testForm.LoadData(c.Request()); err != nil {
			return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
		}

		testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
			_, fetchErr := txDao.FindRecordById(collection, testRecord.Id, ruleFunc)
			return fetchErr
		})
		if testErr != nil {
			return rest.NewBadRequestError("Failed to create record.", testErr)
		}
	}

	record := models.NewRecord(collection)
	form := forms.NewRecordUpsert(api.app, record)

	// load request
	if err := form.LoadData(c.Request()); err != nil {
		return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
	}

	event := &core.RecordCreateEvent{
		HttpContext: c,
		Record:      record,
	}

	// create the record
	submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
		return func() error {
			return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
				if err := next(); err != nil {
					return rest.NewBadRequestError("Failed to create record.", err)
				}

				return e.HttpContext.JSON(http.StatusOK, e.Record)
			})
		}
	})

	if submitErr == nil {
		api.app.OnRecordAfterCreateRequest().Trigger(event)
	}

	return submitErr
}

func (api *recordApi) update(c echo.Context) error {
	collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
	if collection == nil {
		return rest.NewNotFoundError("", "Missing collection context.")
	}

	admin, _ := c.Get(ContextAdminKey).(*models.Admin)
	if admin == nil && collection.UpdateRule == nil {
		// only admins can access if the rule is nil
		return rest.NewForbiddenError("Only admins can perform this action.", nil)
	}

	recordId := c.PathParam("id")
	if recordId == "" {
		return rest.NewNotFoundError("", nil)
	}

	requestData := api.exportRequestData(c)

	ruleFunc := func(q *dbx.SelectQuery) error {
		if admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
			resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)
			expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
			if err != nil {
				return err
			}
			resolver.UpdateQuery(q)
			q.AndWhere(expr)
		}
		return nil
	}

	// fetch record
	record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc)
	if fetchErr != nil || record == nil {
		return rest.NewNotFoundError("", fetchErr)
	}

	form := forms.NewRecordUpsert(api.app, record)

	// load request
	if err := form.LoadData(c.Request()); err != nil {
		return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
	}

	event := &core.RecordUpdateEvent{
		HttpContext: c,
		Record:      record,
	}

	// update the record
	submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
		return func() error {
			return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
				if err := next(); err != nil {
					return rest.NewBadRequestError("Failed to update record.", err)
				}

				return e.HttpContext.JSON(http.StatusOK, e.Record)
			})
		}
	})

	if submitErr == nil {
		api.app.OnRecordAfterUpdateRequest().Trigger(event)
	}

	return submitErr
}

func (api *recordApi) delete(c echo.Context) error {
	collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
	if collection == nil {
		return rest.NewNotFoundError("", "Missing collection context.")
	}

	admin, _ := c.Get(ContextAdminKey).(*models.Admin)
	if admin == nil && collection.DeleteRule == nil {
		// only admins can access if the rule is nil
		return rest.NewForbiddenError("Only admins can perform this action.", nil)
	}

	recordId := c.PathParam("id")
	if recordId == "" {
		return rest.NewNotFoundError("", nil)
	}

	requestData := api.exportRequestData(c)

	ruleFunc := func(q *dbx.SelectQuery) error {
		if admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
			resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)
			expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
			if err != nil {
				return err
			}
			resolver.UpdateQuery(q)
			q.AndWhere(expr)
		}
		return nil
	}

	record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc)
	if fetchErr != nil || record == nil {
		return rest.NewNotFoundError("", fetchErr)
	}

	event := &core.RecordDeleteEvent{
		HttpContext: c,
		Record:      record,
	}

	handlerErr := api.app.OnRecordBeforeDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error {
		// delete the record
		if err := api.app.Dao().DeleteRecord(e.Record); err != nil {
			return rest.NewBadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err)
		}

		return e.HttpContext.NoContent(http.StatusNoContent)
	})

	if handlerErr == nil {
		api.app.OnRecordAfterDeleteRequest().Trigger(event)
	}

	return handlerErr
}

func (api *recordApi) exportRequestData(c echo.Context) map[string]any {
	result := map[string]any{}
	queryParams := map[string]any{}
	bodyData := map[string]any{}
	method := c.Request().Method

	echo.BindQueryParams(c, &queryParams)

	rest.BindBody(c, &bodyData)

	result["method"] = method
	result["query"] = queryParams
	result["data"] = bodyData
	result["user"] = nil

	loggedUser, _ := c.Get(ContextUserKey).(*models.User)
	if loggedUser != nil {
		result["user"], _ = loggedUser.AsMap()
	}

	return result
}

func (api *recordApi) expandFunc(c echo.Context, requestData map[string]any) daos.ExpandFetchFunc {
	admin, _ := c.Get(ContextAdminKey).(*models.Admin)

	return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) {
		return api.app.Dao().FindRecordsByIds(relCollection, relIds, func(q *dbx.SelectQuery) error {
			if admin != nil {
				return nil // admin can access everything
			}

			if relCollection.ViewRule == nil {
				return fmt.Errorf("Only admins can view collection %q records", relCollection.Name)
			}

			if *relCollection.ViewRule != "" {
				resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), relCollection, requestData)
				expr, err := search.FilterData(*(relCollection.ViewRule)).BuildExpr(resolver)
				if err != nil {
					return err
				}
				resolver.UpdateQuery(q)
				q.AndWhere(expr)
			}

			return nil
		})
	}
}
