package settings

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"

	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/go-ozzo/ozzo-validation/v4/is"
	"github.com/pocketbase/pocketbase/tools/auth"
	"github.com/pocketbase/pocketbase/tools/cron"
	"github.com/pocketbase/pocketbase/tools/mailer"
	"github.com/pocketbase/pocketbase/tools/rest"
	"github.com/pocketbase/pocketbase/tools/security"
)

// SecretMask is the default settings secrets replacement value
// (see Settings.RedactClone()).
const SecretMask string = "******"

// Settings defines common app configuration options.
type Settings struct {
	mux sync.RWMutex

	Meta    MetaConfig    `form:"meta" json:"meta"`
	Logs    LogsConfig    `form:"logs" json:"logs"`
	Smtp    SmtpConfig    `form:"smtp" json:"smtp"`
	S3      S3Config      `form:"s3" json:"s3"`
	Backups BackupsConfig `form:"backups" json:"backups"`

	AdminAuthToken           TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
	AdminPasswordResetToken  TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
	AdminFileToken           TokenConfig `form:"adminFileToken" json:"adminFileToken"`
	RecordAuthToken          TokenConfig `form:"recordAuthToken" json:"recordAuthToken"`
	RecordPasswordResetToken TokenConfig `form:"recordPasswordResetToken" json:"recordPasswordResetToken"`
	RecordEmailChangeToken   TokenConfig `form:"recordEmailChangeToken" json:"recordEmailChangeToken"`
	RecordVerificationToken  TokenConfig `form:"recordVerificationToken" json:"recordVerificationToken"`
	RecordFileToken          TokenConfig `form:"recordFileToken" json:"recordFileToken"`

	// Deprecated: Will be removed in v0.9+
	EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`

	GoogleAuth    AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
	FacebookAuth  AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"`
	GithubAuth    AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
	GitlabAuth    AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
	DiscordAuth   AuthProviderConfig `form:"discordAuth" json:"discordAuth"`
	TwitterAuth   AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
	MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"`
	SpotifyAuth   AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
	KakaoAuth     AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"`
	TwitchAuth    AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"`
	StravaAuth    AuthProviderConfig `form:"stravaAuth" json:"stravaAuth"`
	GiteeAuth     AuthProviderConfig `form:"giteeAuth" json:"giteeAuth"`
	LivechatAuth  AuthProviderConfig `form:"livechatAuth" json:"livechatAuth"`
	GiteaAuth     AuthProviderConfig `form:"giteaAuth" json:"giteaAuth"`
	OIDCAuth      AuthProviderConfig `form:"oidcAuth" json:"oidcAuth"`
	OIDC2Auth     AuthProviderConfig `form:"oidc2Auth" json:"oidc2Auth"`
	OIDC3Auth     AuthProviderConfig `form:"oidc3Auth" json:"oidc3Auth"`
	AppleAuth     AuthProviderConfig `form:"appleAuth" json:"appleAuth"`
	InstagramAuth AuthProviderConfig `form:"instagramAuth" json:"instagramAuth"`
	VKAuth        AuthProviderConfig `form:"vkAuth" json:"vkAuth"`
	YandexAuth    AuthProviderConfig `form:"yandexAuth" json:"yandexAuth"`
}

