package core

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/fatih/color"
	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/daos"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/tools/filesystem"
	"github.com/pocketbase/pocketbase/tools/hook"
	"github.com/pocketbase/pocketbase/tools/mailer"
	"github.com/pocketbase/pocketbase/tools/security"
	"github.com/pocketbase/pocketbase/tools/store"
	"github.com/pocketbase/pocketbase/tools/subscriptions"
)

var _ App = (*BaseApp)(nil)

// BaseApp implements core.App and defines the base PocketBase app structure.
type BaseApp struct {
	// configurable parameters
	isDebug       bool
	dataDir       string
	encryptionEnv string

	// internals
	cache               *store.Store[any]
	settings            *Settings
	db                  *dbx.DB
	dao                 *daos.Dao
	logsDB              *dbx.DB
	logsDao             *daos.Dao
	subscriptionsBroker *subscriptions.Broker

	// serve event hooks
	onBeforeServe *hook.Hook[*ServeEvent]

	// dao event hooks
	onModelBeforeCreate *hook.Hook[*ModelEvent]
	onModelAfterCreate  *hook.Hook[*ModelEvent]
	onModelBeforeUpdate *hook.Hook[*ModelEvent]
	onModelAfterUpdate  *hook.Hook[*ModelEvent]
	onModelBeforeDelete *hook.Hook[*ModelEvent]
	onModelAfterDelete  *hook.Hook[*ModelEvent]

	// mailer event hooks
	onMailerBeforeAdminResetPasswordSend *hook.Hook[*MailerAdminEvent]
	onMailerAfterAdminResetPasswordSend  *hook.Hook[*MailerAdminEvent]
	onMailerBeforeUserResetPasswordSend  *hook.Hook[*MailerUserEvent]
	onMailerAfterUserResetPasswordSend   *hook.Hook[*MailerUserEvent]
	onMailerBeforeUserVerificationSend   *hook.Hook[*MailerUserEvent]
	onMailerAfterUserVerificationSend    *hook.Hook[*MailerUserEvent]
	onMailerBeforeUserChangeEmailSend    *hook.Hook[*MailerUserEvent]
	onMailerAfterUserChangeEmailSend     *hook.Hook[*MailerUserEvent]

	// realtime api event hooks
	onRealtimeConnectRequest         *hook.Hook[*RealtimeConnectEvent]
	onRealtimeBeforeSubscribeRequest *hook.Hook[*RealtimeSubscribeEvent]
	onRealtimeAfterSubscribeRequest  *hook.Hook[*RealtimeSubscribeEvent]

	// settings api event hooks
	onSettingsListRequest         *hook.Hook[*SettingsListEvent]
	onSettingsBeforeUpdateRequest *hook.Hook[*SettingsUpdateEvent]
	onSettingsAfterUpdateRequest  *hook.Hook[*SettingsUpdateEvent]

	// file api event hooks
	onFileDownloadRequest *hook.Hook[*FileDownloadEvent]

	// admin api event hooks
	onAdminsListRequest        *hook.Hook[*AdminsListEvent]
	onAdminViewRequest         *hook.Hook[*AdminViewEvent]
	onAdminBeforeCreateRequest *hook.Hook[*AdminCreateEvent]
	onAdminAfterCreateRequest  *hook.Hook[*AdminCreateEvent]
	onAdminBeforeUpdateRequest *hook.Hook[*AdminUpdateEvent]
	onAdminAfterUpdateRequest  *hook.Hook[*AdminUpdateEvent]
	onAdminBeforeDeleteRequest *hook.Hook[*AdminDeleteEvent]
	onAdminAfterDeleteRequest  *hook.Hook[*AdminDeleteEvent]
	onAdminAuthRequest         *hook.Hook[*AdminAuthEvent]

	// user api event hooks
	onUsersListRequest         *hook.Hook[*UsersListEvent]
	onUserViewRequest          *hook.Hook[*UserViewEvent]
	onUserBeforeCreateRequest  *hook.Hook[*UserCreateEvent]
	onUserAfterCreateRequest   *hook.Hook[*UserCreateEvent]
	onUserBeforeUpdateRequest  *hook.Hook[*UserUpdateEvent]
	onUserAfterUpdateRequest   *hook.Hook[*UserUpdateEvent]
	onUserBeforeDeleteRequest  *hook.Hook[*UserDeleteEvent]
	onUserAfterDeleteRequest   *hook.Hook[*UserDeleteEvent]
	onUserAuthRequest          *hook.Hook[*UserAuthEvent]
	onUserBeforeOauth2Register *hook.Hook[*UserOauth2RegisterEvent]
	onUserAfterOauth2Register  *hook.Hook[*UserOauth2RegisterEvent]

	// record api event hooks
	onRecordsListRequest        *hook.Hook[*RecordsListEvent]
	onRecordViewRequest         *hook.Hook[*RecordViewEvent]
	onRecordBeforeCreateRequest *hook.Hook[*RecordCreateEvent]
	onRecordAfterCreateRequest  *hook.Hook[*RecordCreateEvent]
	onRecordBeforeUpdateRequest *hook.Hook[*RecordUpdateEvent]
	onRecordAfterUpdateRequest  *hook.Hook[*RecordUpdateEvent]
	onRecordBeforeDeleteRequest *hook.Hook[*RecordDeleteEvent]
	onRecordAfterDeleteRequest  *hook.Hook[*RecordDeleteEvent]

	// collection api event hooks
	onCollectionsListRequest        *hook.Hook[*CollectionsListEvent]
	onCollectionViewRequest         *hook.Hook[*CollectionViewEvent]
	onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent]
	onCollectionAfterCreateRequest  *hook.Hook[*CollectionCreateEvent]
	onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent]
	onCollectionAfterUpdateRequest  *hook.Hook[*CollectionUpdateEvent]
	onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent]
	onCollectionAfterDeleteRequest  *hook.Hook[*CollectionDeleteEvent]
}

