package core

import (
	"context"
	"encoding/json/v2"
	"errors"
	"fmt"
	"os"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	validation "github.com/pocketbase/ozzo-validation/v4"
	"github.com/pocketbase/ozzo-validation/v4/is"
	"github.com/pocketbase/pocketbase/core/validators"
	"github.com/pocketbase/pocketbase/tools/cron"
	"github.com/pocketbase/pocketbase/tools/hook"
	"github.com/pocketbase/pocketbase/tools/mailer"
	"github.com/pocketbase/pocketbase/tools/security"
	"github.com/pocketbase/pocketbase/tools/types"
)

const (
	paramsTable = "_params"

	paramsKeySettings = "settings"

	systemHookIdSettings = "__pbSettingsSystemHook__"
)

func (app *BaseApp) registerSettingsHooks() {
	saveFunc := func(me *ModelEvent) error {
		if err := me.Next(); err != nil {
			return err
		}

		if me.Model.PK() == paramsKeySettings {
			// auto reload the app settings because we don't know whether
			// the Settings model is the app one or a different one
			return errors.Join(
				me.App.Settings().PostScan(),
				me.App.ReloadSettings(),
			)
		}

		return nil
	}

	app.OnModelAfterCreateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
		Id:       systemHookIdSettings,
		Func:     saveFunc,
		Priority: -999,
	})

	app.OnModelAfterUpdateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
		Id:       systemHookIdSettings,
		Func:     saveFunc,
		Priority: -999,
	})

	app.OnModelDelete(paramsTable).Bind(&hook.Handler[*ModelEvent]{
		Id: systemHookIdSettings,
		Func: func(me *ModelEvent) error {
			if me.Model.PK() == paramsKeySettings {
				return errors.New("the app params settings cannot be deleted")
			}

			return me.Next()
		},
		Priority: -999,
	})

	app.OnCollectionUpdate().Bind(&hook.Handler[*CollectionEvent]{
		Id: systemHookIdSettings,
		Func: func(e *CollectionEvent) error {
			oldCollection, err := e.App.FindCachedCollectionByNameOrId(e.Collection.Id)
			if err != nil {
				return fmt.Errorf("failed to retrieve old cached collection: %w", err)
			}

			err = e.Next()
			if err != nil {
				return err
			}

			// update existing rate limit rules on collection rename
			if oldCollection.Name != e.Collection.Name {
				var hasChange bool

				rules := e.App.Settings().RateLimits.Rules
				for i := 0; i < len(rules); i++ {
					if strings.HasPrefix(rules[i].Label, oldCollection.Name+":") {
						rules[i].Label = strings.Replace(rules[i].Label, oldCollection.Name+":", e.Collection.Name+":", 1)
						hasChange = true
					}
				}

				if hasChange {
					e.App.Settings().RateLimits.Rules = rules
					err = e.App.Save(e.App.Settings())
					if err != nil {
						return err
					}
				}
			}

			return nil
		},
		Priority: 99,
	})
}

var (
	_ Model         = (*Settings)(nil)
	_ PostValidator = (*Settings)(nil)
	_ DBExporter    = (*Settings)(nil)
)

type settings struct {
	// SuperuserIPs defines an optional list of the superuser allowed
	// individual IPs and subnets (in CIDR notation).
	SuperuserIPs []string `form:"superuserIPs" json:"superuserIPs"`

	SMTP         SMTPConfig         `form:"smtp" json:"smtp"`
	Backups      BackupsConfig      `form:"backups" json:"backups"`
	S3           S3Config           `form:"s3" json:"s3"`
	Meta         MetaConfig         `form:"meta" json:"meta"`
	RateLimits   RateLimitsConfig   `form:"rateLimits" json:"rateLimits"`
	TrustedProxy TrustedProxyConfig `form:"trustedProxy" json:"trustedProxy"`
	Batch        BatchConfig        `form:"batch" json:"batch"`
	Logs         LogsConfig         `form:"logs" json:"logs"`
}

// Settings defines the PocketBase app settings.
type Settings struct {
	settings

	mu    sync.RWMutex
	isNew bool
}

