package apis

import (
	"errors"
	"net/http"
	"slices"
	"strings"

	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tools/auth"
	"github.com/pocketbase/pocketbase/tools/router"
	"github.com/pocketbase/pocketbase/tools/search"
	"github.com/pocketbase/pocketbase/tools/security"
)

// bindCollectionApi registers the collection api endpoints and the corresponding handlers.
func bindCollectionApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
	subGroup := rg.Group("/collections").Bind(RequireSuperuserAuth())
	subGroup.GET("", collectionsList)
	subGroup.POST("", collectionCreate)
	subGroup.GET("/{collection}", collectionView)
	subGroup.PATCH("/{collection}", collectionUpdate)
	subGroup.DELETE("/{collection}", collectionDelete)
	subGroup.DELETE("/{collection}/truncate", collectionTruncate)
	subGroup.PUT("/import", collectionsImport)
	subGroup.GET("/meta/scaffolds", collectionScaffolds)

	// @todo experimental
	subGroup.GET("/meta/oauth2-providers", collectionListOAuth2Providers)
	subGroup.POST("/meta/dry-run-view", collectionDryRunView)
}

func collectionsList(e *core.RequestEvent) error {
	fieldResolver := search.NewSimpleFieldResolver(
		"id", "created", "updated", "name", "system", "type",
	)

	collections := []*core.Collection{}

	result, err := search.NewProvider(fieldResolver).
		Query(e.App.CollectionQuery()).
		ParseAndExec(e.Request.URL.Query().Encode(), &collections)

	if err != nil {
		return e.BadRequestError("", err)
	}

	event := new(core.CollectionsListRequestEvent)
	event.RequestEvent = e
	event.Collections = collections
	event.Result = result

	return event.App.OnCollectionsListRequest().Trigger(event, func(e *core.CollectionsListRequestEvent) error {
		return execAfterSuccessTx(true, e.App, func() error {
			return e.JSON(http.StatusOK, e.Result)
		})
	})
}

func collectionView(e *core.RequestEvent) error {
	collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
	if err != nil || collection == nil {
		return e.NotFoundError("", err)
	}

	event := new(core.CollectionRequestEvent)
	event.RequestEvent = e
	event.Collection = collection

	return e.App.OnCollectionViewRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
		return execAfterSuccessTx(true, e.App, func() error {
			return e.JSON(http.StatusOK, e.Collection)
		})
	})
}

func collectionCreate(e *core.RequestEvent) error {
	// populate the minimal required factory collection data (if any)
	factoryExtract := struct {
		Type string `form:"type" json:"type"`
		Name string `form:"name" json:"name"`
	}{}
	if err := e.BindBody(&factoryExtract); err != nil {
		return e.BadRequestError("Failed to load the collection type data due to invalid formatting.", err)
	}

	// create scaffold
	collection := core.NewCollection(factoryExtract.Type, factoryExtract.Name)

	// merge the scaffold with the submitted request data
	if err := e.BindBody(collection); err != nil {
		return e.BadRequestError("Failed to load the submitted data due to invalid formatting.", err)
	}

	event := new(core.CollectionRequestEvent)
	event.RequestEvent = e
	event.Collection = collection

	return e.App.OnCollectionCreateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
		if err := e.App.Save(e.Collection); err != nil {
			// validation failure
			var validationErrors validation.Errors
			if errors.As(err, &validationErrors) {
				return e.BadRequestError("Failed to create collection.", validationErrors)
			}

			// other generic db error
			return e.BadRequestError("Failed to create collection. Raw error: \n"+err.Error(), nil)
		}

		return execAfterSuccessTx(true, e.App, func() error {
			return e.JSON(http.StatusOK, e.Collection)
		})
	})
}

func collectionUpdate(e *core.RequestEvent) error {
	collection, err := e.App.FindCollectionByNameOrId(e.Request.PathValue("collection"))
	if err != nil || collection == nil {
		return e.NotFoundError("", err)
	}

	if err := e.BindBody(collection); err != nil {
		return e.BadRequestError("Failed to load the submitted data due to invalid formatting.", err)
	}

	event := new(core.CollectionRequestEvent)
	event.RequestEvent = e
	event.Collection = collection

	return event.App.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
		if err := e.App.Save(e.Collection); err != nil {
			// validation failure
			var validationErrors validation.Errors
			if errors.As(err, &validationErrors) {
				return e.BadRequestError("Failed to update collection.", validationErrors)
			}

			// other generic db error
			return e.BadRequestError("Failed to update collection. Raw error: \n"+err.Error(), nil)
		}

		return execAfterSuccessTx(true, e.App, func() error {
			return e.JSON(http.StatusOK, e.Collection)
		})
	})
}