// NewBaseApp creates and returns a new BaseApp instance
// configured with the provided arguments.
//
// To initialize the app, you need to call `app.Bootsrap()`.
func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
	app := &BaseApp{
		dataDir:             dataDir,
		isDebug:             isDebug,
		encryptionEnv:       encryptionEnv,
		cache:               store.New[any](nil),
		settings:            NewSettings(),
		subscriptionsBroker: subscriptions.NewBroker(),

		// serve event hooks
		onBeforeServe: &hook.Hook[*ServeEvent]{},

		// dao event hooks
		onModelBeforeCreate: &hook.Hook[*ModelEvent]{},
		onModelAfterCreate:  &hook.Hook[*ModelEvent]{},
		onModelBeforeUpdate: &hook.Hook[*ModelEvent]{},
		onModelAfterUpdate:  &hook.Hook[*ModelEvent]{},
		onModelBeforeDelete: &hook.Hook[*ModelEvent]{},
		onModelAfterDelete:  &hook.Hook[*ModelEvent]{},

		// mailer event hooks
		onMailerBeforeAdminResetPasswordSend: &hook.Hook[*MailerAdminEvent]{},
		onMailerAfterAdminResetPasswordSend:  &hook.Hook[*MailerAdminEvent]{},
		onMailerBeforeUserResetPasswordSend:  &hook.Hook[*MailerUserEvent]{},
		onMailerAfterUserResetPasswordSend:   &hook.Hook[*MailerUserEvent]{},
		onMailerBeforeUserVerificationSend:   &hook.Hook[*MailerUserEvent]{},
		onMailerAfterUserVerificationSend:    &hook.Hook[*MailerUserEvent]{},
		onMailerBeforeUserChangeEmailSend:    &hook.Hook[*MailerUserEvent]{},
		onMailerAfterUserChangeEmailSend:     &hook.Hook[*MailerUserEvent]{},

		// realtime API event hooks
		onRealtimeConnectRequest:         &hook.Hook[*RealtimeConnectEvent]{},
		onRealtimeBeforeSubscribeRequest: &hook.Hook[*RealtimeSubscribeEvent]{},
		onRealtimeAfterSubscribeRequest:  &hook.Hook[*RealtimeSubscribeEvent]{},

		// settings API event hooks
		onSettingsListRequest:         &hook.Hook[*SettingsListEvent]{},
		onSettingsBeforeUpdateRequest: &hook.Hook[*SettingsUpdateEvent]{},
		onSettingsAfterUpdateRequest:  &hook.Hook[*SettingsUpdateEvent]{},

		// file API event hooks
		onFileDownloadRequest: &hook.Hook[*FileDownloadEvent]{},

		// admin API event hooks
		onAdminsListRequest:        &hook.Hook[*AdminsListEvent]{},
		onAdminViewRequest:         &hook.Hook[*AdminViewEvent]{},
		onAdminBeforeCreateRequest: &hook.Hook[*AdminCreateEvent]{},
		onAdminAfterCreateRequest:  &hook.Hook[*AdminCreateEvent]{},
		onAdminBeforeUpdateRequest: &hook.Hook[*AdminUpdateEvent]{},
		onAdminAfterUpdateRequest:  &hook.Hook[*AdminUpdateEvent]{},
		onAdminBeforeDeleteRequest: &hook.Hook[*AdminDeleteEvent]{},
		onAdminAfterDeleteRequest:  &hook.Hook[*AdminDeleteEvent]{},
		onAdminAuthRequest:         &hook.Hook[*AdminAuthEvent]{},

		// user API event hooks
		onUsersListRequest:         &hook.Hook[*UsersListEvent]{},
		onUserViewRequest:          &hook.Hook[*UserViewEvent]{},
		onUserBeforeCreateRequest:  &hook.Hook[*UserCreateEvent]{},
		onUserAfterCreateRequest:   &hook.Hook[*UserCreateEvent]{},
		onUserBeforeUpdateRequest:  &hook.Hook[*UserUpdateEvent]{},
		onUserAfterUpdateRequest:   &hook.Hook[*UserUpdateEvent]{},
		onUserBeforeDeleteRequest:  &hook.Hook[*UserDeleteEvent]{},
		onUserAfterDeleteRequest:   &hook.Hook[*UserDeleteEvent]{},
		onUserAuthRequest:          &hook.Hook[*UserAuthEvent]{},
		onUserBeforeOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{},
		onUserAfterOauth2Register:  &hook.Hook[*UserOauth2RegisterEvent]{},

		// record API event hooks
		onRecordsListRequest:        &hook.Hook[*RecordsListEvent]{},
		onRecordViewRequest:         &hook.Hook[*RecordViewEvent]{},
		onRecordBeforeCreateRequest: &hook.Hook[*RecordCreateEvent]{},
		onRecordAfterCreateRequest:  &hook.Hook[*RecordCreateEvent]{},
		onRecordBeforeUpdateRequest: &hook.Hook[*RecordUpdateEvent]{},
		onRecordAfterUpdateRequest:  &hook.Hook[*RecordUpdateEvent]{},
		onRecordBeforeDeleteRequest: &hook.Hook[*RecordDeleteEvent]{},
		onRecordAfterDeleteRequest:  &hook.Hook[*RecordDeleteEvent]{},

		// collection API event hooks
		onCollectionsListRequest:        &hook.Hook[*CollectionsListEvent]{},
		onCollectionViewRequest:         &hook.Hook[*CollectionViewEvent]{},
		onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{},
		onCollectionAfterCreateRequest:  &hook.Hook[*CollectionCreateEvent]{},
		onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{},
		onCollectionAfterUpdateRequest:  &hook.Hook[*CollectionUpdateEvent]{},
		onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{},
		onCollectionAfterDeleteRequest:  &hook.Hook[*CollectionDeleteEvent]{},
	}

	app.registerDefaultHooks()

	return app
}

