// 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 (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"html"
	"io/ioutil"
	"path/filepath"
	"strings"
	ptem "text/template"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/appctx"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/httputils"
	"yunion.io/x/pkg/utils"
	"yunion.io/x/sqlchemy"

	api "yunion.io/x/onecloud/pkg/apis/notify"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	"yunion.io/x/onecloud/pkg/notify/options"
)

type STemplateManager struct {
	db.SStandaloneAnonResourceBaseManager
}

var TemplateManager *STemplateManager

func init() {
	TemplateManager = &STemplateManager{
		SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager(
			STemplate{},
			"template_tbl",
			"notifytemplate",
			"notifytemplates",
		),
	}
	TemplateManager.SetVirtualObject(TemplateManager)
}

const (
	CONTACTTYPE_ALL = "all"
)

type STemplate struct {
	db.SStandaloneAnonResourceBase

	ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"`
	Topic       string `width:"128" nullable:"false" create:"required" update:"user" list:"user"`

	// title | content | remote
	TemplateType string `width:"10" nullable:"false" create:"required" update:"user" list:"user"`
	Content      string `length:"text" nullable:"true" create:"required" get:"user" list:"user" update:"user"`
	Lang         string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user" create:"optional"`
	Example      string `nullable:"true" create:"optional" get:"user" list:"user" update:"user"`
}

const (
	verifyUrlPath = "/email-verification/id/{0}/token/{1}?region=%s"
	templatePath  = "/opt/yunion/share/template"
)

func (tm *STemplateManager) GetEmailUrl() string {
	return httputils.JoinPath(options.Options.ApiServer, fmt.Sprintf(verifyUrlPath, options.Options.Region))
}

func (tm *STemplateManager) defaultTemplate() ([]STemplate, error) {
	templates := make([]STemplate, 0, 4)

	for _, templateType := range []string{"title", "content"} {
		for _, lang := range []string{api.TEMPLATE_LANG_CN, api.TEMPLATE_LANG_EN} {
			contactType, topic := CONTACTTYPE_ALL, ""
			titleTemplatePath := fmt.Sprintf("%s/%s@%s", templatePath, templateType, lang)
			files, err := ioutil.ReadDir(titleTemplatePath)
			if err != nil {
				return templates, errors.Wrapf(err, "Read Dir '%s'", titleTemplatePath)
			}
			for _, file := range files {
				if file.IsDir() {
					continue
				}
				spliteName := strings.Split(file.Name(), ".")
				topic = spliteName[0]
				if len(spliteName) > 1 {
					contactType = spliteName[1]
				}
				fullPath := filepath.Join(titleTemplatePath, file.Name())
				content, err := ioutil.ReadFile(fullPath)
				if err != nil {
					return templates, err
				}
				templates = append(templates, STemplate{
					ContactType:  contactType,
					Topic:        topic,
					Lang:         lang,
					TemplateType: templateType,
					Content:      string(content),
				})
			}
		}
	}
	return templates, nil
}

type SCompanyInfo struct {
	LoginLogo       string `json:"login_logo"`
	LoginLogoFormat string `json:"login_logo_format"`
	Copyright       string `json:"copyright"`
	Name            string `json:"name"`
}

func (tm *STemplateManager) GetCompanyInfo(ctx context.Context) (SCompanyInfo, error) {
	return SCompanyInfo{
		Name: options.Options.GetPlatformName(appctx.Lang(ctx)),
	}, nil
}

var (
	forceInitTopic = []string{
		"VERIFY",
		"USER_LOGIN_EXCEPTION",
	}
	defaultLang = api.TEMPLATE_LANG_CN
)

func getTemplateLangFromCtx(ctx context.Context) string {
	return notifyclientI18nTable.Lookup(ctx, tempalteLang)
}

