package model

import (
	"errors"
	"fmt"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/pocket-id/pocket-id/backend/internal/common"
)

type AppConfigVariable struct {
	Key   string `gorm:"primaryKey;not null"`
	Value string
}

// IsTrue returns true if the value is a truthy string, such as "true", "t", "yes", "1", etc.
func (a *AppConfigVariable) IsTrue() bool {
	ok, _ := strconv.ParseBool(a.Value)
	return ok
}

// AsDurationMinutes returns the value as a time.Duration, interpreting the string as a whole number of minutes.
func (a *AppConfigVariable) AsDurationMinutes() time.Duration {
	val, err := strconv.Atoi(a.Value)
	if err != nil {
		return 0
	}
	return time.Duration(val) * time.Minute
}

type AppConfig struct {
	// General
	AppName                   AppConfigVariable `key:"appName,public"` // Public
	SessionDuration           AppConfigVariable `key:"sessionDuration"`
	HomePageURL               AppConfigVariable `key:"homePageUrl,public"` // Public
	EmailsVerified            AppConfigVariable `key:"emailsVerified"`
	AccentColor               AppConfigVariable `key:"accentColor,public"`         // Public
	DisableAnimations         AppConfigVariable `key:"disableAnimations,public"`   // Public
	AllowOwnAccountEdit       AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
	AllowUserSignups          AppConfigVariable `key:"allowUserSignups,public"`    // Public
	SignupDefaultUserGroupIDs AppConfigVariable `key:"signupDefaultUserGroupIDs"`
	SignupDefaultCustomClaims AppConfigVariable `key:"signupDefaultCustomClaims"`
	// Internal
	InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal
	// Email
	RequireUserEmail                           AppConfigVariable `key:"requireUserEmail,public"` // Public
	SmtpHost                                   AppConfigVariable `key:"smtpHost"`
	SmtpPort                                   AppConfigVariable `key:"smtpPort"`
	SmtpFrom                                   AppConfigVariable `key:"smtpFrom"`
	SmtpUser                                   AppConfigVariable `key:"smtpUser"`
	SmtpPassword                               AppConfigVariable `key:"smtpPassword,sensitive"`
	SmtpTls                                    AppConfigVariable `key:"smtpTls"`
	SmtpSkipCertVerify                         AppConfigVariable `key:"smtpSkipCertVerify"`
	EmailLoginNotificationEnabled              AppConfigVariable `key:"emailLoginNotificationEnabled"`
	EmailOneTimeAccessAsUnauthenticatedEnabled AppConfigVariable `key:"emailOneTimeAccessAsUnauthenticatedEnabled,public"` // Public
	EmailOneTimeAccessAsAdminEnabled           AppConfigVariable `key:"emailOneTimeAccessAsAdminEnabled,public"`           // Public
	EmailApiKeyExpirationEnabled               AppConfigVariable `key:"emailApiKeyExpirationEnabled"`
	EmailVerificationEnabled                   AppConfigVariable `key:"emailVerificationEnabled,public"` // Public
	// LDAP
	LdapEnabled                        AppConfigVariable `key:"ldapEnabled,public"` // Public
	LdapUrl                            AppConfigVariable `key:"ldapUrl"`
	LdapBindDn                         AppConfigVariable `key:"ldapBindDn"`
	LdapBindPassword                   AppConfigVariable `key:"ldapBindPassword,sensitive"`
	LdapBase                           AppConfigVariable `key:"ldapBase"`
	LdapUserSearchFilter               AppConfigVariable `key:"ldapUserSearchFilter"`
	LdapUserGroupSearchFilter          AppConfigVariable `key:"ldapUserGroupSearchFilter"`
	LdapSkipCertVerify                 AppConfigVariable `key:"ldapSkipCertVerify"`
	LdapAttributeUserUniqueIdentifier  AppConfigVariable `key:"ldapAttributeUserUniqueIdentifier"`
	LdapAttributeUserUsername          AppConfigVariable `key:"ldapAttributeUserUsername"`
	LdapAttributeUserEmail             AppConfigVariable `key:"ldapAttributeUserEmail"`
	LdapAttributeUserFirstName         AppConfigVariable `key:"ldapAttributeUserFirstName"`
	LdapAttributeUserLastName          AppConfigVariable `key:"ldapAttributeUserLastName"`
	LdapAttributeUserDisplayName       AppConfigVariable `key:"ldapAttributeUserDisplayName"`
	LdapAttributeUserProfilePicture    AppConfigVariable `key:"ldapAttributeUserProfilePicture"`
	LdapAttributeGroupMember           AppConfigVariable `key:"ldapAttributeGroupMember"`
	LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
	LdapAttributeGroupName             AppConfigVariable `key:"ldapAttributeGroupName"`
	LdapAdminGroupName                 AppConfigVariable `key:"ldapAdminGroupName"`
	LdapSoftDeleteUsers                AppConfigVariable `key:"ldapSoftDeleteUsers"`
}