// Bootstrap initializes the application
// (aka. create data dir, open db connections, load settings, etc.)
func (app *BaseApp) Bootstrap() error {
	// clear resources of previous core state (if any)
	if err := app.ResetBootstrapState(); err != nil {
		return err
	}

	// ensure that data dir exist
	if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil {
		return err
	}

	if err := app.initDataDB(); err != nil {
		return err
	}

	if err := app.initLogsDB(); err != nil {
		return err
	}

	// we don't check for an error because the db migrations may
	// have not been executed yet.
	app.RefreshSettings()

	return nil
}

// ResetBootstrapState takes care for releasing initialized app resources
// (eg. closing db connections).
func (app *BaseApp) ResetBootstrapState() error {
	if app.db != nil {
		if err := app.db.Close(); err != nil {
			return err
		}
	}

	if app.logsDB != nil {
		if err := app.logsDB.Close(); err != nil {
			return err
		}
	}

	app.dao = nil
	app.logsDao = nil
	app.settings = nil

	return nil
}

// DB returns the default app database instance.
func (app *BaseApp) DB() *dbx.DB {
	return app.db
}

// Dao returns the default app Dao instance.
func (app *BaseApp) Dao() *daos.Dao {
	return app.dao
}

// LogsDB returns the app logs database instance.
func (app *BaseApp) LogsDB() *dbx.DB {
	return app.logsDB
}

// LogsDao returns the app logs Dao instance.
func (app *BaseApp) LogsDao() *daos.Dao {
	return app.logsDao
}

// DataDir returns the app data directory path.
func (app *BaseApp) DataDir() string {
	return app.dataDir
}