func collectionDelete(e *core.RequestEvent) error {
	collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
	if err != nil || collection == nil {
		return e.NotFoundError("", err)
	}

	event := new(core.CollectionRequestEvent)
	event.RequestEvent = e
	event.Collection = collection

	return e.App.OnCollectionDeleteRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
		if err := e.App.Delete(e.Collection); err != nil {
			msg := "Failed to delete collection"

			// check fo references
			refs, _ := e.App.FindCollectionReferences(e.Collection, e.Collection.Id)
			if len(refs) > 0 {
				names := make([]string, 0, len(refs))
				for ref := range refs {
					names = append(names, ref.Name)
				}
				msg += " probably due to existing reference in " + strings.Join(names, ", ")
			}

			return e.BadRequestError(msg, err)
		}

		return execAfterSuccessTx(true, e.App, func() error {
			return e.NoContent(http.StatusNoContent)
		})
	})
}

func collectionTruncate(e *core.RequestEvent) error {
	collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
	if err != nil || collection == nil {
		return e.NotFoundError("", err)
	}

	if collection.IsView() {
		return e.BadRequestError("View collections cannot be truncated since they don't store their own records.", nil)
	}

	err = e.App.TruncateCollection(collection)
	if err != nil {
		return e.BadRequestError("Failed to truncate collection (most likely due to required cascade delete record references).", err)
	}

	return e.NoContent(http.StatusNoContent)
}

func collectionScaffolds(e *core.RequestEvent) error {
	randomId := security.RandomStringWithAlphabet(10, core.DefaultIdAlphabet) // could be used as part of the default indexes name

	collections := map[string]*core.Collection{
		core.CollectionTypeBase: core.NewBaseCollection("", randomId),
		core.CollectionTypeAuth: core.NewAuthCollection("", randomId),
		core.CollectionTypeView: core.NewViewCollection("", randomId),
	}

	for _, c := range collections {
		c.Id = "" // clear random id
	}

	return e.JSON(http.StatusOK, collections)
}

type providerListItem struct {
	order int

	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	Logo        string `json:"logo"`
}

func collectionListOAuth2Providers(e *core.RequestEvent) error {
	providers := make([]*providerListItem, 0, len(auth.Providers))

	for name, factory := range auth.Providers {
		p := factory()

		providers = append(providers, &providerListItem{
			order:       p.Order(),
			Name:        name,
			DisplayName: p.DisplayName(),
			Logo:        p.Logo(),
		})
	}

	slices.SortStableFunc(providers, func(a, b *providerListItem) int {
		// sort by order
		if a.order < b.order {
			return -1
		}
		if a.order > b.order {
			return 1
		}

		// fallback sort by name
		if a.Name < b.Name {
			return -1
		}
		if a.Name > b.Name {
			return 1
		}

		return 0
	})

	return e.JSON(http.StatusOK, providers)
}

func collectionDryRunView(e *core.RequestEvent) error {
	// extra precaution in case reused in custom route group
	if !e.HasSuperuserAuth() {
		return e.ForbiddenError("", nil)
	}

	form := dryRunViewForm{}

	err := e.BindBody(&form)
	if err != nil {
		return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err))
	}

	err = form.validate()
	if err != nil {
		return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err))
	}

	result, err := e.App.DryRunView(form.Query, 10)
	if err != nil {
		return firstApiError(err, e.BadRequestError("Invalid view query. Raw error: \n"+err.Error(), nil))
	}

	return e.JSON(http.StatusOK, result)
}

type dryRunViewForm struct {
	Query string `form:"query" json:"query"`
}

func (form *dryRunViewForm) validate() error {
	return validation.ValidateStruct(form,
		validation.Field(&form.Query, validation.Required, validation.Length(0, 5000)),
	)
}