func (c *AppConfig) ToAppConfigVariableSlice(showAll bool, redactSensitiveValues bool) []AppConfigVariable {
	// Use reflection to iterate through all fields
	cfgValue := reflect.ValueOf(c).Elem()
	cfgType := cfgValue.Type()

	var res []AppConfigVariable

	for i := range cfgType.NumField() {
		field := cfgType.Field(i)

		key, attrs, _ := strings.Cut(field.Tag.Get("key"), ",")
		if key == "" {
			continue
		}

		// If we're only showing public variables and this is not public, skip it
		if !showAll && attrs != "public" {
			continue
		}

		value := cfgValue.Field(i).FieldByName("Value").String()

		// Redact sensitive values if the value isn't empty, the UI config is disabled, and redactSensitiveValues is true
		if value != "" && common.EnvConfig.UiConfigDisabled && redactSensitiveValues && attrs == "sensitive" {
			value = "XXXXXXXXXX"
		}

		appConfigVariable := AppConfigVariable{
			Key:   key,
			Value: value,
		}

		res = append(res, appConfigVariable)
	}

	return res
}

func (c *AppConfig) FieldByKey(key string) (defaultValue string, isInternal bool, err error) {
	rv := reflect.ValueOf(c).Elem()
	rt := rv.Type()

	// Find the field in the struct whose "key" tag matches
	for i := range rt.NumField() {
		// Grab only the first part of the key, if there's a comma with additional properties
		tagValue := strings.Split(rt.Field(i).Tag.Get("key"), ",")
		keyFromTag := tagValue[0]
		isInternal = slices.Contains(tagValue, "internal")
		if keyFromTag != key {
			continue
		}

		valueField := rv.Field(i).FieldByName("Value")
		return valueField.String(), isInternal, nil
	}

	// If we are here, the config key was not found
	return "", false, AppConfigKeyNotFoundError{field: key}
}

func (c *AppConfig) UpdateField(key string, value string, noInternal bool) error {
	rv := reflect.ValueOf(c).Elem()
	rt := rv.Type()

	// Find the field in the struct whose "key" tag matches, then update that
	for i := range rt.NumField() {
		// Separate the key (before the comma) from any optional attributes after
		tagValue, attrs, _ := strings.Cut(rt.Field(i).Tag.Get("key"), ",")
		if tagValue != key {
			continue
		}

		// If the field is internal and noInternal is true, we skip that
		if noInternal && attrs == "internal" {
			return AppConfigInternalForbiddenError{field: key}
		}

		valueField := rv.Field(i).FieldByName("Value")
		if !valueField.CanSet() {
			return fmt.Errorf("field Value in AppConfigVariable is not settable for config key '%s'", key)
		}

		// Update the value
		valueField.SetString(value)

		// Return once updated
		return nil
	}

	// If we're here, we have not found the right field to update
	return AppConfigKeyNotFoundError{field: key}
}

type AppConfigKeyNotFoundError struct {
	field string
}

func (e AppConfigKeyNotFoundError) Error() string {
	return "cannot find config key '" + e.field + "'"
}

func (e AppConfigKeyNotFoundError) Is(target error) bool {
	// Ignore the field property when checking if an error is of the type AppConfigKeyNotFoundError
	x := AppConfigKeyNotFoundError{}
	return errors.As(target, &x)
}

type AppConfigInternalForbiddenError struct {
	field string
}

func (e AppConfigInternalForbiddenError) Error() string {
	return "field '" + e.field + "' is internal and can't be updated"
}

func (e AppConfigInternalForbiddenError) Is(target error) bool {
	// Ignore the field property when checking if an error is of the type AppConfigInternalForbiddenError
	x := AppConfigInternalForbiddenError{}
	return errors.As(target, &x)
}