// EncryptionEnv returns the name of the app secret env key
// (used for settings encryption).
func (app *BaseApp) EncryptionEnv() string {
	return app.encryptionEnv
}

// IsDebug returns whether the app is in debug mode
// (showing more detailed error logs, executed sql statements, etc.).
func (app *BaseApp) IsDebug() bool {
	return app.isDebug
}

// Settings returns the loaded app settings.
func (app *BaseApp) Settings() *Settings {
	return app.settings
}

// Cache returns the app internal cache store.
func (app *BaseApp) Cache() *store.Store[any] {
	return app.cache
}

// SubscriptionsBroker returns the app realtime subscriptions broker instance.
func (app *BaseApp) SubscriptionsBroker() *subscriptions.Broker {
	return app.subscriptionsBroker
}

// NewMailClient creates and returns a new SMTP or Sendmail client
// based on the current app settings.
func (app *BaseApp) NewMailClient() mailer.Mailer {
	if app.Settings().Smtp.Enabled {
		return mailer.NewSmtpClient(
			app.Settings().Smtp.Host,
			app.Settings().Smtp.Port,
			app.Settings().Smtp.Username,
			app.Settings().Smtp.Password,
			app.Settings().Smtp.Tls,
		)
	}

	return &mailer.Sendmail{}
}

// NewFilesystem creates a new local or S3 filesystem instance
// based on the current app settings.
//
// NB! Make sure to call `Close()` on the returned result
// after you are done working with it.
func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
	if app.settings.S3.Enabled {
		return filesystem.NewS3(
			app.settings.S3.Bucket,
			app.settings.S3.Region,
			app.settings.S3.Endpoint,
			app.settings.S3.AccessKey,
			app.settings.S3.Secret,
		)
	}

	// fallback to local filesystem
	return filesystem.NewLocal(filepath.Join(app.DataDir(), "storage"))
}

// RefreshSettings reinitializes and reloads the stored application settings.
func (app *BaseApp) RefreshSettings() error {
	if app.settings == nil {
		app.settings = NewSettings()
	}

	encryptionKey := os.Getenv(app.EncryptionEnv())

	param, err := app.Dao().FindParamByKey(models.ParamAppSettings)
	if err != nil && err != sql.ErrNoRows {
		return err
	}

	if param == nil {
		// no settings were previously stored
		return app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey)
	}

	// load the settings from the stored param into the app ones
	// ---
	newSettings := NewSettings()

	// try first without decryption
	plainDecodeErr := json.Unmarshal(param.Value, newSettings)

	// failed, try to decrypt
	if plainDecodeErr != nil {
		// load without decrypt has failed and there is no encryption key to use for decrypt
		if encryptionKey == "" {
			return errors.New("Failed to load the stored app settings (missing or invalid encryption key).")
		}

		// decrypt
		decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey)
		if decryptErr != nil {
			return decryptErr
		}

		// decode again
		decryptedDecodeErr := json.Unmarshal(decrypted, newSettings)
		if decryptedDecodeErr != nil {
			return decryptedDecodeErr
		}
	}

	if err := app.settings.Merge(newSettings); err != nil {
		return err
	}

	if plainDecodeErr == nil && encryptionKey != "" {
		// save because previously the settings weren't stored encrypted
		saveErr := app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey)
		if saveErr != nil {
			return saveErr
		}
	}

	return nil
}

// -------------------------------------------------------------------
// Serve event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] {
	return app.onBeforeServe
}

// -------------------------------------------------------------------
// Dao event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnModelBeforeCreate() *hook.Hook[*ModelEvent] {
	return app.onModelBeforeCreate
}

func (app *BaseApp) OnModelAfterCreate() *hook.Hook[*ModelEvent] {
	return app.onModelAfterCreate
}

func (app *BaseApp) OnModelBeforeUpdate() *hook.Hook[*ModelEvent] {
	return app.onModelBeforeUpdate
}

func (app *BaseApp) OnModelAfterUpdate() *hook.Hook[*ModelEvent] {
	return app.onModelAfterUpdate
}

func (app *BaseApp) OnModelBeforeDelete() *hook.Hook[*ModelEvent] {
	return app.onModelBeforeDelete
}

func (app *BaseApp) OnModelAfterDelete() *hook.Hook[*ModelEvent] {
	return app.onModelAfterDelete
}

// -------------------------------------------------------------------
// Mailer event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] {
	return app.onMailerBeforeAdminResetPasswordSend
}

