// Copyright 2019 Yunion
//
// 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 models

import (
	"context"
	"strings"

	"yunion.io/x/cloudmux/pkg/cloudprovider"
	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/compare"
	"yunion.io/x/pkg/util/samlutils"
	"yunion.io/x/sqlchemy"

	"yunion.io/x/onecloud/pkg/apis"
	api "yunion.io/x/onecloud/pkg/apis/cloudid"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
	"yunion.io/x/onecloud/pkg/cloudid/options"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
)

type SSAMLProviderManager struct {
	db.SStatusInfrasResourceBaseManager
	db.SExternalizedResourceBaseManager
	SCloudaccountResourceBaseManager
	SCloudproviderResourceBaseManager
}

var SAMLProviderManager *SSAMLProviderManager

func init() {
	SAMLProviderManager = &SSAMLProviderManager{
		SStatusInfrasResourceBaseManager: db.NewStatusInfrasResourceBaseManager(
			SSAMLProvider{},
			"saml_provider_tbl",
			"saml_provider",
			"saml_providers",
		),
	}
	SAMLProviderManager.SetVirtualObject(SAMLProviderManager)
}

type SSAMLProvider struct {
	db.SStatusInfrasResourceBase
	db.SExternalizedResourceBase

	SCloudaccountResourceBase
	SCloudproviderResourceBase

	EntityId         string `get:"domain" create:"domain_optional" list:"domain"`
	MetadataDocument string `get:"domain" create:"domain_optional"`
	AuthUrl          string `width:"512" charset:"ascii" get:"domain" list:"domain"`
}

func (manager *SSAMLProviderManager) GetIVirtualModelManager() db.IVirtualModelManager {
	return manager.GetVirtualObject().(db.IVirtualModelManager)
}

func (manager *SSAMLProviderManager) GetResourceCount() ([]db.SScopeResourceCount, error) {
	return nil, nil
}

func (manager *SSAMLProviderManager) FetchUniqValues(ctx context.Context, data jsonutils.JSONObject) jsonutils.JSONObject {
	accountId, _ := data.GetString("cloudaccount_id")
	return jsonutils.Marshal(map[string]string{"cloudaccount_id": accountId})
}

func (manager *SSAMLProviderManager) FilterByUniqValues(q *sqlchemy.SQuery, values jsonutils.JSONObject) *sqlchemy.SQuery {
	accountId, _ := values.GetString("cloudaccount_id")
	if len(accountId) > 0 {
		q = q.Equals("cloudaccount_id", accountId)
	}
	return q
}

func (self *SSAMLProvider) GetCloudprovider() (*SCloudprovider, error) {
	provider, err := CloudproviderManager.FetchById(self.ManagerId)
	if err != nil {
		return nil, errors.Wrapf(err, "CloudproviderManager.FetchById(%s)", self.ManagerId)
	}
	return provider.(*SCloudprovider), nil
}

func (self *SSAMLProvider) GetProvider() (cloudprovider.ICloudProvider, error) {
	if len(self.ManagerId) > 0 {
		provider, err := self.GetCloudprovider()
		if err != nil {
			return nil, errors.Wrap(err, "GetCloudprovider")
		}
		return provider.GetProvider()
	}
	account, err := self.GetCloudaccount()
	if err != nil {
		return nil, errors.Wrap(err, "GetCloudaccount")
	}
	return account.GetProvider()
}

// 公有云身份提供商列表
func (manager *SSAMLProviderManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.SAMLProviderListInput) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SStatusInfrasResourceBaseManager.ListItemFilter(ctx, q, userCred, query.StatusInfrasResourceBaseListInput)
	if err != nil {
		return nil, err
	}
	q, err = manager.SCloudaccountResourceBaseManager.ListItemFilter(ctx, q, userCred, query.CloudaccountResourceListInput)
	if err != nil {
		return nil, err
	}

	q, err = manager.SCloudproviderResourceBaseManager.ListItemFilter(ctx, q, userCred, query.CloudproviderResourceListInput)
	if err != nil {
		return nil, err
	}

	return q, nil
}

// 删除
func (self *SSAMLProvider) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	params := jsonutils.NewDict()
	return self.StartSAMLProviderDeleteTask(ctx, userCred, params, "")
}

func (self *SSAMLProvider) StartSAMLProviderDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, parentTaskId string) error {
	task, err := taskman.TaskManager.NewTask(ctx, "SAMLProviderDeleteTask", self, userCred, data, parentTaskId, "", nil)
	if err != nil {
		return errors.Wrap(err, "NewTask")
	}
	self.SetStatus(ctx, userCred, apis.STATUS_DELETING, "")
	return task.ScheduleRun(nil)
}

func (self *SSAMLProvider) GetISAMLProvider() (cloudprovider.ICloudSAMLProvider, error) {
	if len(self.ExternalId) == 0 {
		return nil, errors.Wrapf(cloudprovider.ErrNotFound, "empty external id")
	}
	provider, err := self.GetProvider()
	if err != nil {
		return nil, err
	}
	samlProviders, err := provider.GetICloudSAMLProviders()
	if err != nil {
		return nil, err
	}
	for i := range samlProviders {
		if samlProviders[i].GetGlobalId() == self.ExternalId {
			return samlProviders[i], nil
		}
	}
	return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%v", self.ExternalId)
}

