package server

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io/fs"
	"net/netip"
	"reflect"
	"text/template"
	"time"

	"heckel.io/ntfy/v2/user"
)

// Defines default config settings (excluding limits, see below)
const (
	DefaultListenHTTP                           = ":80"
	DefaultCacheDuration                        = 12 * time.Hour
	DefaultCacheBatchTimeout                    = time.Duration(0)
	DefaultKeepaliveInterval                    = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
	DefaultManagerInterval                      = time.Minute
	DefaultManagerBatchSize                     = 30000
	DefaultDelayedSenderInterval                = 10 * time.Second
	DefaultMessageDelayMin                      = 10 * time.Second
	DefaultMessageDelayMax                      = 3 * 24 * time.Hour
	DefaultFirebaseKeepaliveInterval            = 3 * time.Hour    // ~control topic (Android), not too frequently to save battery
	DefaultFirebasePollInterval                 = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
	DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded"
	DefaultStripePriceCacheDuration             = 3 * time.Hour    // Time to keep Stripe prices cached in memory before a refresh is needed
)

// Platform-specific default paths (set in config_unix.go or config_windows.go)
var (
	DefaultConfigFile  string
	DefaultTemplateDir string
)

// Defines default Web Push settings
const (
	DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
	DefaultWebPushExpiryDuration        = 60 * 24 * time.Hour
)

// Defines all global and per-visitor limits
// - message size limit: the max number of bytes for a message
// - total topic limit: max number of topics overall
// - various attachment limits
const (
	DefaultMessageSizeLimit            = 4096 // Bytes; note that FCM/APNS have a limit of ~4 KB for the entire message
	DefaultTotalTopicLimit             = 15000
	DefaultAttachmentTotalSizeLimit    = int64(5 * 1024 * 1024 * 1024) // 5 GB
	DefaultAttachmentFileSizeLimit     = int64(15 * 1024 * 1024)       // 15 MB
	DefaultAttachmentExpiryDuration    = 3 * time.Hour
	DefaultAttachmentOrphanGracePeriod = time.Hour // Don't delete orphaned objects younger than this to avoid races with in-flight uploads

)

// Defines all per-visitor limits
// - per visitor subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
// - per visitor request limit: max number of PUT/GET/.. requests (here: 60 requests bucket, replenished at a rate of one per 5 seconds)
// - per visitor email limit: max number of emails (here: 16 email bucket, replenished at a rate of one per hour)
// - per visitor attachment size limit: total per-visitor attachment size in bytes to be stored on the server
// - per visitor attachment daily bandwidth limit: number of bytes that can be transferred to/from the server
const (
	DefaultVisitorSubscriptionLimit             = 30
	DefaultVisitorRequestLimitBurst             = 60
	DefaultVisitorRequestLimitReplenish         = 5 * time.Second
	DefaultVisitorMessageDailyLimit             = 0
	DefaultVisitorEmailLimitBurst               = 16
	DefaultVisitorEmailLimitReplenish           = time.Hour
	DefaultVisitorTopicCreationLimitBurst       = 100
	DefaultVisitorTopicCreationLimitReplenish   = time.Minute
	DefaultVisitorAccountCreationLimitBurst     = 3
	DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
	DefaultVisitorAuthFailureLimitBurst         = 30
	DefaultVisitorAuthFailureLimitReplenish     = time.Minute
	DefaultVisitorAttachmentTotalSizeLimit      = 100 * 1024 * 1024 // 100 MB
	DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB
	DefaultVisitorPrefixBitsIPv4                = 32                // Use the entire IPv4 address for rate limiting
	DefaultVisitorPrefixBitsIPv6                = 64                // Use /64 for IPv6 rate limiting
)

var (
	// DefaultVisitorStatsResetTime defines the time at which visitor stats are reset (wall clock only)
	DefaultVisitorStatsResetTime = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)

	// DefaultDisallowedTopics defines the topics that are forbidden, because they are used elsewhere. This array can be
	// extended using the server.yml config. If updated, also update in Android and web app.
	DefaultDisallowedTopics = []string{"docs", "static", "file", "app", "metrics", "account", "settings", "signup", "login", "v1"}
)