func (app *BaseApp) OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] {
	return app.onMailerAfterAdminResetPasswordSend
}

func (app *BaseApp) OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerBeforeUserResetPasswordSend
}

func (app *BaseApp) OnMailerAfterUserResetPasswordSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerAfterUserResetPasswordSend
}

func (app *BaseApp) OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerBeforeUserVerificationSend
}

func (app *BaseApp) OnMailerAfterUserVerificationSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerAfterUserVerificationSend
}

func (app *BaseApp) OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerBeforeUserChangeEmailSend
}

func (app *BaseApp) OnMailerAfterUserChangeEmailSend() *hook.Hook[*MailerUserEvent] {
	return app.onMailerAfterUserChangeEmailSend
}

// -------------------------------------------------------------------
// Realtime API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectEvent] {
	return app.onRealtimeConnectRequest
}

func (app *BaseApp) OnRealtimeBeforeSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] {
	return app.onRealtimeBeforeSubscribeRequest
}

func (app *BaseApp) OnRealtimeAfterSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] {
	return app.onRealtimeAfterSubscribeRequest
}

// -------------------------------------------------------------------
// Settings API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnSettingsListRequest() *hook.Hook[*SettingsListEvent] {
	return app.onSettingsListRequest
}

func (app *BaseApp) OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] {
	return app.onSettingsBeforeUpdateRequest
}

func (app *BaseApp) OnSettingsAfterUpdateRequest() *hook.Hook[*SettingsUpdateEvent] {
	return app.onSettingsAfterUpdateRequest
}

// -------------------------------------------------------------------
// File API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnFileDownloadRequest() *hook.Hook[*FileDownloadEvent] {
	return app.onFileDownloadRequest
}

// -------------------------------------------------------------------
// Admin API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnAdminsListRequest() *hook.Hook[*AdminsListEvent] {
	return app.onAdminsListRequest
}

func (app *BaseApp) OnAdminViewRequest() *hook.Hook[*AdminViewEvent] {
	return app.onAdminViewRequest
}

func (app *BaseApp) OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] {
	return app.onAdminBeforeCreateRequest
}

func (app *BaseApp) OnAdminAfterCreateRequest() *hook.Hook[*AdminCreateEvent] {
	return app.onAdminAfterCreateRequest
}

func (app *BaseApp) OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] {
	return app.onAdminBeforeUpdateRequest
}

func (app *BaseApp) OnAdminAfterUpdateRequest() *hook.Hook[*AdminUpdateEvent] {
	return app.onAdminAfterUpdateRequest
}

func (app *BaseApp) OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] {
	return app.onAdminBeforeDeleteRequest
}

func (app *BaseApp) OnAdminAfterDeleteRequest() *hook.Hook[*AdminDeleteEvent] {
	return app.onAdminAfterDeleteRequest
}

func (app *BaseApp) OnAdminAuthRequest() *hook.Hook[*AdminAuthEvent] {
	return app.onAdminAuthRequest
}

// -------------------------------------------------------------------
// User API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnUsersListRequest() *hook.Hook[*UsersListEvent] {
	return app.onUsersListRequest
}

func (app *BaseApp) OnUserViewRequest() *hook.Hook[*UserViewEvent] {
	return app.onUserViewRequest
}

func (app *BaseApp) OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent] {
	return app.onUserBeforeCreateRequest
}

func (app *BaseApp) OnUserAfterCreateRequest() *hook.Hook[*UserCreateEvent] {
	return app.onUserAfterCreateRequest
}

func (app *BaseApp) OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent] {
	return app.onUserBeforeUpdateRequest
}

func (app *BaseApp) OnUserAfterUpdateRequest() *hook.Hook[*UserUpdateEvent] {
	return app.onUserAfterUpdateRequest
}

func (app *BaseApp) OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent] {
	return app.onUserBeforeDeleteRequest
}

func (app *BaseApp) OnUserAfterDeleteRequest() *hook.Hook[*UserDeleteEvent] {
	return app.onUserAfterDeleteRequest
}

func (app *BaseApp) OnUserAuthRequest() *hook.Hook[*UserAuthEvent] {
	return app.onUserAuthRequest
}

func (app *BaseApp) OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
	return app.onUserBeforeOauth2Register
}

func (app *BaseApp) OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
	return app.onUserAfterOauth2Register
}

// -------------------------------------------------------------------
// Record API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnRecordsListRequest() *hook.Hook[*RecordsListEvent] {
	return app.onRecordsListRequest
}

