// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/casdoor/casdoor/idp"
	"github.com/casdoor/casdoor/util"
	"github.com/xorm-io/core"
)

type SignupItem struct {
	Name        string `json:"name"`
	Visible     bool   `json:"visible"`
	Required    bool   `json:"required"`
	Prompted    bool   `json:"prompted"`
	Label       string `json:"label"`
	Placeholder string `json:"placeholder"`
	Rule        string `json:"rule"`
}

type SamlItem struct {
	Name       string `json:"name"`
	NameFormat string `json:"nameformat"`
	Value      string `json:"value"`
}

type Application struct {
	Owner       string `xorm:"varchar(100) notnull pk" json:"owner"`
	Name        string `xorm:"varchar(100) notnull pk" json:"name"`
	CreatedTime string `xorm:"varchar(100)" json:"createdTime"`

	DisplayName         string          `xorm:"varchar(100)" json:"displayName"`
	Logo                string          `xorm:"varchar(200)" json:"logo"`
	HomepageUrl         string          `xorm:"varchar(100)" json:"homepageUrl"`
	Description         string          `xorm:"varchar(100)" json:"description"`
	Organization        string          `xorm:"varchar(100)" json:"organization"`
	Cert                string          `xorm:"varchar(100)" json:"cert"`
	EnablePassword      bool            `json:"enablePassword"`
	EnableSignUp        bool            `json:"enableSignUp"`
	EnableSigninSession bool            `json:"enableSigninSession"`
	EnableAutoSignin    bool            `json:"enableAutoSignin"`
	EnableCodeSignin    bool            `json:"enableCodeSignin"`
	EnableSamlCompress  bool            `json:"enableSamlCompress"`
	EnableSamlC14n10    bool            `json:"enableSamlC14n10"`
	EnableWebAuthn      bool            `json:"enableWebAuthn"`
	EnableLinkWithEmail bool            `json:"enableLinkWithEmail"`
	OrgChoiceMode       string          `json:"orgChoiceMode"`
	SamlReplyUrl        string          `xorm:"varchar(100)" json:"samlReplyUrl"`
	Providers           []*ProviderItem `xorm:"mediumtext" json:"providers"`
	SignupItems         []*SignupItem   `xorm:"varchar(2000)" json:"signupItems"`
	GrantTypes          []string        `xorm:"varchar(1000)" json:"grantTypes"`
	OrganizationObj     *Organization   `xorm:"-" json:"organizationObj"`
	CertPublicKey       string          `xorm:"-" json:"certPublicKey"`
	Tags                []string        `xorm:"mediumtext" json:"tags"`
	InvitationCodes     []string        `xorm:"varchar(200)" json:"invitationCodes"`
	SamlAttributes      []*SamlItem     `xorm:"varchar(1000)" json:"samlAttributes"`

	ClientId             string     `xorm:"varchar(100)" json:"clientId"`
	ClientSecret         string     `xorm:"varchar(100)" json:"clientSecret"`
	RedirectUris         []string   `xorm:"varchar(1000)" json:"redirectUris"`
	TokenFormat          string     `xorm:"varchar(100)" json:"tokenFormat"`
	ExpireInHours        int        `json:"expireInHours"`
	RefreshExpireInHours int        `json:"refreshExpireInHours"`
	SignupUrl            string     `xorm:"varchar(200)" json:"signupUrl"`
	SigninUrl            string     `xorm:"varchar(200)" json:"signinUrl"`
	ForgetUrl            string     `xorm:"varchar(200)" json:"forgetUrl"`
	AffiliationUrl       string     `xorm:"varchar(100)" json:"affiliationUrl"`
	TermsOfUse           string     `xorm:"varchar(100)" json:"termsOfUse"`
	SignupHtml           string     `xorm:"mediumtext" json:"signupHtml"`
	SigninHtml           string     `xorm:"mediumtext" json:"signinHtml"`
	ThemeData            *ThemeData `xorm:"json" json:"themeData"`
	FormCss              string     `xorm:"text" json:"formCss"`
	FormCssMobile        string     `xorm:"text" json:"formCssMobile"`
	FormOffset           int        `json:"formOffset"`
	FormSideHtml         string     `xorm:"mediumtext" json:"formSideHtml"`
	FormBackgroundUrl    string     `xorm:"varchar(200)" json:"formBackgroundUrl"`
}

func GetApplicationCount(owner, field, value string) (int64, error) {
	session := GetSession(owner, -1, -1, field, value, "", "")
	return session.Count(&Application{})
}

func GetOrganizationApplicationCount(owner, Organization, field, value string) (int64, error) {
	session := GetSession(owner, -1, -1, field, value, "", "")
	return session.Count(&Application{Organization: Organization})
}

func GetApplications(owner string) ([]*Application, error) {
	applications := []*Application{}
	err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner})
	if err != nil {
		return applications, err
	}

	return applications, nil
}

func GetOrganizationApplications(owner string, organization string) ([]*Application, error) {
	applications := []*Application{}
	err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Organization: organization})
	if err != nil {
		return applications, err
	}

	return applications, nil
}