func (tm *STemplateManager) InitializeData() error {
	// init lang
	q := tm.Query().IsEmpty("lang")
	var noLangTemplates []STemplate
	err := db.FetchModelObjects(tm, q, &noLangTemplates)
	if err != nil {
		return errors.Wrap(err, "unable to fetch templates")
	}
	for i := range noLangTemplates {
		t := &noLangTemplates[i]
		_, err := db.Update(t, func() error {
			t.Lang = defaultLang
			return nil
		})
		if err != nil {
			return err
		}
	}
	templates, err := tm.defaultTemplate()
	if err != nil {
		return err
	}
	for _, template := range templates {
		q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType).Equals("lang", template.Lang)
		count, _ := q.CountWithError()
		if count > 0 && !utils.IsInStringArray(template.Topic, forceInitTopic) {
			continue
		}
		if count == 0 {
			err := tm.TableSpec().Insert(context.TODO(), &template)
			if err != nil {
				return errors.Wrap(err, "sqlchemy.TableSpec.Insert")
			}
			continue
		}
		oldTemplates := make([]STemplate, 0, 1)
		err := db.FetchModelObjects(tm, q, &oldTemplates)
		if err != nil {
			return errors.Wrap(err, "db.FetchModelObjects")
		}
		// delete addtion
		var (
			ctx      = context.Background()
			userCred = auth.AdminCredential()
		)
		for i := 1; i < len(oldTemplates); i++ {
			err := oldTemplates[i].Delete(ctx, userCred)
			if err != nil {
				return errors.Wrap(err, "STemplate.Delete")
			}
		}
		// update
		oldTemplate := &oldTemplates[0]
		_, err = db.Update(oldTemplate, func() error {
			oldTemplate.Content = template.Content
			return nil
		})
		if err != nil {
			return errors.Wrap(err, "db.Update")
		}
	}
	return nil
}

// FillWithTemplate will return the title and content generated by corresponding template.
// Local cache about common template will be considered in case of performance issues.
func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) {
	if len(lang) == 0 {
		params.Title = no.Topic
		params.Message = no.Message
		return
	}
	params.Topic = no.Topic
	templates := make([]STemplate, 0, 3)
	// if strings.Contains(no.Topic, "-cn") || strings.Contains(no.Topic, "-en") {
	// 	no.Topic = no.Topic[:len(no.Topic)-3]
	// }

	q := tm.Query().Equals("topic", strings.ToUpper(no.Topic)).Equals("lang", lang).In("contact_type", []string{CONTACTTYPE_ALL, no.ContactType})
	err = db.FetchModelObjects(tm, q, &templates)
	if errors.Cause(err) == sql.ErrNoRows || len(templates) == 0 {
		// no such template, return as is
		params.Title = no.Topic
		params.Message = no.Message
		return
	}
	if err != nil {
		err = errors.Wrap(err, "db.FetchModelObjects")
		return
	}
	for _, template := range tm.chooseTemplate(no.ContactType, templates) {
		var title, content string
		switch template.TemplateType {
		case api.TEMPLATE_TYPE_TITLE:
			title, err = template.Execute(no.Message)
			if err != nil {
				return
			}
			params.Title = title
		case api.TEMPLATE_TYPE_CONTENT:
			content, err = template.Execute(no.Message)
			if err != nil {
				return
			}
			params.Message = content
		case api.TEMPLATE_TYPE_REMOTE:
			params.RemoteTemplate = template.Content
			params.Message = no.Message
		default:
			err = errors.Error("no support template type")
			return
		}
	}
	params.Message = html.UnescapeString(params.Message)
	return
}

func (tm *STemplateManager) chooseTemplate(contactType string, tempaltes []STemplate) []*STemplate {
	var titleTemplate, contentTemplate *STemplate
	// contactType first
	for i := range tempaltes {
		switch tempaltes[i].TemplateType {
		case api.TEMPLATE_TYPE_REMOTE:
			if tempaltes[i].ContactType == contactType {
				return []*STemplate{&tempaltes[i]}
			}
		case api.TEMPLATE_TYPE_TITLE:
			if tempaltes[i].ContactType == contactType {
				titleTemplate = &tempaltes[i]
			} else if titleTemplate == nil {
				titleTemplate = &tempaltes[i]
			}
		case api.TEMPLATE_TYPE_CONTENT:
			if tempaltes[i].ContactType == contactType {
				contentTemplate = &tempaltes[i]
			} else if contentTemplate == nil {
				contentTemplate = &tempaltes[i]
			}
		}
	}
	ret := make([]*STemplate, 0, 2)
	if titleTemplate != nil {
		ret = append(ret, titleTemplate)
	}
	if contentTemplate != nil {
		ret = append(ret, contentTemplate)
	}
	return ret
}

func (tm *STemplate) Execute(str string) (string, error) {
	tem, err := ptem.New("tmp").Parse(tm.Content)
	if err != nil {
		return "", errors.Wrapf(err, "Template.Parse for template %s", tm.GetId())
	}
	var buffer bytes.Buffer
	tmpMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(str), &tmpMap)
	if err != nil {
		return "", errors.Wrap(err, "json.Unmarshal")
	}
	err = tem.Execute(&buffer, tmpMap)
	if err != nil {
		return "", errors.Wrap(err, "template,Execute")
	}
	return buffer.String(), nil
}