// Config is the main config struct for the application. Use New to instantiate a default config struct.
type Config struct {
	File                                 string // Config file, only used for testing
	BaseURL                              string
	ListenHTTP                           string
	ListenHTTPS                          string
	ListenUnix                           string
	ListenUnixMode                       fs.FileMode
	KeyFile                              string
	CertFile                             string
	DatabaseURL                          string   // PostgreSQL connection string (e.g. "postgres://user:pass@host:5432/ntfy")
	DatabaseReplicaURLs                  []string // PostgreSQL read replica connection strings
	FirebaseKeyFile                      string
	CacheFile                            string
	CacheDuration                        time.Duration
	CacheStartupQueries                  string
	CacheBatchSize                       int
	CacheBatchTimeout                    time.Duration
	AuthFile                             string
	AuthStartupQueries                   string
	AuthDefault                          user.Permission
	AuthUsers                            []*user.User
	AuthAccess                           map[string][]*user.Grant
	AuthTokens                           map[string][]*user.Token
	AuthBcryptCost                       int
	AuthStatsQueueWriterInterval         time.Duration
	AuthAccessCacheEnabled               bool          // Enables the in-memory ACL cache (high volume servers only)
	AuthAccessCacheReloadInterval        time.Duration // Reload interval for access cache, relevant for ACL writes from CLI
	AttachmentCacheDir                   string
	AttachmentTotalSizeLimit             int64
	AttachmentFileSizeLimit              int64
	AttachmentExpiryDuration             time.Duration
	AttachmentOrphanGracePeriod          time.Duration
	TemplateDir                          string // Directory to load named templates from
	KeepaliveInterval                    time.Duration
	ManagerInterval                      time.Duration
	ManagerBatchSize                     int
	DisallowedTopics                     []string
	WebRoot                              string // empty to disable
	DelayedSenderInterval                time.Duration
	FirebaseKeepaliveInterval            time.Duration
	FirebasePollInterval                 time.Duration
	FirebaseQuotaExceededPenaltyDuration time.Duration
	UpstreamBaseURL                      string
	UpstreamAccessToken                  string
	SMTPSenderAddr                       string
	SMTPSenderUser                       string
	SMTPSenderPass                       string
	SMTPSenderFrom                       string
	SMTPSenderVerify                     bool
	SMTPServerListen                     string
	SMTPServerDomain                     string
	SMTPServerAddrPrefix                 string
	TwilioAccount                        string
	TwilioAuthToken                      string
	TwilioPhoneNumber                    string
	TwilioCallsBaseURL                   string
	TwilioVerifyBaseURL                  string
	TwilioVerifyService                  string
	TwilioCallFormat                     *template.Template
	MetricsEnable                        bool
	MetricsListenHTTP                    string
	ProfileListenHTTP                    string
	MessageDelayMin                      time.Duration
	MessageDelayMax                      time.Duration
	MessageSizeLimit                     int
	TotalTopicLimit                      int
	TotalAttachmentSizeLimit             int64
	VisitorSubscriptionLimit             int
	VisitorAttachmentTotalSizeLimit      int64
	VisitorAttachmentDailyBandwidthLimit int64
	VisitorRequestLimitBurst             int
	VisitorRequestLimitReplenish         time.Duration
	VisitorRequestExemptPrefixes         []netip.Prefix
	VisitorMessageDailyLimit             int
	VisitorEmailLimitBurst               int
	VisitorEmailLimitReplenish           time.Duration
	VisitorTopicCreationLimitBurst       int           // Burst of new topic creations per visitor
	VisitorTopicCreationLimitReplenish   time.Duration // Interval at which topic-creation tokens are refilled
	VisitorAccountCreationLimitBurst     int
	VisitorAccountCreationLimitReplenish time.Duration
	VisitorAuthFailureLimitBurst         int
	VisitorAuthFailureLimitReplenish     time.Duration
	VisitorStatsResetTime                time.Time      // Time of the day at which to reset visitor stats
	VisitorSubscriberRateLimiting        bool           // Enable subscriber-based rate limiting for UnifiedPush topics
	VisitorPrefixBitsIPv4                int            // Number of bits for IPv4 rate limiting (default: 32)
	VisitorPrefixBitsIPv6                int            // Number of bits for IPv6 rate limiting (default: 64)
	BehindProxy                          bool           // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported)
	ProxyForwardedHeader                 string         // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported)
	ProxyTrustedPrefixes                 []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true
	StripeSecretKey                      string
	StripeWebhookKey                     string
	StripePriceCacheDuration             time.Duration
	BillingContact                       string
	EnableSignup                         bool // Enable creation of accounts via API and UI
	EnableLogin                          bool
	RequireLogin                         bool
	EnableReservations                   bool // Allow users with role "user" to own/reserve topics
	EnableMetrics                        bool
	AccessControlAllowOrigin             string // CORS header field to restrict access from web clients
	WebPushPrivateKey                    string
	WebPushPublicKey                     string
	WebPushFile                          string
	WebPushEmailAddress                  string
	WebPushStartupQueries                string
	WebPushExpiryDuration                time.Duration
	WebPushExpiryWarningDuration         time.Duration
	BuildVersion                         string // Injected by App
	BuildDate                            string // Injected by App
	BuildCommit                          string // Injected by App
}

