package webhook

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"time"

	"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"
)

type Webhook struct {
	ID          string `db:"id"          json:"id"`
	Name        string `db:"name"        json:"name"`
	Collection  string `db:"collection"  json:"collection"`
	Destination string `db:"destination" json:"destination"`
}

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

type Payload struct {
	Action     string    `json:"action"`
	Collection string    `json:"collection"`
	Record     any       `json:"record"`
	Auth       *AuthUser `json:"auth,omitempty"`
	Admin      *AuthUser `json:"admin,omitempty"`
}

type AuthUser struct {
	ID                     string     `json:"id"`
	Username               string     `json:"username"`
	Active                 bool       `json:"active"`
	Name                   *string    `json:"name,omitempty"`
	Email                  *string    `json:"email,omitempty"`
	Avatar                 *string    `json:"avatar,omitempty"`
	Lastresetsentat        *time.Time `json:"lastresetsentat,omitempty"`
	Lastverificationsentat *time.Time `json:"lastverificationsentat,omitempty"`
	Created                time.Time  `json:"created"`
	Updated                time.Time  `json:"updated"`
}

func SanitizeUser(user *sqlc.User) *AuthUser {
	if user == nil {
		return nil
	}

	return &AuthUser{
		ID:                     user.ID,
		Username:               user.Username,
		Active:                 user.Active,
		Name:                   user.Name,
		Email:                  user.Email,
		Avatar:                 user.Avatar,
		Lastresetsentat:        user.Lastresetsentat,
		Lastverificationsentat: user.Lastverificationsentat,
		Created:                user.Created,
		Updated:                user.Updated,
	}
}

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

		return
	}

	webhooks, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListWebhooksRow, error) {
		return queries.ListWebhooks(ctx, sqlc.ListWebhooksParams{Limit: limit, Offset: offset})
	})
	if err != nil {
		slog.ErrorContext(ctx, "failed to list webhooks", "error", err.Error())

		return
	}

	if len(webhooks) == 0 {
		return
	}

	payload, err := json.Marshal(&Payload{
		Action:     event,
		Collection: collection,
		Record:     record,
		Auth:       SanitizeUser(user),
		Admin:      nil,
	})
	if err != nil {
		slog.ErrorContext(ctx, "failed to marshal payload", "error", err.Error())

		return
	}

	for _, webhook := range webhooks {
		if err := sendWebhook(ctx, webhook, payload); err != nil {
			slog.ErrorContext(ctx, "failed to send webhook", "action", event, "name", webhook.Name, "collection", webhook.Collection, "destination", webhook.Destination, "error", err.Error())
		} else {
			slog.InfoContext(ctx, "webhook sent", "action", event, "name", webhook.Name, "collection", webhook.Collection, "destination", webhook.Destination)
		}
	}
}

func sendWebhook(ctx context.Context, webhook sqlc.ListWebhooksRow, payload []byte) error {
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, webhook.Destination, bytes.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)

		return fmt.Errorf("failed to send webhook: %s", string(b))
	}

	return nil
}
