package hook

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"slices"

	"github.com/SecurityBrewery/catalyst/app/auth/usercontext"
	"github.com/SecurityBrewery/catalyst/app/database"
	"github.com/SecurityBrewery/catalyst/app/database/sqlc"
	"github.com/SecurityBrewery/catalyst/app/hook"
	"github.com/SecurityBrewery/catalyst/app/reaction/action"
	"github.com/SecurityBrewery/catalyst/app/settings"
	"github.com/SecurityBrewery/catalyst/app/webhook"
)

type Hook struct {
	Collections []string `json:"collections"`
	Events      []string `json:"events"`
}

func BindHooks(hooks *hook.Hooks, queries *sqlc.Queries, test bool) {
	hooks.OnRecordAfterCreateRequest.Subscribe(func(ctx context.Context, table string, record any) {
		bindHook(ctx, queries, database.CreateAction, table, record, test)
	})
	hooks.OnRecordAfterUpdateRequest.Subscribe(func(ctx context.Context, table string, record any) {
		bindHook(ctx, queries, database.UpdateAction, table, record, test)
	})
	hooks.OnRecordAfterDeleteRequest.Subscribe(func(ctx context.Context, table string, record any) {
		bindHook(ctx, queries, database.DeleteAction, table, record, test)
	})
}

func bindHook(ctx context.Context, queries *sqlc.Queries, event, collection string, record any, test bool) {
	user, ok := usercontext.UserFromContext(ctx)
	if !ok {
		slog.ErrorContext(ctx, "failed to get user from session")

		return
	}

	if !test {
		go mustRunHook(context.Background(), queries, collection, event, record, user) //nolint:contextcheck
	} else {
		mustRunHook(ctx, queries, collection, event, record, user)
	}
}

func mustRunHook(ctx context.Context, queries *sqlc.Queries, collection, event string, record any, auth *sqlc.User) {
	if err := runHook(ctx, queries, collection, event, record, auth); err != nil {
		slog.ErrorContext(ctx, fmt.Sprintf("failed to run hook reaction: %v", err))
	}
}

func runHook(ctx context.Context, queries *sqlc.Queries, collection, event string, record any, auth *sqlc.User) error {
	payload, err := json.Marshal(&webhook.Payload{
		Action:     event,
		Collection: collection,
		Record:     record,
		Auth:       webhook.SanitizeUser(auth),
		Admin:      nil,
	})
	if err != nil {
		return fmt.Errorf("failed to marshal webhook payload: %w", err)
	}

	hooks, err := findByHookTrigger(ctx, queries, collection, event)
	if err != nil {
		return fmt.Errorf("failed to find hook by trigger: %w", err)
	}

	if len(hooks) == 0 {
		return nil
	}

	settings, err := settings.Load(ctx, queries)
	if err != nil {
		return fmt.Errorf("failed to load settings: %w", err)
	}

	var errs []error

	for _, hook := range hooks {
		_, err = action.Run(ctx, settings.Meta.AppURL, queries, hook.Action, hook.Actiondata, payload)
		if err != nil {
			errs = append(errs, fmt.Errorf("failed to run hook reaction: %w", err))
		}
	}

	return errors.Join(errs...)
}

func findByHookTrigger(ctx context.Context, queries *sqlc.Queries, collection, event string) ([]*sqlc.ListReactionsByTriggerRow, error) {
	reactions, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListReactionsByTriggerRow, error) {
		return queries.ListReactionsByTrigger(ctx, sqlc.ListReactionsByTriggerParams{Trigger: "hook", Limit: limit, Offset: offset})
	})
	if err != nil {
		return nil, fmt.Errorf("failed to find hook reaction: %w", err)
	}

	if len(reactions) == 0 {
		return nil, nil
	}

	var matchedRecords []*sqlc.ListReactionsByTriggerRow

	for _, reaction := range reactions {
		var hook Hook
		if err := json.Unmarshal(reaction.Triggerdata, &hook); err != nil {
			return nil, err
		}

		if slices.Contains(hook.Collections, collection) && slices.Contains(hook.Events, event) {
			matchedRecords = append(matchedRecords, &reaction)
		}
	}

	return matchedRecords, nil
}