func newDefaultSettings() *Settings {
	return &Settings{
		isNew: true,
		settings: settings{
			Meta: MetaConfig{
				AccentColor:   "#1055c9",
				AppName:       "Acme",
				AppURL:        "http://localhost:8090",
				HideControls:  false,
				SenderName:    "Support",
				SenderAddress: "support@example.com",
			},
			Logs: LogsConfig{
				MaxDays: 5,
				LogIP:   true,
			},
			SMTP: SMTPConfig{
				Enabled:  false,
				Host:     "smtp.example.com",
				Port:     587,
				Username: "",
				Password: "",
				TLS:      false,
			},
			Backups: BackupsConfig{
				CronMaxKeep: 3,
			},
			Batch: BatchConfig{
				Enabled:     false,
				MaxRequests: 50,
				Timeout:     3,
			},
			RateLimits: RateLimitsConfig{
				Enabled: false, // @todo once tested enough enable by default for new installations
				Rules: []RateLimitRule{
					{Label: "*:auth", MaxRequests: 2, Duration: 3},
					{Label: "*:create", MaxRequests: 20, Duration: 5},
					{Label: "/api/batch", MaxRequests: 3, Duration: 1},
					{Label: "/api/", MaxRequests: 300, Duration: 10},
				},
			},
		},
	}
}

// TableName implements [Model.TableName] interface method.
func (s *Settings) TableName() string {
	return paramsTable
}

// PK implements [Model.LastSavedPK] interface method.
func (s *Settings) LastSavedPK() any {
	return paramsKeySettings
}

// PK implements [Model.PK] interface method.
func (s *Settings) PK() any {
	return paramsKeySettings
}

// IsNew implements [Model.IsNew] interface method.
func (s *Settings) IsNew() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.isNew
}

// MarkAsNew implements [Model.MarkAsNew] interface method.
func (s *Settings) MarkAsNew() {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.isNew = true
}

// MarkAsNew implements [Model.MarkAsNotNew] interface method.
func (s *Settings) MarkAsNotNew() {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.isNew = false
}

// PostScan implements [Model.PostScan] interface method.
func (s *Settings) PostScan() error {
	s.MarkAsNotNew()
	return nil
}

// String returns a serialized string representation of the current settings.
func (s *Settings) String() string {
	s.mu.RLock()
	defer s.mu.RUnlock()

	raw, _ := s.MarshalJSON()
	return string(raw)
}

// DBExport prepares and exports the current settings for db persistence.
func (s *Settings) DBExport(app App) (map[string]any, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	now := types.NowDateTime()

	result := map[string]any{
		"id": s.PK(),
	}

	if s.IsNew() {
		result["created"] = now
	}
	result["updated"] = now

	encoded, err := json.Marshal(s.settings, json.Deterministic(true))
	if err != nil {
		return nil, err
	}

	encryptionKey := os.Getenv(app.EncryptionEnv())
	if encryptionKey != "" {
		encryptVal, encryptErr := security.Encrypt(encoded, encryptionKey)
		if encryptErr != nil {
			return nil, encryptErr
		}

		result["value"] = encryptVal
	} else {
		result["value"] = encoded
	}

	return result, nil
}

// PostValidate implements the [PostValidator] interface and defines
// the Settings model validations.
func (s *Settings) PostValidate(ctx context.Context, app App) error {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return validation.ValidateStructWithContext(ctx, s,
		validation.Field(&s.SuperuserIPs, validation.Each(validation.Required, validation.By(validators.IPOrSubnet))),
		validation.Field(&s.Meta),
		validation.Field(&s.Logs),
		validation.Field(&s.SMTP),
		validation.Field(&s.S3),
		validation.Field(&s.Backups),
		validation.Field(&s.Batch),
		validation.Field(&s.RateLimits),
		validation.Field(&s.TrustedProxy),
	)
}

