package user

import (
	"context"
	"crypto/md5"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/cloudreve/Cloudreve/v4/application/dependency"
	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/auth"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/request"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
	"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
	"github.com/cloudreve/Cloudreve/v4/pkg/util"
	"github.com/gin-gonic/gin"
	"github.com/pquerna/otp/totp"
)

const (
	twoFaEnableSessionKey = "2fa_init_"
)

// Init2FA 初始化二步验证
func Init2FA(c *gin.Context) (string, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)

	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      "Cloudreve",
		AccountName: user.Email,
	})
	if err != nil {
		return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
	}

	if err := dep.KV().Set(fmt.Sprintf("%s%d", twoFaEnableSessionKey, user.ID), key.Secret(), 600); err != nil {
		return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to store TOTP session", err)
	}

	return key.Secret(), nil
}

type (
	// AvatarService Service to get avatar
	GetAvatarService struct {
		NoCache bool `form:"nocache"`
	}
	GetAvatarServiceParamsCtx struct{}
)

const (
	GravatarAvatar = "gravatar"
	FileAvatar     = "file"
)

// Get 获取用户头像
func (service *GetAvatarService) Get(c *gin.Context) error {
	dep := dependency.FromContext(c)
	settings := dep.SettingProvider()
	// 查找目标用户
	uid := hashid.FromContext(c)
	userClient := dep.UserClient()
	user, err := userClient.GetByID(c, uid)

	if err != nil {
		return serializer.NewError(serializer.CodeUserNotFound, "", err)
	}

	if !service.NoCache {
		c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", settings.PublicResourceMaxAge(c)))
	}

	// 未设定头像时，返回404错误
	if user.Avatar == "" {
		c.Status(404)
		return nil
	}

	avatarSettings := settings.Avatar(c)

	// Gravatar 头像重定向
	if user.Avatar == GravatarAvatar {
		gravatarRoot, err := url.Parse(avatarSettings.Gravatar)
		if err != nil {
			return serializer.NewError(serializer.CodeInternalSetting, "Failed to parse Gravatar server", err)
		}
		email_lowered := strings.ToLower(user.Email)
		has := md5.Sum([]byte(email_lowered))
		avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=200", has))

		c.Redirect(http.StatusFound, gravatarRoot.ResolveReference(avatar).String())
		return nil
	}

	// 本地文件头像
	if user.Avatar == FileAvatar {
		avatarRoot := util.DataPath(avatarSettings.Path)

		avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", user.ID)))
		if err != nil {
			dep.Logger().Warning("Failed to open avatar file", err)
			c.Status(404)
		}
		defer avatar.Close()

		http.ServeContent(c.Writer, c.Request, "avatar.png", user.UpdatedAt, avatar)
		return nil
	}

	c.Status(404)
	return nil
}

// Settings 获取用户设定
func GetUserSettings(c *gin.Context) (*UserSettings, error) {
	dep := dependency.FromContext(c)
	u := inventory.UserFromContext(c)
	userClient := dep.UserClient()
	passkeys, err := userClient.ListPasskeys(c, u.ID)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user passkey", err)
	}

	ctx := context.WithValue(c, inventory.LoadOAuthGrantClient{}, true)
	grants, err := dep.OAuthClientClient().GetGrantsByUserID(ctx, u.ID)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user OAuth grants", err)
	}

	return BuildUserSettings(u, passkeys, dep.UAParser(), grants), nil

	// 用户组有效期

	//return serializer.Response{
	//	Data: map[string]interface{}{
	//		"uid":           user.ID,
	//		"qq":            user.OpenID != "",
	//		"homepage":      !user.OptionsSerialized.ProfileOff,
	//		"two_factor":    user.TwoFactor != "",
	//		"prefer_theme":  user.OptionsSerialized.PreferredTheme,
	//		"themes":        model.GetSettingByName("themes"),
	//		"group_expires": groupExpires,
	//		"authn":         serializer.BuildWebAuthnList(user.WebAuthnCredentials()),
	//	},
	//}
}

func UpdateUserAvatar(c *gin.Context) error {
	dep := dependency.FromContext(c)
	u := inventory.UserFromContext(c)
	settings := dep.SettingProvider()

	avatarSettings := settings.AvatarProcess(c)
	if c.Request.ContentLength == -1 || c.Request.ContentLength > avatarSettings.MaxFileSize {
		request.BlackHole(c.Request.Body)
		return serializer.NewError(serializer.CodeFileTooLarge, "", nil)
	}

	if c.Request.ContentLength == 0 {
		// Use Gravatar for empty body
		if _, err := dep.UserClient().UpdateAvatar(c, u, GravatarAvatar); err != nil {
			return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
		}

		return nil
	}

	return updateAvatarFile(c, u, c.GetHeader("Content-Type"), c.Request.Body, avatarSettings)
}