func (self *SSAMLProvider) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return nil
}

func (self *SSAMLProvider) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return self.SStatusInfrasResourceBase.Delete(ctx, userCred)
}

func (self *SSAMLProvider) syncRemove(ctx context.Context, userCred mcclient.TokenCredential) error {
	return self.RealDelete(ctx, userCred)
}

func (self *SSAMLProvider) SyncWithCloudSAMLProvider(ctx context.Context, userCred mcclient.TokenCredential, ext cloudprovider.ICloudSAMLProvider, managerId string) error {
	_, err := db.Update(self, func() error {
		self.ExternalId = ext.GetGlobalId()
		self.AuthUrl = ext.GetAuthUrl(options.Options.ApiServer)
		metadata, _ := ext.GetMetadataDocument()
		if metadata != nil {
			self.EntityId = metadata.EntityId
			self.MetadataDocument = metadata.String()
		}
		self.Status = apis.STATUS_UNKNOWN
		if self.EntityId == options.Options.ApiServer && strings.Contains(self.MetadataDocument, "login/"+self.ManagerId) {
			self.Status = apis.STATUS_AVAILABLE
		}
		if len(managerId) > 0 {
			self.ManagerId = managerId
		}
		return nil
	})
	return err
}

func (self *SSAMLProvider) GetMetadataDocument() (samlutils.EntityDescriptor, error) {
	return samlutils.ParseMetadata([]byte(self.MetadataDocument))
}

func (manager *SSAMLProviderManager) FetchCustomizeColumns(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	objs []interface{},
	fields stringutils2.SSortedStrings,
	isList bool,
) []api.SAMLProviderDetails {
	rows := make([]api.SAMLProviderDetails, len(objs))
	infRows := manager.SStatusInfrasResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	acRows := manager.SCloudaccountResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	for i := range rows {
		rows[i] = api.SAMLProviderDetails{
			StatusInfrasResourceBaseDetails: infRows[i],
			CloudaccountResourceDetails:     acRows[i],
		}
	}
	return rows
}

func (self *SCloudaccount) SyncSAMLProviders(ctx context.Context, userCred mcclient.TokenCredential, samls []cloudprovider.ICloudSAMLProvider, managerId string) compare.SyncResult {
	result := compare.SyncResult{}
	dbSamls, err := self.GetSAMLProviders(managerId)
	if err != nil {
		result.Error(errors.Wrap(err, "GetSAMLProviders"))
		return result
	}

	removed := make([]SSAMLProvider, 0)
	commondb := make([]SSAMLProvider, 0)
	commonext := make([]cloudprovider.ICloudSAMLProvider, 0)
	added := make([]cloudprovider.ICloudSAMLProvider, 0)

	err = compare.CompareSets(dbSamls, samls, &removed, &commondb, &commonext, &added)
	if err != nil {
		result.Error(errors.Wrap(err, "compare.CompareSets"))
		return result
	}

	for i := 0; i < len(removed); i++ {
		err = removed[i].RealDelete(ctx, userCred)
		if err != nil {
			result.DeleteError(err)
			continue
		}
		result.Delete()
	}

	for i := 0; i < len(commondb); i++ {
		err = commondb[i].SyncWithCloudSAMLProvider(ctx, userCred, commonext[i], managerId)
		if err != nil {
			result.UpdateError(err)
			continue
		}
		result.Update()
	}

	for i := 0; i < len(added); i++ {
		err = self.newFromCloudSAMLProvider(ctx, userCred, added[i], managerId)
		if err != nil {
			result.AddError(err)
			continue
		}
		result.Add()
	}
	return result
}

func (self *SCloudaccount) newFromCloudSAMLProvider(ctx context.Context, userCred mcclient.TokenCredential, ext cloudprovider.ICloudSAMLProvider, managerId string) error {
	saml := &SSAMLProvider{}
	saml.SetModelManager(SAMLProviderManager, saml)
	saml.Name = ext.GetName()
	saml.ExternalId = ext.GetGlobalId()
	saml.DomainId = self.DomainId
	saml.CloudaccountId = self.Id
	saml.ManagerId = managerId
	saml.AuthUrl = ext.GetAuthUrl(options.Options.ApiServer)
	metadata, _ := ext.GetMetadataDocument()
	if metadata != nil {
		saml.EntityId = metadata.EntityId
		saml.MetadataDocument = metadata.String()
	}
	saml.Status = ext.GetStatus()
	saml.Status = apis.STATUS_UNKNOWN
	if saml.EntityId == options.Options.ApiServer && strings.Contains(saml.MetadataDocument, "login/"+saml.ManagerId) {
		saml.Status = apis.STATUS_AVAILABLE
	}
	return SAMLProviderManager.TableSpec().Insert(ctx, saml)
}

func (self *SCloudprovider) GetSamlProviders() ([]SSAMLProvider, error) {
	q := SAMLProviderManager.Query().Equals("manager_id", self.Id)
	ret := []SSAMLProvider{}
	err := db.FetchModelObjects(SAMLProviderManager, q, &ret)
	if err != nil {
		return nil, err
	}
	return ret, nil
}