func (tm *STemplateManager) PerformSave(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.TemplateManagerSaveInput) (jsonutils.JSONObject, error) {
	q := tm.Query().Equals("contact_type", input.ContactType)
	templates := []STemplate{}
	err := db.FetchModelObjects(tm, q, &templates)
	if err != nil {
		return nil, err
	}
	tempaltesMap := make(map[string]*api.TemplateCreateInput, len(input.Templates))
	for i := range input.Templates {
		template := &input.Templates[i]
		if template.ContactType != input.ContactType {
			continue
		}
		input.Templates[i], err = tm.ValidateCreateData(ctx, userCred, userCred, nil, input.Templates[i])
		if err != nil {
			return nil, err
		}
		key := fmt.Sprintf("%s-%s-%s", template.Topic, template.TemplateType, template.Lang)
		tempaltesMap[key] = template
	}
	for i := range templates {
		key := fmt.Sprintf("%s-%s-%s", templates[i].Topic, templates[i].TemplateType, templates[i].Lang)
		if _, ok := tempaltesMap[key]; !ok {
			continue
		}
		if input.Force {
			err := templates[i].Delete(ctx, userCred)
			if err != nil {
				return nil, errors.Wrapf(err, "unable to delete template %s", templates[i].Id)
			}
		} else {
			delete(tempaltesMap, key)
		}
	}
	for _, template := range tempaltesMap {
		t := STemplate{
			ContactType:  input.ContactType,
			Topic:        template.Topic,
			TemplateType: template.TemplateType,
			Lang:         template.Lang,
			Example:      template.Example,
			Content:      template.Content,
		}
		err = tm.TableSpec().Insert(ctx, &t)
		if err != nil {
			return nil, errors.Wrap(err, "unable to insert template")
		}
	}
	return nil, nil
}

func (tm *STemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.TemplateCreateInput) (api.TemplateCreateInput, error) {
	var err error
	input.StandaloneAnonResourceCreateInput, err = tm.SStandaloneAnonResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StandaloneAnonResourceCreateInput)
	if err != nil {
		return input, err
	}
	if !utils.IsInStringArray(input.TemplateType, []string{
		api.TEMPLATE_TYPE_CONTENT, api.TEMPLATE_TYPE_REMOTE, api.TEMPLATE_TYPE_TITLE,
	}) {
		return input, httperrors.NewInputParameterError("no such support for tempalte type %s", input.TemplateType)
	}
	if input.TemplateType != api.TEMPLATE_TYPE_REMOTE {
		if err := tm.validate(input.Content, input.Example); err != nil {
			return input, httperrors.NewInputParameterError("%s", err.Error())
		}
	}
	if input.Lang == "" {
		input.Lang = api.TEMPLATE_LANG_CN
	}
	if !utils.IsInStringArray(input.Lang, []string{api.TEMPLATE_LANG_EN, api.TEMPLATE_LANG_CN}) {
		return input, httperrors.NewInputParameterError("no such lang %s", input.Lang)
	}
	return input, nil
}

func (tm *STemplateManager) validate(template string, example string) error {
	// check example availability
	tem, err := ptem.New("tmp").Parse(template)
	if err != nil {
		return errors.Wrap(err, "invalid template")
	}
	var buffer bytes.Buffer
	tmpMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(example), &tmpMap)
	if err != nil {
		return errors.Wrap(err, "invalid example")
	}
	err = tem.Execute(&buffer, tmpMap)
	if err != nil {
		return errors.Wrap(err, "invalid example")
	}
	return nil
}

func (tm *STemplateManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.TemplateListInput) (*sqlchemy.SQuery, error) {
	q, err := tm.SStandaloneAnonResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneAnonResourceListInput)
	if err != nil {
		return nil, err
	}
	if len(input.Topic) > 0 {
		q = q.Equals("topic", input.Topic)
	}
	if len(input.TemplateType) > 0 {
		q = q.Equals("template_type", input.TemplateType)
	}
	if len(input.ContactType) > 0 {
		q = q.Equals("contact_type", input.ContactType)
	}
	if len(input.Lang) > 0 {
		q = q.Equals("lang", input.Lang)
	}
	return q, nil
}

func (t *STemplate) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.TemplateUpdateInput) (api.TemplateUpdateInput, error) {
	var err error
	input.StandaloneAnonResourceBaseUpdateInput, err = t.SStandaloneAnonResourceBase.ValidateUpdateData(ctx, userCred, query, input.StandaloneAnonResourceBaseUpdateInput)
	if err != nil {
		return input, err
	}
	if t.TemplateType == api.TEMPLATE_TYPE_REMOTE {
		return input, nil
	}
	if err := TemplateManager.validate(input.Content, input.Example); err != nil {
		return input, httperrors.NewInputParameterError("%s", err.Error())
	}
	return input, nil
}