func updateAvatarFile(ctx context.Context, u *ent.User, contentType string, file io.Reader, avatarSettings *setting.AvatarProcess) error {
	dep := dependency.FromContext(ctx)
	// Detect ext from content type
	ext := "png"
	switch contentType {
	case "image/jpeg", "image/jpg":
		ext = "jpg"
	case "image/gif":
		ext = "gif"
	}
	avatar, err := thumb.NewThumbFromFile(file, ext)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "Invalid image", err)
	}

	// Resize and save avatar
	avatar.CreateAvatar(avatarSettings.MaxWidth)
	avatarRoot := util.DataPath(avatarSettings.Path)
	f, err := util.CreatNestedFile(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", u.ID)))
	if err != nil {
		return serializer.NewError(serializer.CodeIOFailed, "Failed to create avatar file", err)
	}

	defer f.Close()
	if err := avatar.Save(f, &setting.ThumbEncode{
		Quality: 100,
		Format:  "png",
	}); err != nil {
		return serializer.NewError(serializer.CodeIOFailed, "Failed to save avatar file", err)
	}

	if _, err := dep.UserClient().UpdateAvatar(ctx, u, FileAvatar); err != nil {
		return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
	}

	return nil
}

type (
	PatchUserSetting struct {
		Nick                    *string   `json:"nick" binding:"omitempty,min=1,max=255"`
		Language                *string   `json:"language" binding:"omitempty,min=1,max=255"`
		PreferredTheme          *string   `json:"preferred_theme" binding:"omitempty,hexcolor|rgb|rgba|hsl"`
		VersionRetentionEnabled *bool     `json:"version_retention_enabled" binding:"omitempty"`
		VersionRetentionExt     *[]string `json:"version_retention_ext" binding:"omitempty"`
		VersionRetentionMax     *int      `json:"version_retention_max" binding:"omitempty,min=0"`
		CurrentPassword         *string   `json:"current_password" binding:"omitempty,min=4,max=128"`
		NewPassword             *string   `json:"new_password" binding:"omitempty,min=6,max=128"`
		TwoFAEnabled            *bool     `json:"two_fa_enabled" binding:"omitempty"`
		TwoFACode               *string   `json:"two_fa_code" binding:"omitempty"`
		DisableViewSync         *bool     `json:"disable_view_sync" binding:"omitempty"`
		ShareLinksInProfile     *string   `json:"share_links_in_profile" binding:"omitempty"`
	}
	PatchUserSettingParamsCtx struct{}
)

func (s *PatchUserSetting) Patch(c *gin.Context) error {
	dep := dependency.FromContext(c)
	u := inventory.UserFromContext(c)
	userClient := dep.UserClient()
	saveSetting := false

	if s.Nick != nil {
		if _, err := userClient.UpdateNickname(c, u, *s.Nick); err != nil {
			return serializer.NewError(serializer.CodeDBError, "Failed to update user nick", err)
		}
	}

	if s.Language != nil {
		u.Settings.Language = *s.Language
		saveSetting = true
	}

	if s.PreferredTheme != nil {
		u.Settings.PreferredTheme = *s.PreferredTheme
		saveSetting = true
	}

	if s.VersionRetentionEnabled != nil {
		u.Settings.VersionRetention = *s.VersionRetentionEnabled
		saveSetting = true
	}

	if s.VersionRetentionExt != nil {
		u.Settings.VersionRetentionExt = *s.VersionRetentionExt
		saveSetting = true
	}

	if s.VersionRetentionMax != nil {
		u.Settings.VersionRetentionMax = *s.VersionRetentionMax
		saveSetting = true
	}

	if s.DisableViewSync != nil {
		u.Settings.DisableViewSync = *s.DisableViewSync
		saveSetting = true
	}

	if s.ShareLinksInProfile != nil {
		u.Settings.ShareLinksInProfile = types.ShareLinksInProfileLevel(*s.ShareLinksInProfile)
		saveSetting = true
	}

	if s.CurrentPassword != nil && s.NewPassword != nil {
		if err := auth.CheckScope(c, types.ScopeUserSecurityInfoWrite); err != nil {
			return err
		}

		if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
			return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
		}

		if _, err := userClient.UpdatePassword(c, u, *s.NewPassword); err != nil {
			return serializer.NewError(serializer.CodeDBError, "Failed to update user password", err)
		}
	}

	if s.TwoFAEnabled != nil {
		if err := auth.CheckScope(c, types.ScopeUserSecurityInfoWrite); err != nil {
			return err
		}

		if *s.TwoFAEnabled {
			kv := dep.KV()
			secret, ok := kv.Get(fmt.Sprintf("%s%d", twoFaEnableSessionKey, u.ID))
			if !ok {
				return serializer.NewError(serializer.CodeInternalSetting, "You have not initiated 2FA session", nil)
			}

			if !totp.Validate(*s.TwoFACode, secret.(string)) {
				return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
			}

			if _, err := userClient.UpdateTwoFASecret(c, u, secret.(string)); err != nil {
				return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
			}

		} else {
			if !totp.Validate(*s.TwoFACode, u.TwoFactorSecret) {
				return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
			}

			if _, err := userClient.UpdateTwoFASecret(c, u, ""); err != nil {
				return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
			}

		}
	}

	if saveSetting {
		if err := userClient.SaveSettings(c, u); err != nil {
			return serializer.NewError(serializer.CodeDBError, "Failed to update user settings", err)
		}
	}

	return nil
}