// New creates and returns a new default Settings instance.
func New() *Settings {
	return &Settings{
		Meta: MetaConfig{
			AppName:                    "Acme",
			AppUrl:                     "http://localhost:8090",
			HideControls:               false,
			SenderName:                 "Support",
			SenderAddress:              "support@example.com",
			VerificationTemplate:       defaultVerificationTemplate,
			ResetPasswordTemplate:      defaultResetPasswordTemplate,
			ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
		},
		Logs: LogsConfig{
			MaxDays: 5,
		},
		Smtp: SmtpConfig{
			Enabled:  false,
			Host:     "smtp.example.com",
			Port:     587,
			Username: "",
			Password: "",
			Tls:      false,
		},
		Backups: BackupsConfig{
			CronMaxKeep: 3,
		},
		AdminAuthToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 1209600, // 14 days
		},
		AdminPasswordResetToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 1800, // 30 minutes
		},
		AdminFileToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 120, // 2 minutes
		},
		RecordAuthToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 1209600, // 14 days
		},
		RecordPasswordResetToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 1800, // 30 minutes
		},
		RecordVerificationToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 604800, // 7 days
		},
		RecordFileToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 120, // 2 minutes
		},
		RecordEmailChangeToken: TokenConfig{
			Secret:   security.RandomString(50),
			Duration: 1800, // 30 minutes
		},
		GoogleAuth: AuthProviderConfig{
			Enabled: false,
		},
		FacebookAuth: AuthProviderConfig{
			Enabled: false,
		},
		GithubAuth: AuthProviderConfig{
			Enabled: false,
		},
		GitlabAuth: AuthProviderConfig{
			Enabled: false,
		},
		DiscordAuth: AuthProviderConfig{
			Enabled: false,
		},
		TwitterAuth: AuthProviderConfig{
			Enabled: false,
		},
		MicrosoftAuth: AuthProviderConfig{
			Enabled: false,
		},
		SpotifyAuth: AuthProviderConfig{
			Enabled: false,
		},
		KakaoAuth: AuthProviderConfig{
			Enabled: false,
		},
		TwitchAuth: AuthProviderConfig{
			Enabled: false,
		},
		StravaAuth: AuthProviderConfig{
			Enabled: false,
		},
		GiteeAuth: AuthProviderConfig{
			Enabled: false,
		},
		LivechatAuth: AuthProviderConfig{
			Enabled: false,
		},
		GiteaAuth: AuthProviderConfig{
			Enabled: false,
		},
		OIDCAuth: AuthProviderConfig{
			Enabled: false,
		},
		OIDC2Auth: AuthProviderConfig{
			Enabled: false,
		},
		OIDC3Auth: AuthProviderConfig{
			Enabled: false,
		},
		AppleAuth: AuthProviderConfig{
			Enabled: false,
		},
		InstagramAuth: AuthProviderConfig{
			Enabled: false,
		},
		VKAuth: AuthProviderConfig{
			Enabled: false,
		},
		YandexAuth: AuthProviderConfig{
			Enabled: false,
		},
	}
}

// Validate makes Settings validatable by implementing [validation.Validatable] interface.
func (s *Settings) Validate() error {
	s.mux.Lock()
	defer s.mux.Unlock()

	return validation.ValidateStruct(s,
		validation.Field(&s.Meta),
		validation.Field(&s.Logs),
		validation.Field(&s.AdminAuthToken),
		validation.Field(&s.AdminPasswordResetToken),
		validation.Field(&s.AdminFileToken),
		validation.Field(&s.RecordAuthToken),
		validation.Field(&s.RecordPasswordResetToken),
		validation.Field(&s.RecordEmailChangeToken),
		validation.Field(&s.RecordVerificationToken),
		validation.Field(&s.RecordFileToken),
		validation.Field(&s.Smtp),
		validation.Field(&s.S3),
		validation.Field(&s.Backups),
		validation.Field(&s.GoogleAuth),
		validation.Field(&s.FacebookAuth),
		validation.Field(&s.GithubAuth),
		validation.Field(&s.GitlabAuth),
		validation.Field(&s.DiscordAuth),
		validation.Field(&s.TwitterAuth),
		validation.Field(&s.MicrosoftAuth),
		validation.Field(&s.SpotifyAuth),
		validation.Field(&s.KakaoAuth),
		validation.Field(&s.TwitchAuth),
		validation.Field(&s.StravaAuth),
		validation.Field(&s.GiteeAuth),
		validation.Field(&s.LivechatAuth),
		validation.Field(&s.GiteaAuth),
		validation.Field(&s.OIDCAuth),
		validation.Field(&s.OIDC2Auth),
		validation.Field(&s.OIDC3Auth),
		validation.Field(&s.AppleAuth),
		validation.Field(&s.InstagramAuth),
		validation.Field(&s.VKAuth),
		validation.Field(&s.YandexAuth),
	)
}

// Merge merges `other` settings into the current one.
func (s *Settings) Merge(other *Settings) error {
	s.mux.Lock()
	defer s.mux.Unlock()

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

	return json.Unmarshal(bytes, s)
}

// Clone creates a new deep copy of the current settings.
func (s *Settings) Clone() (*Settings, error) {
	clone := &Settings{}
	if err := clone.Merge(s); err != nil {
		return nil, err
	}
	return clone, nil
}

