package main

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"

	"go.uber.org/zap"

	"scrabble/gateway/internal/config"
	"scrabble/gateway/internal/ratelimit"
)

const (
	// blocklistFetchTimeout bounds one feed fetch.
	blocklistFetchTimeout = 30 * time.Second
	// maxBlocklistBytes caps the fetched feed (Spamhaus DROP is well under 1 MiB; the cap stops a
	// hostile or misconfigured URL from streaming unbounded data into memory).
	maxBlocklistBytes = 8 << 20
)

// parseAllowlist parses the never-block entries (CIDRs or bare IPs, a bare IP read as a /32) into
// networks. A malformed entry is a fatal config error — the operator must fix it, not silently lose
// a protected range.
func parseAllowlist(entries []string) ([]*net.IPNet, error) {
	out := make([]*net.IPNet, 0, len(entries))
	for _, e := range entries {
		s := strings.TrimSpace(e)
		if s == "" {
			continue
		}
		if !strings.Contains(s, "/") {
			s += "/32"
		}
		_, n, err := net.ParseCIDR(s)
		if err != nil {
			return nil, fmt.Errorf("gateway: GATEWAY_BLOCKLIST_ALLOW: %q: %w", e, err)
		}
		out = append(out, n)
	}
	return out, nil
}

// runBlocklistRefresher keeps the community IP blocklist feed current: it fetches immediately, then
// every cfg.Refresh, and applies each outcome through ratelimit.ApplyRefresh (keep-last-good on a
// transient failure; drop the feed once it goes stale, fail-open). It returns when ctx is cancelled.
func runBlocklistRefresher(ctx context.Context, bl *ratelimit.Blocklist, cfg config.BlocklistConfig, log *zap.Logger) {
	client := &http.Client{Timeout: blocklistFetchTimeout}
	for {
		cidrs, err := fetchBlocklist(ctx, client, cfg.URL)
		switch ratelimit.ApplyRefresh(bl, cidrs, err, time.Now(), cfg.MaxStaleness) {
		case ratelimit.RefreshUpdated:
			log.Info("blocklist refreshed", zap.String("url", cfg.URL), zap.Int("entries", bl.Len()))
		case ratelimit.RefreshKept:
			log.Warn("blocklist refresh failed; keeping the last-good feed", zap.Error(err))
		case ratelimit.RefreshDropped:
			log.Warn("blocklist refresh failed and the feed is stale; dropped it (fail-open)", zap.Error(err))
		}
		select {
		case <-ctx.Done():
			return
		case <-time.After(cfg.Refresh):
		}
	}
}

// fetchBlocklist GETs the feed and parses it, bounded by the fetch timeout and the size cap.
func fetchBlocklist(ctx context.Context, client *http.Client, url string) ([]*net.IPNet, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("gateway: blocklist fetch %s: status %d", url, resp.StatusCode)
	}
	return ratelimit.ParseDROP(io.LimitReader(resp.Body, maxBlocklistBytes))
}