func GetPaginationApplications(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) {
	var applications []*Application
	session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
	err := session.Find(&applications)
	if err != nil {
		return applications, err
	}

	return applications, nil
}

func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) {
	applications := []*Application{}
	session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
	err := session.Find(&applications, &Application{Organization: organization})
	if err != nil {
		return applications, err
	}

	return applications, nil
}

func getProviderMap(owner string) (m map[string]*Provider, err error) {
	providers, err := GetProviders(owner)
	if err != nil {
		return nil, err
	}

	m = map[string]*Provider{}
	for _, provider := range providers {
		// Get QRCode only once
		if provider.Type == "WeChat" && provider.DisableSsl && provider.Content == "" {
			provider.Content, err = idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2)
			if err != nil {
				return
			}
			UpdateProvider(provider.Owner+"/"+provider.Name, provider)
		}

		m[provider.Name] = GetMaskedProvider(provider, true)
	}

	return m, err
}

func extendApplicationWithProviders(application *Application) (err error) {
	m, err := getProviderMap(application.Organization)
	if err != nil {
		return err
	}

	for _, providerItem := range application.Providers {
		if provider, ok := m[providerItem.Name]; ok {
			providerItem.Provider = provider
		}
	}

	return
}

func extendApplicationWithOrg(application *Application) (err error) {
	organization, err := getOrganization(application.Owner, application.Organization)
	application.OrganizationObj = organization
	return
}

func getApplication(owner string, name string) (*Application, error) {
	if owner == "" || name == "" {
		return nil, nil
	}

	application := Application{Owner: owner, Name: name}
	existed, err := ormer.Engine.Get(&application)
	if err != nil {
		return nil, err
	}

	if existed {
		err = extendApplicationWithProviders(&application)
		if err != nil {
			return nil, err
		}

		err = extendApplicationWithOrg(&application)
		if err != nil {
			return nil, err
		}

		return &application, nil
	} else {
		return nil, nil
	}
}

func GetApplicationByOrganizationName(organization string) (*Application, error) {
	application := Application{}
	existed, err := ormer.Engine.Where("organization=?", organization).Get(&application)
	if err != nil {
		return nil, nil
	}

	if existed {
		err = extendApplicationWithProviders(&application)
		if err != nil {
			return nil, err
		}

		err = extendApplicationWithOrg(&application)
		if err != nil {
			return nil, err
		}

		return &application, nil
	} else {
		return nil, nil
	}
}

func GetApplicationByUser(user *User) (*Application, error) {
	if user.SignupApplication != "" {
		return getApplication("admin", user.SignupApplication)
	} else {
		return GetApplicationByOrganizationName(user.Owner)
	}
}

func GetApplicationByUserId(userId string) (application *Application, err error) {
	owner, name := util.GetOwnerAndNameFromId(userId)
	if owner == "app" {
		application, err = getApplication("admin", name)
		return
	}

	user, err := GetUser(userId)
	if err != nil {
		return nil, err
	}
	application, err = GetApplicationByUser(user)
	return
}

func GetApplicationByClientId(clientId string) (*Application, error) {
	application := Application{}
	existed, err := ormer.Engine.Where("client_id=?", clientId).Get(&application)
	if err != nil {
		return nil, err
	}

	if existed {
		err = extendApplicationWithProviders(&application)
		if err != nil {
			return nil, err
		}

		err = extendApplicationWithOrg(&application)
		if err != nil {
			return nil, err
		}

		return &application, nil
	} else {
		return nil, nil
	}
}

func GetApplication(id string) (*Application, error) {
	owner, name := util.GetOwnerAndNameFromId(id)
	return getApplication(owner, name)
}

func GetMaskedApplication(application *Application, userId string) *Application {
	if application == nil {
		return nil
	}

	if userId != "" {
		if isUserIdGlobalAdmin(userId) {
			return application
		}

		user, _ := GetUser(userId)
		if user != nil && user.IsApplicationAdmin(application) {
			return application
		}
	}

	if application.ClientSecret != "" {
		application.ClientSecret = "***"
	}

	if application.OrganizationObj != nil {
		if application.OrganizationObj.MasterPassword != "" {
			application.OrganizationObj.MasterPassword = "***"
		}
		if application.OrganizationObj.DefaultPassword != "" {
			application.OrganizationObj.DefaultPassword = "***"
		}
		if application.OrganizationObj.MasterVerificationCode != "" {
			application.OrganizationObj.MasterVerificationCode = "***"
		}
		if application.OrganizationObj.PasswordType != "" {
			application.OrganizationObj.PasswordType = "***"
		}
		if application.OrganizationObj.PasswordSalt != "" {
			application.OrganizationObj.PasswordSalt = "***"
		}
	}

	if application.InvitationCodes != nil {
		application.InvitationCodes = []string{"***"}
	}

	return application
}

func GetMaskedApplications(applications []*Application, userId string) []*Application {
	if isUserIdGlobalAdmin(userId) {
		return applications
	}

	for _, application := range applications {
		application = GetMaskedApplication(application, userId)
	}
	return applications
}