func (app *BaseApp) OnRecordViewRequest() *hook.Hook[*RecordViewEvent] {
	return app.onRecordViewRequest
}

func (app *BaseApp) OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent] {
	return app.onRecordBeforeCreateRequest
}

func (app *BaseApp) OnRecordAfterCreateRequest() *hook.Hook[*RecordCreateEvent] {
	return app.onRecordAfterCreateRequest
}

func (app *BaseApp) OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent] {
	return app.onRecordBeforeUpdateRequest
}

func (app *BaseApp) OnRecordAfterUpdateRequest() *hook.Hook[*RecordUpdateEvent] {
	return app.onRecordAfterUpdateRequest
}

func (app *BaseApp) OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent] {
	return app.onRecordBeforeDeleteRequest
}

func (app *BaseApp) OnRecordAfterDeleteRequest() *hook.Hook[*RecordDeleteEvent] {
	return app.onRecordAfterDeleteRequest
}

// -------------------------------------------------------------------
// Collection API event hooks
// -------------------------------------------------------------------

func (app *BaseApp) OnCollectionsListRequest() *hook.Hook[*CollectionsListEvent] {
	return app.onCollectionsListRequest
}

func (app *BaseApp) OnCollectionViewRequest() *hook.Hook[*CollectionViewEvent] {
	return app.onCollectionViewRequest
}

func (app *BaseApp) OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] {
	return app.onCollectionBeforeCreateRequest
}

func (app *BaseApp) OnCollectionAfterCreateRequest() *hook.Hook[*CollectionCreateEvent] {
	return app.onCollectionAfterCreateRequest
}

func (app *BaseApp) OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] {
	return app.onCollectionBeforeUpdateRequest
}

func (app *BaseApp) OnCollectionAfterUpdateRequest() *hook.Hook[*CollectionUpdateEvent] {
	return app.onCollectionAfterUpdateRequest
}

func (app *BaseApp) OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] {
	return app.onCollectionBeforeDeleteRequest
}

func (app *BaseApp) OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] {
	return app.onCollectionAfterDeleteRequest
}

// -------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------

func (app *BaseApp) initLogsDB() error {
	var connectErr error
	app.logsDB, connectErr = connectDB(filepath.Join(app.DataDir(), "logs.db"))
	if connectErr != nil {
		return connectErr
	}

	app.logsDao = app.createDao(app.logsDB)

	return nil
}

func (app *BaseApp) initDataDB() error {
	var connectErr error
	app.db, connectErr = connectDB(filepath.Join(app.DataDir(), "data.db"))
	if connectErr != nil {
		return connectErr
	}

	app.db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
		if app.IsDebug() {
			color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql)
		}
	}

	app.db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
		if app.IsDebug() {
			color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql)
		}
	}

	app.dao = app.createDao(app.db)

	return nil
}

func (app *BaseApp) createDao(db dbx.Builder) *daos.Dao {
	dao := daos.New(db)

	dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		return app.OnModelBeforeCreate().Trigger(&ModelEvent{eventDao, m})
	}

	dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) {
		app.OnModelAfterCreate().Trigger(&ModelEvent{eventDao, m})
	}

	dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		return app.OnModelBeforeUpdate().Trigger(&ModelEvent{eventDao, m})
	}

	dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) {
		app.OnModelAfterUpdate().Trigger(&ModelEvent{eventDao, m})
	}

	dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		return app.OnModelBeforeDelete().Trigger(&ModelEvent{eventDao, m})
	}

	dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) {
		app.OnModelAfterDelete().Trigger(&ModelEvent{eventDao, m})
	}

	return dao
}

func (app *BaseApp) registerDefaultHooks() {
	deletePrefix := func(prefix string) error {
		fs, err := app.NewFilesystem()
		if err != nil {
			return err
		}
		defer fs.Close()

		failed := fs.DeletePrefix(prefix)
		if len(failed) > 0 {
			return errors.New("Failed to delete the files at " + prefix)
		}

		return nil
	}

	// delete storage files from deleted Collection, Records, etc.
	app.OnModelAfterDelete().Add(func(e *ModelEvent) error {
		if m, ok := e.Model.(models.FilesManager); ok && m.BaseFilesPath() != "" {
			if err := deletePrefix(m.BaseFilesPath()); err != nil && app.IsDebug() {
				// non critical error - only log for debug
				// (usually could happen because of S3 api limits)
				log.Println(err)
			}
		}

		return nil
	})
}