// RedactClone creates a new deep copy of the current settings,
// while replacing the secret values with `******`.
func (s *Settings) RedactClone() (*Settings, error) {
	clone, err := s.Clone()
	if err != nil {
		return nil, err
	}

	sensitiveFields := []*string{
		&clone.Smtp.Password,
		&clone.S3.Secret,
		&clone.Backups.S3.Secret,
		&clone.AdminAuthToken.Secret,
		&clone.AdminPasswordResetToken.Secret,
		&clone.AdminFileToken.Secret,
		&clone.RecordAuthToken.Secret,
		&clone.RecordPasswordResetToken.Secret,
		&clone.RecordEmailChangeToken.Secret,
		&clone.RecordVerificationToken.Secret,
		&clone.RecordFileToken.Secret,
		&clone.GoogleAuth.ClientSecret,
		&clone.FacebookAuth.ClientSecret,
		&clone.GithubAuth.ClientSecret,
		&clone.GitlabAuth.ClientSecret,
		&clone.DiscordAuth.ClientSecret,
		&clone.TwitterAuth.ClientSecret,
		&clone.MicrosoftAuth.ClientSecret,
		&clone.SpotifyAuth.ClientSecret,
		&clone.KakaoAuth.ClientSecret,
		&clone.TwitchAuth.ClientSecret,
		&clone.StravaAuth.ClientSecret,
		&clone.GiteeAuth.ClientSecret,
		&clone.LivechatAuth.ClientSecret,
		&clone.GiteaAuth.ClientSecret,
		&clone.OIDCAuth.ClientSecret,
		&clone.OIDC2Auth.ClientSecret,
		&clone.OIDC3Auth.ClientSecret,
		&clone.AppleAuth.ClientSecret,
		&clone.InstagramAuth.ClientSecret,
		&clone.VKAuth.ClientSecret,
		&clone.YandexAuth.ClientSecret,
	}

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

	return clone, nil
}

// NamedAuthProviderConfigs returns a map with all registered OAuth2
// provider configurations (indexed by their name identifier).
func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
	s.mux.RLock()
	defer s.mux.RUnlock()

	return map[string]AuthProviderConfig{
		auth.NameGoogle:     s.GoogleAuth,
		auth.NameFacebook:   s.FacebookAuth,
		auth.NameGithub:     s.GithubAuth,
		auth.NameGitlab:     s.GitlabAuth,
		auth.NameDiscord:    s.DiscordAuth,
		auth.NameTwitter:    s.TwitterAuth,
		auth.NameMicrosoft:  s.MicrosoftAuth,
		auth.NameSpotify:    s.SpotifyAuth,
		auth.NameKakao:      s.KakaoAuth,
		auth.NameTwitch:     s.TwitchAuth,
		auth.NameStrava:     s.StravaAuth,
		auth.NameGitee:      s.GiteeAuth,
		auth.NameLivechat:   s.LivechatAuth,
		auth.NameGitea:      s.GiteaAuth,
		auth.NameOIDC:       s.OIDCAuth,
		auth.NameOIDC + "2": s.OIDC2Auth,
		auth.NameOIDC + "3": s.OIDC3Auth,
		auth.NameApple:      s.AppleAuth,
		auth.NameInstagram:  s.InstagramAuth,
		auth.NameVK:         s.VKAuth,
		auth.NameYandex:     s.YandexAuth,
	}
}

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

type TokenConfig struct {
	Secret   string `form:"secret" json:"secret"`
	Duration int64  `form:"duration" json:"duration"`
}

// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface.
func (c TokenConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)),
		validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(63072000)),
	)
}

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

type SmtpConfig struct {
	Enabled  bool   `form:"enabled" json:"enabled"`
	Host     string `form:"host" json:"host"`
	Port     int    `form:"port" json:"port"`
	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"`
}

// 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),
		),
	)
}

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

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"`
	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 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 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 {
	AppName                    string        `form:"appName" json:"appName"`
	AppUrl                     string        `form:"appUrl" json:"appUrl"`
	HideControls               bool          `form:"hideControls" json:"hideControls"`
	SenderName                 string        `form:"senderName" json:"senderName"`
	SenderAddress              string        `form:"senderAddress" json:"senderAddress"`
	VerificationTemplate       EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
	ResetPasswordTemplate      EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
	ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
}

// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
func (c MetaConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
		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),
		validation.Field(&c.VerificationTemplate, validation.Required),
		validation.Field(&c.ResetPasswordTemplate, validation.Required),
		validation.Field(&c.ConfirmEmailChangeTemplate, validation.Required),
	)
}

type EmailTemplate struct {
	Body      string `form:"body" json:"body"`
	Subject   string `form:"subject" json:"subject"`
	ActionUrl string `form:"actionUrl" json:"actionUrl"`
}

// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface.
func (t EmailTemplate) Validate() error {
	return validation.ValidateStruct(&t,
		validation.Field(&t.Subject, validation.Required),
		validation.Field(
			&t.Body,
			validation.Required,
			validation.By(checkPlaceholderParams(EmailPlaceholderActionUrl)),
		),
		validation.Field(
			&t.ActionUrl,
			validation.Required,
			validation.By(checkPlaceholderParams(EmailPlaceholderToken)),
		),
	)
}

func checkPlaceholderParams(params ...string) validation.RuleFunc {
	return func(value any) error {
		v, _ := value.(string)

		for _, param := range params {
			if !strings.Contains(v, param) {
				return validation.NewError(
					"validation_missing_required_param",
					fmt.Sprintf("Missing required parameter %q", param),
				)
			}
		}

		return nil
	}
}

// Resolve replaces the placeholder parameters in the current email
// template and returns its components as ready-to-use strings.
func (t EmailTemplate) Resolve(
	appName string,
	appUrl,
	token string,
) (subject, body, actionUrl string) {
	// replace action url placeholder params (if any)
	actionUrlParams := map[string]string{
		EmailPlaceholderAppName: appName,
		EmailPlaceholderAppUrl:  appUrl,
		EmailPlaceholderToken:   token,
	}
	actionUrl = t.ActionUrl
	for k, v := range actionUrlParams {
		actionUrl = strings.ReplaceAll(actionUrl, k, v)
	}
	actionUrl, _ = rest.NormalizeUrl(actionUrl)

	// replace body placeholder params (if any)
	bodyParams := map[string]string{
		EmailPlaceholderAppName:   appName,
		EmailPlaceholderAppUrl:    appUrl,
		EmailPlaceholderToken:     token,
		EmailPlaceholderActionUrl: actionUrl,
	}
	body = t.Body
	for k, v := range bodyParams {
		body = strings.ReplaceAll(body, k, v)
	}

	// replace subject placeholder params (if any)
	subjectParams := map[string]string{
		EmailPlaceholderAppName: appName,
		EmailPlaceholderAppUrl:  appUrl,
	}
	subject = t.Subject
	for k, v := range subjectParams {
		subject = strings.ReplaceAll(subject, k, v)
	}

	return subject, body, actionUrl
}

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

type LogsConfig struct {
	MaxDays int `form:"maxDays" json:"maxDays"`
}

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

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

type AuthProviderConfig struct {
	Enabled      bool   `form:"enabled" json:"enabled"`
	ClientId     string `form:"clientId" json:"clientId"`
	ClientSecret string `form:"clientSecret" json:"clientSecret"`
	AuthUrl      string `form:"authUrl" json:"authUrl"`
	TokenUrl     string `form:"tokenUrl" json:"tokenUrl"`
	UserApiUrl   string `form:"userApiUrl" json:"userApiUrl"`
}

// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface.
func (c AuthProviderConfig) Validate() error {
	return validation.ValidateStruct(&c,
		validation.Field(&c.ClientId, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.ClientSecret, validation.When(c.Enabled, validation.Required)),
		validation.Field(&c.AuthUrl, is.URL),
		validation.Field(&c.TokenUrl, is.URL),
		validation.Field(&c.UserApiUrl, is.URL),
	)
}

// SetupProvider loads the current AuthProviderConfig into the specified provider.
func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
	if !c.Enabled {
		return errors.New("The provider is not enabled.")
	}

	if c.ClientId != "" {
		provider.SetClientId(c.ClientId)
	}

	if c.ClientSecret != "" {
		provider.SetClientSecret(c.ClientSecret)
	}

	if c.AuthUrl != "" {
		provider.SetAuthUrl(c.AuthUrl)
	}

	if c.UserApiUrl != "" {
		provider.SetUserApiUrl(c.UserApiUrl)
	}

	if c.TokenUrl != "" {
		provider.SetTokenUrl(c.TokenUrl)
	}

	return nil
}

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

// Deprecated: Will be removed in v0.9+
type EmailAuthConfig struct {
	Enabled           bool     `form:"enabled" json:"enabled"`
	ExceptDomains     []string `form:"exceptDomains" json:"exceptDomains"`
	OnlyDomains       []string `form:"onlyDomains" json:"onlyDomains"`
	MinPasswordLength int      `form:"minPasswordLength" json:"minPasswordLength"`
}

// Deprecated: Will be removed in v0.9+
func (c EmailAuthConfig) Validate() error {
	return nil
}
