package webhook

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"strings"
	"time"

	"ymhut-box/server/feedback-mailer/internal/config"
	"ymhut-box/server/feedback-mailer/internal/db"
)

type Dispatcher struct {
	cfg   *config.Config
	store *db.Store
}

type Event struct {
	Event      string `json:"event"`
	Delivery   string `json:"delivery"`
	OccurredAt string `json:"occurredAt"`
	Data       any    `json:"data"`
}

func NewDispatcher(cfg *config.Config, store *db.Store) *Dispatcher {
	return &Dispatcher{cfg: cfg, store: store}
}

func (d *Dispatcher) Dispatch(event string, data any) {
	if d == nil || len(d.cfg.Webhooks) == 0 {
		return
	}
	for _, hook := range d.cfg.Webhooks {
		if !hook.Enabled || hook.URL == "" || !matchesEvent(hook.Events, event) {
			continue
		}
		hookCopy := hook
		go d.Deliver(hookCopy, event, data)
	}
}

func (d *Dispatcher) DispatchSync(event string, data any) {
	if d == nil || len(d.cfg.Webhooks) == 0 {
		return
	}
	for _, hook := range d.cfg.Webhooks {
		if !hook.Enabled || hook.URL == "" || !matchesEvent(hook.Events, event) {
			continue
		}
		d.Deliver(hook, event, data)
	}
}

func (d *Dispatcher) DispatchTest(data any) int {
	if d == nil || len(d.cfg.Webhooks) == 0 {
		return 0
	}
	count := 0
	for _, hook := range d.cfg.Webhooks {
		if !hook.Enabled || hook.URL == "" {
			continue
		}
		d.Deliver(hook, "feedback.test", data)
		count++
	}
	return count
}

func (d *Dispatcher) Deliver(hook config.WebhookConfig, event string, data any) {
	if d == nil || hook.URL == "" {
		return
	}
	payload, deliveryKey := buildPayload(event, data)
	sum := sha256.Sum256(payload)
	id, err := d.store.InsertWebhookDelivery(db.WebhookDelivery{
		WebhookName:   hook.Name,
		Event:         event,
		Status:        "pending",
		PayloadSHA256: hex.EncodeToString(sum[:]),
		CreatedAt:     db.Now(),
	})
	if err != nil {
		return
	}

	maxRetries := hook.MaxRetries
	if maxRetries < 0 {
		maxRetries = 0
	}
	attempts := 0
	status := "failed"
	responseCode := 0
	errorMessage := ""
	for attempts <= maxRetries {
		attempts++
		code, err := postJSON(hook, event, deliveryKey, payload)
		responseCode = code
		if err == nil && code >= 200 && code < 300 {
			status = "sent"
			errorMessage = ""
			break
		}
		if err != nil {
			errorMessage = err.Error()
		} else {
			errorMessage = "webhook returned HTTP " + http.StatusText(code)
			if errorMessage == "webhook returned HTTP " {
				errorMessage = "webhook returned HTTP status"
			}
		}
		if attempts <= maxRetries {
			time.Sleep(time.Duration(attempts) * 350 * time.Millisecond)
		}
	}
	_ = d.store.FinishWebhookDelivery(id, status, attempts, responseCode, errorMessage)
}

func buildPayload(event string, data any) ([]byte, string) {
	now := db.Now()
	rawDelivery := sha256.Sum256([]byte(event + "\n" + now + "\n" + db.ToJSON(data)))
	delivery := hex.EncodeToString(rawDelivery[:16])
	payload := Event{Event: event, Delivery: delivery, OccurredAt: now, Data: data}
	encoded, err := json.Marshal(payload)
	if err != nil {
		return []byte(`{"event":"` + event + `"}`), delivery
	}
	return encoded, delivery
}

func postJSON(hook config.WebhookConfig, event, delivery string, body []byte) (int, error) {
	timeout := hook.TimeoutSeconds
	if timeout <= 0 {
		timeout = 5
	}
	request, err := http.NewRequest(http.MethodPost, hook.URL, bytes.NewReader(body))
	if err != nil {
		return 0, err
	}
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("User-Agent", "YMhut-Feedback-Webhook/1.0")
	request.Header.Set("X-YMhut-Event", event)
	request.Header.Set("X-YMhut-Delivery", delivery)
	request.Header.Set("X-YMhut-Signature", sign(hook.Secret, body))

	client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
	response, err := client.Do(request)
	if err != nil {
		return 0, err
	}
	defer response.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
	return response.StatusCode, nil
}

func sign(secret string, body []byte) string {
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(body)
	return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}

func matchesEvent(events []string, event string) bool {
	if len(events) == 0 {
		return true
	}
	for _, candidate := range events {
		candidate = strings.TrimSpace(candidate)
		if candidate == "*" || candidate == event {
			return true
		}
		if strings.HasSuffix(candidate, ".*") && strings.HasPrefix(event, strings.TrimSuffix(candidate, "*")) {
			return true
		}
	}
	return false
}
