package apps

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"sync"
	"time"
)

// Fetcher pulls URL sources once, caches the body forever, and serves
// from cache on subsequent calls. Path sources don't go through here —
// the handler reads the file directly.
//
// Concurrent calls for the same URL dedupe via singleflight. There is no
// background refresh, no conditional GET.
//
// Signature verification (Ed25519). Strict. On every fetch, also
// fetches <url>.sig (raw 64-byte Ed25519 signature). The fetched body
// is rejected unless the .sig is present, well-formed, and verifies
// against the trusted public key. Rejection causes the apps resolver
// to fall through to the embedded copy.
//
// There is no "accept unsigned with a warning" mode and no embedded
// default key. The operator configures VerifyKey explicitly via
// --apps-pubkey or ZDDC_APPS_PUBKEY (same posture as TLS certificates:
// zddc-server bakes nothing in). When VerifyKey is nil, every URL fetch
// is rejected with an error noting the missing config — the resolver
// falls back to embedded and operators get a clear signal that they
// need to opt in.
//
// Every URL the resolver might fetch is expected to have a
// corresponding .sig published by whoever signed the artifact.
// Operators using custom mirrors must sign their own artifacts and
// host the .sig alongside, then configure their public key here.
type Fetcher struct {
	Cache  *Cache
	Client *http.Client
	Logger *slog.Logger

	// VerifyKey is the Ed25519 public key against which fetched
	// artifacts are verified. Set at startup from the operator's
	// configured --apps-pubkey path. nil = URL fetches refuse-by-
	// default (caller falls back to embedded).
	VerifyKey ed25519.PublicKey

	sf            singleflightGroup
	embeddedFails sync.Map // url → struct{} (rate-limit "fell back to embedded" warnings)
}

// NewFetcher returns a Fetcher with sensible defaults: 10s timeout, no
// redirects (ops must point at the final URL).
func NewFetcher(cache *Cache, logger *slog.Logger) *Fetcher {
	if logger == nil {
		logger = slog.Default()
	}
	return &Fetcher{
		Cache:  cache,
		Logger: logger,
		// VerifyKey starts nil. Operator configures it via
		// cfg.AppsPubKey at server startup; main.go sets it on the
		// returned Fetcher before any request is served.
		Client: &http.Client{
			Timeout: 10 * time.Second,
			CheckRedirect: func(*http.Request, []*http.Request) error {
				return http.ErrUseLastResponse
			},
		},
	}
}

// Fetch returns the body for url. If the cache already has it, returns
// the cached bytes immediately. Otherwise fetches, caches, and returns.
// All concurrent requests for the same URL share one outbound fetch.
func (f *Fetcher) Fetch(ctx context.Context, urlStr string) ([]byte, error) {
	if f.Cache != nil {
		if body, err := f.Cache.Read(urlStr); err == nil {
			return body, nil
		}
	}
	val, err := f.sf.Do(urlStr, func() (any, error) {
		return f.fetchOnce(ctx, urlStr)
	})
	if err != nil {
		return nil, err
	}
	return val.([]byte), nil
}

func (f *Fetcher) fetchOnce(ctx context.Context, urlStr string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
	if err != nil {
		return nil, err
	}
	resp, err := f.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("upstream %s returned HTTP %d", urlStr, resp.StatusCode)
	}
	const maxBytes = 25 * 1024 * 1024
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
	if err != nil {
		return nil, err
	}
	if int64(len(body)) > maxBytes {
		return nil, fmt.Errorf("response from %s exceeds %d bytes", urlStr, maxBytes)
	}

	// Signature verification gate. See Fetcher type docstring for the
	// decision matrix. The transitional period accepts unsigned artifacts
	// with a WARN log; flipping RequireSigs makes it strict-reject.
	if err := f.verifyFetched(ctx, urlStr, body); err != nil {
		return nil, fmt.Errorf("signature verification failed: %w", err)
	}

	if f.Cache != nil {
		if err := f.Cache.Write(urlStr, body); err != nil {
			f.Logger.Warn("cache write failed; serving from response anyway",
				"url", urlStr, "err", err)
		}
	}
	return body, nil
}

// verifyFetched fetches <urlStr>.sig and validates body against it.
// Returns nil only when the signature is present, well-formed, and
// verifies against f.VerifyKey. Any other outcome is a hard reject:
// the caller drops the body and the apps resolver falls through to
// the embedded copy.
//
// f.VerifyKey == nil means the operator hasn't configured an apps-
// pubkey. We reject every URL fetch in that state — the operator
// needs to opt in to a specific signing key explicitly. The reject
// error is informative so the WARN log line tells the operator
// exactly what to fix.
func (f *Fetcher) verifyFetched(ctx context.Context, urlStr string, body []byte) error {
	if f.VerifyKey == nil {
		return errors.New("ZDDC_APPS_PUBKEY is not configured; URL-fetched apps require an explicit signing key (see zddc.varasys.io/pubkey.pem for the canonical-channel key)")
	}
	sigURL := urlStr + ".sig"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, sigURL, nil)
	if err != nil {
		return fmt.Errorf("build sig request for %s: %w", sigURL, err)
	}
	resp, err := f.Client.Do(req)
	if err != nil {
		return fmt.Errorf("fetch %s: %w", sigURL, err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("%s returned HTTP %d", sigURL, resp.StatusCode)
	}

	// Raw Ed25519 sig is 64 bytes; cap at a small limit so a hostile
	// upstream can't flood us with a garbage "signature."
	const maxSigBytes = 256
	sig, err := io.ReadAll(io.LimitReader(resp.Body, maxSigBytes+1))
	if err != nil {
		return fmt.Errorf("read %s: %w", sigURL, err)
	}
	if len(sig) > maxSigBytes {
		return fmt.Errorf("%s exceeds %d bytes", sigURL, maxSigBytes)
	}

	if err := VerifyEd25519(f.VerifyKey, body, sig); err != nil {
		// Verification failure is positive evidence of tampering or a
		// build/key mismatch. Logged at WARN so operators see it; the
		// resolver's existing embedded-fallback logging will note that
		// the embedded copy is being served instead.
		f.Logger.Warn("REJECTED: artifact signature does not verify",
			"url", urlStr, "sig_url", sigURL, "err", err)
		return err
	}
	f.Logger.Debug("artifact signature verified", "url", urlStr)
	return nil
}

// LogEmbeddedFallback emits a one-time warning when the embedded fallback
// is used for a particular source URL. Rate-limited per URL.
func (f *Fetcher) LogEmbeddedFallback(app, urlStr string, reason error) {
	if _, loaded := f.embeddedFails.LoadOrStore(urlStr, struct{}{}); loaded {
		return
	}
	f.Logger.Warn("serving embedded fallback for app HTML",
		"app", app, "url", urlStr, "reason", reason)
}