// Merge merges the "other" settings into the current one.
func (s *Settings) Merge(other *Settings) error {
	other.mu.RLock()
	defer other.mu.RUnlock()

	raw, err := json.Marshal(other.settings)
	if err != nil {
		return err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	return json.Unmarshal(raw, &s)
}

// Clone creates a new deep copy of the current settings.
func (s *Settings) Clone() (*Settings, error) {
	clone := &Settings{
		isNew: s.isNew,
	}

	if err := clone.Merge(s); err != nil {
		return nil, err
	}

	return clone, nil
}

// MarshalJSON implements the [json.Marshaler] interface.
//
// Note that sensitive fields (S3 secret, SMTP password, etc.) are excluded.
func (s *Settings) MarshalJSON() ([]byte, error) {
	s.mu.RLock()
	copy := s.settings
	s.mu.RUnlock()

	copy.SMTP.hidePassword = true

	sensitiveFields := []*string{
		&copy.SMTP.Password,
		&copy.S3.Secret,
		&copy.Backups.S3.Secret,
	}

	// mask all sensitive fields
	for _, v := range sensitiveFields {
		if v != nil && *v != "" {
			*v = ""
		}
	}

	return json.Marshal(copy, json.Deterministic(true))
}

// -------------------------------------------------------------------

type SMTPConfig struct {
	// @todo temp workaround to avoid introducing breaking changes;
	// consider refactoring and/or normalizing with the other Settings sensitive fields
	//
	// hidePassword specifies whether to hide the password field from the struct JSON serialization.
	hidePassword bool

	Enabled  bool   `form:"enabled" json:"enabled"`
	Port     int    `form:"port" json:"port"`
	Host     string `form:"host" json:"host"`
	Username string `form:"username" json:"username"`
	Password string `form:"password" json:"password"`

	// SMTP AUTH - PLAIN (default) or LOGIN
	AuthMethod string `form:"authMethod" json:"authMethod"`

	// Whether to enforce TLS encryption for the mail server connection.
	//
	// When set to false StartTLS command is send, leaving the server
	// to decide whether to upgrade the connection or not.
	TLS bool `form:"tls" json:"tls"`

	// LocalName is optional domain name or IP address used for the
	// EHLO/HELO exchange (if not explicitly set, defaults to "localhost").
	//
	// This is required only by some SMTP servers, such as Gmail SMTP-relay.
	LocalName string `form:"localName" json:"localName"`
}

// Validate makes SMTPConfig validatable by implementing [validation.Validatable] interface.
func (c SMTPConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(
			&c.Host,
			validation.When(c.Enabled, validation.Required),
			is.Host,
		),
		validation.Field(
			&c.Port,
			validation.When(c.Enabled, validation.Required),
			validation.Min(0),
		),
		validation.Field(
			&c.AuthMethod,
			// don't require it for backward compatibility
			// (fallback internally to PLAIN)
			// validation.When(c.Enabled, validation.Required),
			validation.In(mailer.SMTPAuthLogin, mailer.SMTPAuthPlain),
		),
		validation.Field(&c.LocalName, is.Host),
	)
}

// MarshalJSON implements the [json.Marshaler] interface.
func (c SMTPConfig) MarshalJSON() ([]byte, error) {
	type alias SMTPConfig

	if c.hidePassword {
		v := struct {
			alias
			Password string `json:"password,omitempty"`
		}{alias(c), ""}

		return json.Marshal(v)
	}

	return json.Marshal(alias(c))
}

// -------------------------------------------------------------------

type S3Config struct {
	Enabled        bool   `form:"enabled" json:"enabled"`
	Bucket         string `form:"bucket" json:"bucket"`
	Region         string `form:"region" json:"region"`
	Endpoint       string `form:"endpoint" json:"endpoint"`
	AccessKey      string `form:"accessKey" json:"accessKey"`
	Secret         string `form:"secret" json:"secret,omitempty"`
	ForcePathStyle bool   `form:"forcePathStyle" json:"forcePathStyle"`
}

// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
func (c S3Config) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)),
	)
}

// -------------------------------------------------------------------

type BatchConfig struct {
	Enabled bool `form:"enabled" json:"enabled"`

	// MaxRequests is the maximum allowed batch request to execute.
	MaxRequests int `form:"maxRequests" json:"maxRequests"`

	// Timeout is the max duration in seconds to wait before cancelling the batch transaction.
	Timeout int64 `form:"timeout" json:"timeout"`

	// MaxBodySize is the maximum allowed batch request body size in bytes.
	//
	// If not set, fallbacks to max ~128MB.
	MaxBodySize int64 `form:"maxBodySize" json:"maxBodySize"`
}

// Validate makes BatchConfig validatable by implementing [validation.Validatable] interface.
func (c BatchConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.MaxRequests, validation.When(c.Enabled, validation.Required), validation.Min(0)),
		validation.Field(&c.Timeout, validation.When(c.Enabled, validation.Required), validation.Min(0)),
		validation.Field(&c.MaxBodySize, validation.Min(0)),
	)
}

