// Package files implements upload storage (GridFS) and the short-lived
// HMAC-signed file URLs from §5.1: /files/{id}?st=<token> lets external
// services (atomizer, work performer) fetch attachments without a session.
package files

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"fmt"
	"time"
)

// DefaultTokenTTL is the §5.1 signed-URL lifetime.
const DefaultTokenTTL = time.Hour

// signingKey derives a dedicated key from the session secret so file tokens
// can never be confused with anything else signed by the same secret.
func signingKey(secret []byte) []byte {
	h := sha256.New()
	h.Write([]byte("bountyboard/file-url/v1"))
	h.Write(secret)
	return h.Sum(nil)
}

func mac(key []byte, fileID string, exp int64) []byte {
	m := hmac.New(sha256.New, key)
	fmt.Fprintf(m, "%s|%d", fileID, exp)
	return m.Sum(nil)
}

// SignToken returns the st token granting access to fileID until expiry.
// Format: base64url(bigendian-int64-exp || hmac-sha256(fileID|exp)).
func SignToken(secret []byte, fileID string, expiresAt time.Time) string {
	exp := expiresAt.Unix()
	key := signingKey(secret)
	buf := make([]byte, 8, 8+sha256.Size)
	binary.BigEndian.PutUint64(buf, uint64(exp))
	buf = append(buf, mac(key, fileID, exp)...)
	return base64.RawURLEncoding.EncodeToString(buf)
}

// VerifyToken reports whether token grants access to fileID at time now.
func VerifyToken(secret []byte, fileID, token string, now time.Time) bool {
	raw, err := base64.RawURLEncoding.DecodeString(token)
	if err != nil || len(raw) != 8+sha256.Size {
		return false
	}
	exp := int64(binary.BigEndian.Uint64(raw[:8]))
	if now.Unix() > exp {
		return false
	}
	expected := mac(signingKey(secret), fileID, exp)
	return hmac.Equal(raw[8:], expected)
}

// SignedURL builds the absolute fetch URL for a stored file.
func SignedURL(appBaseURL string, secret []byte, fileID string, expiresAt time.Time) string {
	return fmt.Sprintf("%s/files/%s?st=%s", appBaseURL, fileID, SignToken(secret, fileID, expiresAt))
}