func GetAllowedApplications(applications []*Application, userId string) ([]*Application, error) {
	if userId == "" || isUserIdGlobalAdmin(userId) {
		return applications, nil
	}

	user, err := GetUser(userId)
	if err != nil {
		return nil, err
	}
	if user != nil && user.IsAdmin {
		return applications, nil
	}

	res := []*Application{}
	for _, application := range applications {
		var allowed bool
		allowed, err = CheckLoginPermission(userId, application)
		if err != nil {
			return nil, err
		}

		if allowed {
			res = append(res, application)
		}
	}
	return res, nil
}

func UpdateApplication(id string, application *Application) (bool, error) {
	owner, name := util.GetOwnerAndNameFromId(id)
	oldApplication, err := getApplication(owner, name)
	if oldApplication == nil {
		return false, err
	}

	if name == "app-built-in" {
		application.Name = name
	}

	if name != application.Name {
		err = applicationChangeTrigger(name, application.Name)
		if err != nil {
			return false, err
		}
	}

	applicationByClientId, err := GetApplicationByClientId(application.ClientId)
	if err != nil {
		return false, err
	}

	if oldApplication.ClientId != application.ClientId && applicationByClientId != nil {
		return false, err
	}

	for _, providerItem := range application.Providers {
		providerItem.Provider = nil
	}

	session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
	if application.ClientSecret == "***" {
		session.Omit("client_secret")
	}
	affected, err := session.Update(application)
	if err != nil {
		return false, err
	}

	return affected != 0, nil
}

func AddApplication(application *Application) (bool, error) {
	if application.Owner == "" {
		application.Owner = "admin"
	}
	if application.Organization == "" {
		application.Organization = "built-in"
	}
	if application.ClientId == "" {
		application.ClientId = util.GenerateClientId()
	}
	if application.ClientSecret == "" {
		application.ClientSecret = util.GenerateClientSecret()
	}

	app, err := GetApplicationByClientId(application.ClientId)
	if err != nil {
		return false, err
	}

	if app != nil {
		return false, nil
	}

	for _, providerItem := range application.Providers {
		providerItem.Provider = nil
	}

	affected, err := ormer.Engine.Insert(application)
	if err != nil {
		return false, nil
	}

	return affected != 0, nil
}

func DeleteApplication(application *Application) (bool, error) {
	if application.Name == "app-built-in" {
		return false, nil
	}

	affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{})
	if err != nil {
		return false, err
	}

	return affected != 0, nil
}

func (application *Application) GetId() string {
	return fmt.Sprintf("%s/%s", application.Owner, application.Name)
}

func (application *Application) IsRedirectUriValid(redirectUri string) bool {
	redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
	for _, targetUri := range redirectUris {
		targetUriRegex := regexp.MustCompile(targetUri)
		if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
			return true
		}
	}
	return false
}

func IsOriginAllowed(origin string) (bool, error) {
	applications, err := GetApplications("")
	if err != nil {
		return false, err
	}

	for _, application := range applications {
		if application.IsRedirectUriValid(origin) {
			return true, nil
		}
	}
	return false, nil
}

func getApplicationMap(organization string) (map[string]*Application, error) {
	applicationMap := make(map[string]*Application)
	applications, err := GetOrganizationApplications("admin", organization)
	if err != nil {
		return applicationMap, err
	}

	for _, application := range applications {
		applicationMap[application.Name] = application
	}

	return applicationMap, nil
}

func ExtendManagedAccountsWithUser(user *User) (*User, error) {
	if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
		return user, nil
	}

	applicationMap, err := getApplicationMap(user.Owner)
	if err != nil {
		return user, err
	}

	var managedAccounts []ManagedAccount
	for _, managedAccount := range user.ManagedAccounts {
		application := applicationMap[managedAccount.Application]
		if application != nil {
			managedAccount.SigninUrl = application.SigninUrl
			managedAccounts = append(managedAccounts, managedAccount)
		}
	}
	user.ManagedAccounts = managedAccounts

	return user, nil
}

func applicationChangeTrigger(oldName string, newName string) error {
	session := ormer.Engine.NewSession()
	defer session.Close()

	err := session.Begin()
	if err != nil {
		return err
	}

	organization := new(Organization)
	organization.DefaultApplication = newName
	_, err = session.Where("default_application=?", oldName).Update(organization)
	if err != nil {
		return err
	}

	user := new(User)
	user.SignupApplication = newName
	_, err = session.Where("signup_application=?", oldName).Update(user)
	if err != nil {
		return err
	}

	resource := new(Resource)
	resource.Application = newName
	_, err = session.Where("application=?", oldName).Update(resource)
	if err != nil {
		return err
	}

	var permissions []*Permission
	err = ormer.Engine.Find(&permissions)
	if err != nil {
		return err
	}
	for i := 0; i < len(permissions); i++ {
		permissionResoureces := permissions[i].Resources
		for j := 0; j < len(permissionResoureces); j++ {
			if permissionResoureces[j] == oldName {
				permissionResoureces[j] = newName
			}
		}
		permissions[i].Resources = permissionResoureces
		_, err = session.Where("name=?", permissions[i].Name).Update(permissions[i])
		if err != nil {
			return err
		}
	}

	return session.Commit()
}