// -------------------------------------------------------------------

type BackupsConfig struct {
	// Cron is a cron expression to schedule auto backups, eg. "* * * * *".
	//
	// Leave it empty to disable the auto backups functionality.
	Cron string `form:"cron" json:"cron"`

	// CronMaxKeep is the max number of cron generated backups to
	// keep before removing older entries.
	//
	// This field works only when the cron config has valid cron expression.
	CronMaxKeep int `form:"cronMaxKeep" json:"cronMaxKeep"`

	// S3 is an optional S3 storage config specifying where to store the app backups.
	S3 S3Config `form:"s3" json:"s3"`
}

// Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface.
func (c BackupsConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.S3),
		validation.Field(&c.Cron, validation.By(checkCronExpression)),
		validation.Field(
			&c.CronMaxKeep,
			validation.When(c.Cron != "", validation.Required),
			validation.Min(1),
		),
	)
}

func checkCronExpression(value any) error {
	v, _ := value.(string)
	if v == "" {
		return nil // nothing to check
	}

	_, err := cron.NewSchedule(v)
	if err != nil {
		return validation.NewError("validation_invalid_cron", err.Error())
	}

	return nil
}

// -------------------------------------------------------------------

type MetaConfig struct {
	// @todo experimental
	//
	// AccentColor specify the UI "accent" color (HEX).
	AccentColor string `form:"accentColor" json:"accentColor"`

	AppName       string `form:"appName" json:"appName"`
	AppURL        string `form:"appURL" json:"appURL"`
	SenderName    string `form:"senderName" json:"senderName"`
	SenderAddress string `form:"senderAddress" json:"senderAddress"`
	HideControls  bool   `form:"hideControls" json:"hideControls"`
}

// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
func (c MetaConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.AccentColor, validation.Length(7, 7), is.HexColor),
		validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
		// @todo when replacing the URL validator we may need a system migration to normalize values without protocol
		validation.Field(&c.AppURL, validation.Required, is.URL),
		validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
		validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
	)
}

// -------------------------------------------------------------------

type LogsConfig struct {
	// MaxDataSize specifies the maximum allowed serialized log data
	// size before it gets truncated (see [Log.DBExport]).
	//
	// If zero, fallbacks to ~16kb by default.
	MaxDataSize int64 `form:"maxDataSize" json:"maxDataSize"`

	MaxDays   int  `form:"maxDays" json:"maxDays"`
	MinLevel  int  `form:"minLevel" json:"minLevel"`
	LogIP     bool `form:"logIP" json:"logIP"`
	LogAuthId bool `form:"logAuthId" json:"logAuthId"`
}

// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface.
func (c LogsConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.MaxDataSize, validation.Min(0), validation.Max(maxSafeJSONInt)),
		validation.Field(&c.MaxDays, validation.Min(0), validation.Max(maxSafeJSONInt)),
		validation.Field(&c.MinLevel, validation.Max(maxSafeJSONInt)),
	)
}

// -------------------------------------------------------------------

type TrustedProxyConfig struct {
	// Headers is a list of explicit trusted header(s) to check.
	Headers []string `form:"headers" json:"headers"`

	// UseLeftmostIP specifies to use the left-mostish IP from the trusted headers.
	//
	// Note that this could be insecure when used with X-Forwarded-For header
	// because some proxies like AWS ELB allow users to prepend their own header value
	// before appending the trusted ones.
	UseLeftmostIP bool `form:"useLeftmostIP" json:"useLeftmostIP"`
}

// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface.
func (c TrustedProxyConfig) Validate() error {
	return nil
}

// -------------------------------------------------------------------

type RateLimitsConfig struct {
	Rules       []RateLimitRule `form:"rules" json:"rules"`
	ExcludedIPs []string        `form:"excludedIPs" json:"excludedIPs"`
	Enabled     bool            `form:"enabled" json:"enabled"`
}

