package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"time"
)

type SendOptions struct {
	In          <-chan BatchPayload
	Metrics     *Metrics
	Endpoint    string
	HTTPTimeout time.Duration

	RetryMax  int
	RetryBase time.Duration
	RetryCap  time.Duration

	Logf func(string, ...any)
}

func SendBatches(ctx context.Context, opt SendOptions) {
	logf := opt.Logf
	if logf == nil {
		logf = func(string, ...any) {}
	}
	if opt.HTTPTimeout <= 0 {
		opt.HTTPTimeout = 5 * time.Second
	}
	if opt.RetryBase <= 0 {
		opt.RetryBase = 500 * time.Millisecond
	}
	if opt.RetryCap <= 0 {
		opt.RetryCap = 30 * time.Second
	}

	client := &http.Client{Timeout: opt.HTTPTimeout}

	for {
		select {
		case <-ctx.Done():
			return
		case payload, ok := <-opt.In:
			if !ok {
				return
			}
			if stringsEmpty(opt.Endpoint) {
				// Endpoint not set: just count as "sent" but don't POST (useful for local dashboard-only mode).
				opt.Metrics.IncBatchesSent(1)
				continue
			}

			err := postWithRetry(ctx, client, opt.Endpoint, payload, opt.RetryMax, opt.RetryBase, opt.RetryCap)
			if err != nil {
				opt.Metrics.IncBatchesFailed(1)
				logf("post failed: %v", err)
				continue
			}
			opt.Metrics.IncBatchesSent(1)
		}
	}
}

func postWithRetry(ctx context.Context, client *http.Client, url string, payload BatchPayload, maxRetry int, base, cap time.Duration) error {
	body, _ := json.Marshal(payload)

	var lastErr error
	for attempt := 0; attempt <= maxRetry; attempt++ {
		if attempt > 0 {
			backoff := base * (1 << (attempt - 1))
			if backoff > cap {
				backoff = cap
			}
			if !sleepCtxErr(ctx, backoff) {
				return ctx.Err()
			}
		}

		req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")

		resp, err := client.Do(req)
		if err != nil {
			lastErr = err
			continue
		}
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return nil
		}
		lastErr = errors.New(resp.Status)
	}
	return lastErr
}

func sleepCtxErr(ctx context.Context, d time.Duration) bool {
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-ctx.Done():
		return false
	case <-t.C:
		return true
	}
}

func stringsEmpty(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] != ' ' && s[i] != '\t' && s[i] != '\n' && s[i] != '\r' {
			return false
		}
	}
	return true
}