// NewConfig instantiates a default new server config
func NewConfig() *Config {
	return &Config{
		File:                                 DefaultConfigFile, // Only used for testing
		BaseURL:                              "",
		ListenHTTP:                           DefaultListenHTTP,
		ListenHTTPS:                          "",
		ListenUnix:                           "",
		ListenUnixMode:                       0,
		KeyFile:                              "",
		CertFile:                             "",
		DatabaseURL:                          "",
		FirebaseKeyFile:                      "",
		CacheFile:                            "",
		CacheDuration:                        DefaultCacheDuration,
		CacheStartupQueries:                  "",
		CacheBatchSize:                       0,
		CacheBatchTimeout:                    0,
		AuthFile:                             "",
		AuthStartupQueries:                   "",
		AuthDefault:                          user.PermissionReadWrite,
		AuthBcryptCost:                       user.DefaultUserPasswordBcryptCost,
		AuthStatsQueueWriterInterval:         user.DefaultUserStatsQueueWriterInterval,
		AuthAccessCacheEnabled:               user.DefaultAccessCacheEnabled,
		AuthAccessCacheReloadInterval:        user.DefaultAccessCacheReloadInterval,
		AttachmentCacheDir:                   "",
		AttachmentTotalSizeLimit:             DefaultAttachmentTotalSizeLimit,
		AttachmentFileSizeLimit:              DefaultAttachmentFileSizeLimit,
		AttachmentExpiryDuration:             DefaultAttachmentExpiryDuration,
		AttachmentOrphanGracePeriod:          DefaultAttachmentOrphanGracePeriod,
		TemplateDir:                          DefaultTemplateDir,
		KeepaliveInterval:                    DefaultKeepaliveInterval,
		ManagerInterval:                      DefaultManagerInterval,
		ManagerBatchSize:                     DefaultManagerBatchSize,
		DisallowedTopics:                     DefaultDisallowedTopics,
		WebRoot:                              "/",
		DelayedSenderInterval:                DefaultDelayedSenderInterval,
		FirebaseKeepaliveInterval:            DefaultFirebaseKeepaliveInterval,
		FirebasePollInterval:                 DefaultFirebasePollInterval,
		FirebaseQuotaExceededPenaltyDuration: DefaultFirebaseQuotaExceededPenaltyDuration,
		UpstreamBaseURL:                      "",
		UpstreamAccessToken:                  "",
		SMTPSenderAddr:                       "",
		SMTPSenderUser:                       "",
		SMTPSenderPass:                       "",
		SMTPSenderFrom:                       "",
		SMTPSenderVerify:                     false,
		SMTPServerListen:                     "",
		SMTPServerDomain:                     "",
		SMTPServerAddrPrefix:                 "",
		TwilioCallsBaseURL:                   "https://api.twilio.com", // Override for tests
		TwilioAccount:                        "",
		TwilioAuthToken:                      "",
		TwilioPhoneNumber:                    "",
		TwilioVerifyBaseURL:                  "https://verify.twilio.com", // Override for tests
		TwilioVerifyService:                  "",
		TwilioCallFormat:                     nil,
		MessageSizeLimit:                     DefaultMessageSizeLimit,
		MessageDelayMin:                      DefaultMessageDelayMin,
		MessageDelayMax:                      DefaultMessageDelayMax,
		TotalTopicLimit:                      DefaultTotalTopicLimit,
		TotalAttachmentSizeLimit:             0,
		VisitorSubscriptionLimit:             DefaultVisitorSubscriptionLimit,
		VisitorSubscriberRateLimiting:        false,
		VisitorAttachmentTotalSizeLimit:      DefaultVisitorAttachmentTotalSizeLimit,
		VisitorAttachmentDailyBandwidthLimit: DefaultVisitorAttachmentDailyBandwidthLimit,
		VisitorRequestLimitBurst:             DefaultVisitorRequestLimitBurst,
		VisitorRequestLimitReplenish:         DefaultVisitorRequestLimitReplenish,
		VisitorRequestExemptPrefixes:         make([]netip.Prefix, 0),
		VisitorMessageDailyLimit:             DefaultVisitorMessageDailyLimit,
		VisitorEmailLimitBurst:               DefaultVisitorEmailLimitBurst,
		VisitorEmailLimitReplenish:           DefaultVisitorEmailLimitReplenish,
		VisitorTopicCreationLimitBurst:       DefaultVisitorTopicCreationLimitBurst,
		VisitorTopicCreationLimitReplenish:   DefaultVisitorTopicCreationLimitReplenish,
		VisitorAccountCreationLimitBurst:     DefaultVisitorAccountCreationLimitBurst,
		VisitorAccountCreationLimitReplenish: DefaultVisitorAccountCreationLimitReplenish,
		VisitorAuthFailureLimitBurst:         DefaultVisitorAuthFailureLimitBurst,
		VisitorAuthFailureLimitReplenish:     DefaultVisitorAuthFailureLimitReplenish,
		VisitorStatsResetTime:                DefaultVisitorStatsResetTime,
		VisitorPrefixBitsIPv4:                DefaultVisitorPrefixBitsIPv4, // Default: use full IPv4 address
		VisitorPrefixBitsIPv6:                DefaultVisitorPrefixBitsIPv6, // Default: use /64 for IPv6
		BehindProxy:                          false,                        // If true, the server will trust the proxy client IP header to determine the client IP address
		ProxyForwardedHeader:                 "X-Forwarded-For",            // Default header for reverse proxy client IPs
		StripeSecretKey:                      "",
		StripeWebhookKey:                     "",
		StripePriceCacheDuration:             DefaultStripePriceCacheDuration,
		BillingContact:                       "",
		EnableSignup:                         false,
		EnableLogin:                          false,
		EnableReservations:                   false,
		RequireLogin:                         false,
		AccessControlAllowOrigin:             "*",
		WebPushPrivateKey:                    "",
		WebPushPublicKey:                     "",
		WebPushFile:                          "",
		WebPushEmailAddress:                  "",
		WebPushExpiryDuration:                DefaultWebPushExpiryDuration,
		WebPushExpiryWarningDuration:         DefaultWebPushExpiryWarningDuration,
		BuildVersion:                         "",
		BuildDate:                            "",
		BuildCommit:                          "",
	}
}

// Hash computes an SHA-256 hash of the configuration. This is used to detect
// configuration changes for the web app version check feature. It uses reflection
// to include all JSON-serializable fields automatically.
func (c *Config) Hash() string {
	v := reflect.ValueOf(*c)
	t := v.Type()
	var result string
	for i := 0; i < v.NumField(); i++ {
		field := v.Field(i)
		fieldName := t.Field(i).Name
		// Try to marshal the field and skip if it fails (e.g. *template.Template, netip.Prefix)
		if b, err := json.Marshal(field.Interface()); err == nil {
			result += fmt.Sprintf("%s:%s|", fieldName, string(b))
		}
	}
	return fmt.Sprintf("%x", sha256.Sum256([]byte(result)))
}