// FindRateLimitRule returns the first matching rule based on the provided labels.
//
// Optionally you can further specify a list of valid RateLimitRule.Audience values to further filter the matching rule
// (aka. the rule Audience will have to exist in one of the specified options).
func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string, optOnlyAudience ...string) (RateLimitRule, bool) {
	var prefixRules []int

	for i, label := range searchLabels {
		// check for direct match
		for j := range c.Rules {
			if label == c.Rules[j].Label &&
				(len(optOnlyAudience) == 0 || slices.Contains(optOnlyAudience, c.Rules[j].Audience)) {
				return c.Rules[j], true
			}

			if i == 0 && strings.HasSuffix(c.Rules[j].Label, "/") {
				prefixRules = append(prefixRules, j)
			}
		}

		// check for prefix match
		if len(prefixRules) > 0 {
			for j := range prefixRules {
				if strings.HasPrefix(label+"/", c.Rules[prefixRules[j]].Label) &&
					(len(optOnlyAudience) == 0 || slices.Contains(optOnlyAudience, c.Rules[prefixRules[j]].Audience)) {
					return c.Rules[prefixRules[j]], true
				}
			}
		}
	}

	return RateLimitRule{}, false
}

// Validate makes RateLimitsConfig validatable by implementing [validation.Validatable] interface.
func (c RateLimitsConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(
			&c.Rules,
			validation.When(c.Enabled, validation.Required),
			validation.By(checkUniqueRuleLabel),
		),
		validation.Field(
			&c.ExcludedIPs,
			validation.Each(validation.Required, validation.By(validators.IPOrSubnet)),
		),
	)
}

func checkUniqueRuleLabel(value any) error {
	rules, ok := value.([]RateLimitRule)
	if !ok {
		return validators.ErrUnsupportedValueType
	}

	existing := make([]string, 0, len(rules))

	for i, rule := range rules {
		fullKey := rule.Label + "@@" + rule.Audience

		var conflicts bool
		for _, key := range existing {
			if strings.HasPrefix(key, fullKey) || strings.HasPrefix(fullKey, key) {
				conflicts = true
				break
			}
		}

		if conflicts {
			return validation.Errors{
				strconv.Itoa(i): validation.Errors{
					"label": validation.NewError("validation_conflicting_rate_limit_rule", "Rate limit rule configuration with label {{.label}} already exists or conflicts with another rule.").
						SetParams(map[string]any{"label": rule.Label}),
				},
			}
		} else {
			existing = append(existing, fullKey)
		}
	}

	return nil
}

var rateLimitRuleLabelRegex = regexp.MustCompile(`^(\w+\ \/[\w\/-]*|\/[\w\/-]*|^\w+\:\w+|\*\:\w+|\w+)$`)

// The allowed RateLimitRule.Audience values
const (
	RateLimitRuleAudienceAll   = ""
	RateLimitRuleAudienceGuest = "@guest"
	RateLimitRuleAudienceAuth  = "@auth"
)

type RateLimitRule struct {
	// Label is the identifier of the current rule.
	//
	// It could be a tag, complete path or path prerefix (when ends with `/`).
	//
	// Example supported labels:
	//   - test_a (plain text "tag")
	//   - users:create
	//   - *:create
	//   - /
	//   - /api
	//   - POST /api/collections/
	Label string `form:"label" json:"label"`

	// Audience specifies the auth group the rule should apply for:
	//   - ""      - both guests and authenticated users (default)
	//   - "@guest" - only for guests
	//   - "@auth"  - only for authenticated users
	Audience string `form:"audience" json:"audience"`

	// Duration specifies the interval (in seconds) per which to reset
	// the counted/accumulated rate limiter tokens.
	Duration int64 `form:"duration" json:"duration"`

	// MaxRequests is the max allowed number of requests per Duration.
	MaxRequests int `form:"maxRequests" json:"maxRequests"`
}

// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface.
func (c RateLimitRule) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.Label, validation.Required, validation.Match(rateLimitRuleLabelRegex)),
		validation.Field(&c.MaxRequests, validation.Required, validation.Min(1)),
		validation.Field(&c.Duration, validation.Required, validation.Min(1)),
		validation.Field(&c.Audience,
			validation.In(RateLimitRuleAudienceAll, RateLimitRuleAudienceGuest, RateLimitRuleAudienceAuth),
		),
	)
}

// DurationTime returns the tag's Duration as [time.Duration].
func (c RateLimitRule) DurationTime() time.Duration {
	return time.Duration(c.Duration) * time.Second
}

// String returns a string representation of the rule.
func (c RateLimitRule) String() string {
	raw, err := json.Marshal(c)
	if err != nil {
		return c.Label // extremely rare case
	}

	return string(raw)
}
