// 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 notifyclient

import (
	"context"
	"fmt"
	"html/template"
	"regexp"
	"strings"
	"time"

	"golang.org/x/text/language"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/appctx"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/httputils"
	"yunion.io/x/pkg/util/sets"

	"yunion.io/x/onecloud/pkg/cloudcommon/consts"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/modulebase"
	"yunion.io/x/onecloud/pkg/mcclient/modules/identity"
	npk "yunion.io/x/onecloud/pkg/mcclient/modules/notify"
)

func notifySystemWarning(ctx context.Context, idstr string, name string, event string, reason string) {
	msg := SSystemEventMsg{
		Id:      idstr,
		Name:    name,
		Event:   event,
		Reason:  reason,
		Created: time.Now(),
	}
	systemNotify(ctx, npk.NotifyPriorityImportant, SYSTEM_WARNING, jsonutils.Marshal(msg))
}

func notifyGeneralSystemError(ctx context.Context, data jsonutils.JSONObject) {
	systemNotify(ctx, npk.NotifyPriorityCritical, SYSTEM_ERROR, data)
}

func notifySystemError(ctx context.Context, idstr string, name string, event string, reason string) {
	msg := SSystemEventMsg{
		Id:      idstr,
		Name:    name,
		Event:   event,
		Reason:  reason,
		Created: time.Now(),
	}
	systemNotify(ctx, npk.NotifyPriorityCritical, SYSTEM_ERROR, jsonutils.Marshal(msg))
}

func systemNotify(ctx context.Context, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject) {
	systemNotifyWithTemplateFuncs(ctx, priority, event, data, nil)
}

func systemNotifyWithTemplateFuncs(ctx context.Context, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) {
	// userId
	notifyWithTemplateFuncs(ctx, notifyAdminUsers, false, priority, event, data, templateFuncs)

	// groupId
	notifyWithTemplateFuncs(ctx, notifyAdminGroups, true, priority, event, data, templateFuncs)
}

func notifyAll(ctx context.Context, recipientId []string, isGroup bool, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject) error {
	return notifyAllWithTemplateFuncs(ctx, recipientId, isGroup, priority, event, data, nil)
}

func notifyAllWithTemplateFuncs(ctx context.Context, recipientId []string, isGroup bool, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) error {
	s, err := AdminSessionGenerator(ctx, consts.GetRegion())
	if err != nil {
		return err
	}
	params := jsonutils.NewDict()
	result, err := npk.NotifyReceiver.PerformClassAction(s, "get-types", params)
	if err != nil {
		return err
	}
	if result == nil {
		return nil
	}
	jarray, err := result.Get("types")
	if err != nil {
		return err
	}
	cTypes := jarray.(*jsonutils.JSONArray).GetStringArray()
	for _, ct := range cTypes {
		RawNotifyWithCtxAndTemplateFuncs(ctx, recipientId, isGroup, npk.TNotifyChannel(ct), priority, event, data, templateFuncs)
	}
	return nil
}

type sTarget struct {
	reIds    []string
	contacts []string
	robots   []string
}

func langRobot(ctx context.Context, robots []string) (map[language.Tag]*sTarget, error) {
	contextLang := appctx.Lang(ctx)
	robotLang, err := getRobotLang(robots)
	if err != nil {
		return nil, err
	}
	ret := make(map[language.Tag]*sTarget)
	for robot, langStr := range robotLang {
		lang, err := language.Parse(langStr)
		if err != nil {
			log.Errorf("can't parse %s to language.Tag: %v", langStr, err)
			lang = contextLang
		}
		t := ret[lang]
		if t == nil {
			ret[lang] = &sTarget{}
			t = ret[lang]
		}
		t.robots = append(t.robots, robot)
	}
	return ret, nil
}

func lang(ctx context.Context, contactType npk.TNotifyChannel, reIds []string, contacts []string) (map[language.Tag]*sTarget, error) {
	contextLang := appctx.Lang(ctx)
	langMap := make(map[language.Tag]*sTarget)
	insertReid := func(lang language.Tag, id string) {
		t := langMap[lang]
		if t == nil {
			t = &sTarget{}
			langMap[lang] = t
		}
		t.reIds = append(t.reIds, id)
	}
	insertContact := func(lang language.Tag, id string) {
		t := langMap[lang]
		if t == nil {
			t = &sTarget{}
			langMap[lang] = t
		}
		t.contacts = append(t.contacts, id)
	}

	uids := append([]string{}, reIds...)
	if contactType == npk.NotifyByWebConsole {
		uids = append(uids, contacts...)
	}

	uidLang, err := UserLangFetcher(uids)
	if err != nil {
		return nil, errors.Wrap(err, "unable to feth UserLang")
	}
	insert := func(id string, insertFunc func(language.Tag, string)) {
		langStr := uidLang[id]
		if len(langStr) == 0 {
			insertFunc(contextLang, id)
			return
		}
		lang, err := language.Parse(langStr)
		if err != nil {
			log.Errorf("can't parse %s to language.Tag: %v", langStr, err)
			insertFunc(contextLang, id)
			return
		}
		insertFunc(lang, id)
	}
	for _, reid := range reIds {
		insert(reid, insertReid)
	}
	if contactType == npk.NotifyByWebConsole {
		for _, contact := range contacts {
			insert(contact, insertContact)
		}
	} else {
		for _, cs := range contacts {
			insertContact(contextLang, cs)
		}
	}
	return langMap, nil
}

func isEmptyRecipients(recipientId, robots []string) bool {
	var recvs []string
	for _, c := range recipientId {
		if len(c) > 0 {
			recvs = append(recvs, c)
		}
	}
	for _, robot := range robots {
		if len(robot) > 0 {
			recvs = append(recvs, robot)
		}
	}
	return len(recvs) == 0
}

func genMsgViaLang(ctx context.Context, p sNotifyParams) ([]npk.SNotifyMessage, error) {
	reIds := make([]string, 0)
	s, err := AdminSessionGenerator(context.Background(), consts.GetRegion())
	if err != nil {
		return nil, err
	}
	if p.isGroup {
		// fetch uid
		uidSet := sets.NewString()
		for _, gid := range p.recipientId {
			users, err := identity.Groups.GetUsers(s, gid, nil)
			if err != nil {
				return nil, errors.Wrapf(err, "Groups.GetUsers for group %q", gid)
			}
			for i := range users.Data {
				id, _ := users.Data[i].GetString("id")
				uidSet.Insert(id)
			}
		}
		reIds = append(reIds, uidSet.UnsortedList()...)
	} else {
		reIds = p.recipientId
	}

	if isEmptyRecipients(p.recipientId, p.robots) {
		return nil, errors.Wrap(errors.ErrEmpty, "empty receipients")
	}

	if !hasTemplateOfTopic(p.event) {
		msg := npk.SNotifyMessage{}
		msg.Uid = reIds
		msg.Priority = p.priority
		msg.Robots = p.robots
		msg.Contacts = p.contacts
		msg.ContactType = p.channel
		msg.Topic = p.event
		msg.Msg = p.data.String()
		msg.Tag = p.tag
		msg.Metadata = p.metadata
		msg.IgnoreNonexistentReceiver = p.ignoreNonexistentReceiver
		return []npk.SNotifyMessage{msg}, nil
	}

	var langMap map[language.Tag]*sTarget

	if p.channel == npk.NotifyByRobot {
		langMap, err = langRobot(ctx, p.robots)
		if err != nil {
			return nil, err
		}
	} else {
		langMap, err = lang(ctx, p.channel, reIds, p.contacts)
		if err != nil {
			return nil, err
		}
	}

	msgs := make([]npk.SNotifyMessage, 0, len(langMap))
	for lang, t := range langMap {
		langSuffix := notifyclientI18nTable.LookupByLang(lang, suffix)
		msg := npk.SNotifyMessage{}
		msg.Uid = t.reIds
		msg.Priority = p.priority
		msg.Robots = p.robots
		msg.Contacts = t.contacts
		msg.ContactType = p.channel
		topic, err := getContent(langSuffix, p.event, "title", p.channel, p.data, p.templateFuncs)
		if err != nil {
			log.Warningf("get title error: %s", err)
		}
		if len(topic) == 0 {
			topic = p.event
		}
		msg.Topic = topic
		body, err := getContent(langSuffix, p.event, "content", p.channel, p.data, p.templateFuncs)
		if err != nil {
			log.Errorf("get content error: %s", err)
		}
		if len(body) == 0 {
			body, _ = p.data.GetString()
		}
		msg.Msg = body
		msg.Tag = p.tag
		msg.Metadata = p.metadata
		msg.IgnoreNonexistentReceiver = p.ignoreNonexistentReceiver
		msgs = append(msgs, msg)
	}
	return msgs, nil
}

type sNotifyParams struct {
	recipientId               []string
	robots                    []string
	isGroup                   bool
	contacts                  []string
	channel                   npk.TNotifyChannel
	priority                  npk.TNotifyPriority
	event                     string
	data                      jsonutils.JSONObject
	createReceiver            bool
	tag                       string
	metadata                  map[string]interface{}
	ignoreNonexistentReceiver bool
	templateFuncs             template.FuncMap
}

// newSNotifyParams 创建 sNotifyParams 结构体，event 和 data 是必需参数
func newSNotifyParams(event string, data jsonutils.JSONObject) sNotifyParams {
	return sNotifyParams{
		event: event,
		data:  data,
	}
}

// withRecipientAndPriority 设置 recipientId、isGroup、priority 和 templateFuncs（用于 notifyCritical/Important/Normal）
func (p sNotifyParams) withRecipientAndPriority(recipientId []string, isGroup bool, priority npk.TNotifyPriority, templateFuncs template.FuncMap) sNotifyParams {
	p.recipientId = recipientId
	p.isGroup = isGroup
	p.priority = priority
	p.templateFuncs = templateFuncs
	return p
}

// withRecipientChannelAndPriority 设置 recipientId、isGroup、channel、priority 和 templateFuncs（用于 RawNotify）
func (p sNotifyParams) withRecipientChannelAndPriority(recipientId []string, isGroup bool, channel npk.TNotifyChannel, priority npk.TNotifyPriority, templateFuncs template.FuncMap) sNotifyParams {
	p.recipientId = recipientId
	p.isGroup = isGroup
	p.channel = channel
	p.priority = priority
	p.templateFuncs = templateFuncs
	return p
}

// withRobotChannelAndPriority 设置 robots、channel、priority 和 templateFuncs（用于 NotifyRobot）
func (p sNotifyParams) withRobotChannelAndPriority(robots []string, channel npk.TNotifyChannel, priority npk.TNotifyPriority, templateFuncs template.FuncMap) sNotifyParams {
	p.robots = robots
	p.channel = channel
	p.priority = priority
	p.templateFuncs = templateFuncs
	return p
}

// withContactChannelAndPriority 设置 contacts、channel 和 priority（用于 NotifyWithContact）
func (p sNotifyParams) withContactChannelAndPriority(contacts []string, channel npk.TNotifyChannel, priority npk.TNotifyPriority) sNotifyParams {
	p.contacts = contacts
	p.channel = channel
	p.priority = priority
	return p
}

// withRecipientId 设置 recipientId
func (p sNotifyParams) withRecipientId(recipientId []string) sNotifyParams {
	p.recipientId = recipientId
	return p
}

// withRobots 设置 robots
func (p sNotifyParams) withRobots(robots []string) sNotifyParams {
	p.robots = robots
	return p
}

// withIsGroup 设置 isGroup
func (p sNotifyParams) withIsGroup(isGroup bool) sNotifyParams {
	p.isGroup = isGroup
	return p
}

// withContacts 设置 contacts
func (p sNotifyParams) withContacts(contacts []string) sNotifyParams {
	p.contacts = contacts
	return p
}

// withChannel 设置 channel
func (p sNotifyParams) withChannel(channel npk.TNotifyChannel) sNotifyParams {
	p.channel = channel
	return p
}

// withPriority 设置 priority
func (p sNotifyParams) withPriority(priority npk.TNotifyPriority) sNotifyParams {
	p.priority = priority
	return p
}

// withCreateReceiver 设置 createReceiver
func (p sNotifyParams) withCreateReceiver(createReceiver bool) sNotifyParams {
	p.createReceiver = createReceiver
	return p
}

// withTag 设置 tag
func (p sNotifyParams) withTag(tag string) sNotifyParams {
	p.tag = tag
	return p
}

// withMetadata 设置 metadata
func (p sNotifyParams) withMetadata(metadata map[string]interface{}) sNotifyParams {
	p.metadata = metadata
	return p
}

// withIgnoreNonexistentReceiver 设置 ignoreNonexistentReceiver
func (p sNotifyParams) withIgnoreNonexistentReceiver(ignoreNonexistentReceiver bool) sNotifyParams {
	p.ignoreNonexistentReceiver = ignoreNonexistentReceiver
	return p
}

// withTemplateFuncs 设置 templateFuncs
func (p sNotifyParams) withTemplateFuncs(templateFuncs template.FuncMap) sNotifyParams {
	p.templateFuncs = templateFuncs
	return p
}

func rawNotify(ctx context.Context, p sNotifyParams) {
	intelliNotify(ctx, p)
}

func notifyCritical(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject) {
	notifyCriticalWithTemplateFuncs(ctx, recipientId, isGroup, event, data, nil)
}

func notifyCriticalWithTemplateFuncs(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) {
	p := newSNotifyParams(event, data).
		withRecipientAndPriority(recipientId, isGroup, npk.NotifyPriorityNormal, templateFuncs)
	notifyWithChannel(ctx, p,
		npk.NotifyByEmail,
		npk.NotifyByDingTalk,
		npk.NotifyByMobile,
		npk.NotifyByWebConsole,
		npk.NotifyByFeishu,
		npk.NotifyByWorkwx,
	)
}

func notifyImportant(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject) {
	notifyImportantWithTemplateFuncs(ctx, recipientId, isGroup, event, data, nil)
}

func notifyImportantWithTemplateFuncs(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) {
	p := newSNotifyParams(event, data).
		withRecipientAndPriority(recipientId, isGroup, npk.NotifyPriorityNormal, templateFuncs)
	notifyWithChannel(ctx, p,
		npk.NotifyByEmail,
		npk.NotifyByDingTalk,
		npk.NotifyByMobile,
		npk.NotifyByWebConsole,
		npk.NotifyByFeishu,
		npk.NotifyByWorkwx,
	)
}

func notifyNormal(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject) {
	notifyNormalWithTemplateFuncs(ctx, recipientId, isGroup, event, data, nil)
}

func notifyNormalWithTemplateFuncs(ctx context.Context, recipientId []string, isGroup bool, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) {
	p := newSNotifyParams(event, data).
		withRecipientAndPriority(recipientId, isGroup, npk.NotifyPriorityNormal, templateFuncs)
	notifyWithChannel(ctx, p,
		npk.NotifyByEmail,
		npk.NotifyByDingTalk,
		npk.NotifyByFeishu,
		npk.NotifyByWorkwx,
		npk.NotifyByWebConsole,
	)
}

func notifyWithChannel(ctx context.Context, p sNotifyParams, channels ...npk.TNotifyChannel) {
	for _, c := range channels {
		np := p
		np.channel = c
		rawNotify(ctx, np)
	}
}

const noSuchReceiver = `no such receiver whose uid is '(.*)'`

var noSuchReceiverRegexp = regexp.MustCompile(noSuchReceiver)

type notifyTask struct {
	ctx            context.Context
	msg            npk.SNotifyMessage
	createReceiver bool
}

func (t *notifyTask) Dump() string {
	return fmt.Sprintf("msg: %v createReceiver: %v", t.msg, t.createReceiver)
}

func (t *notifyTask) Run() {
	s, err := AdminSessionGenerator(t.ctx, consts.GetRegion())
	if err != nil {
		log.Errorf("fail to get session: %v", err)
	}
	for {
		err := npk.Notifications.Send(s, t.msg)
		if err == nil {
			break
		}
		if !t.createReceiver {
			log.Errorf("unable to send notification to contacts %s uid %s gid %s @%s: %s", strings.Join(t.msg.Contacts, ","), strings.Join(t.msg.Uid, ","), strings.Join(t.msg.Gid, ","), t.msg.ContactType, jsonutils.Marshal(err))
			break
		}
		jerr, ok := err.(*httputils.JSONClientError)
		if !ok {
			log.Errorf("unable to send notification: %v", err)
			break
		}
		if jerr.Code > 500 {
			log.Errorf("unable to send notification: %v", err)
			break
		}
		match := noSuchReceiverRegexp.FindStringSubmatch(jerr.Details)
		if match == nil || len(match) <= 1 {
			log.Errorf("unable to send notification: %v", err)
			break
		}
		receiverId := match[1]
		createData := jsonutils.NewDict()
		createData.Set("uid", jsonutils.NewString(receiverId))
		_, err = npk.NotifyReceiver.Create(s, createData)
		if err != nil {
			log.Errorf("try to create receiver %q, but failed: %v", receiverId, err)
			break
		}
		log.Infof("create receiver %q successfully", receiverId)
	}
}

func intelliNotify(ctx context.Context, p sNotifyParams) {
	if isEmptyRecipients(p.recipientId, p.robots) {
		return
	}
	log.Infof("recipientId: %v, robots: %v, contacts: %v, event %s priority %s", p.recipientId, p.robots, p.contacts, p.event, p.priority)
	msgs, err := genMsgViaLang(ctx, p)
	if err != nil {
		log.Errorf("unable send notification: %v", err)
	}
	for i := range msgs {
		t := notifyTask{
			ctx:            ctx,
			createReceiver: p.createReceiver,
			msg:            msgs[i],
		}
		notifyClientWorkerMan.Run(&t, nil, nil)
	}
	// log.Debugf("send notification %s %s", topic, body)
}

func notify(ctx context.Context, recipientId []string, isGroup bool, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject) {
	notifyWithTemplateFuncs(ctx, recipientId, isGroup, priority, event, data, nil)
}

func notifyWithTemplateFuncs(ctx context.Context, recipientId []string, isGroup bool, priority npk.TNotifyPriority, event string, data jsonutils.JSONObject, templateFuncs template.FuncMap) {
	switch priority {
	case npk.NotifyPriorityCritical:
		notifyCriticalWithTemplateFuncs(ctx, recipientId, isGroup, event, data, templateFuncs)
	case npk.NotifyPriorityImportant:
		notifyImportantWithTemplateFuncs(ctx, recipientId, isGroup, event, data, templateFuncs)
	default:
		notifyNormalWithTemplateFuncs(ctx, recipientId, isGroup, event, data, templateFuncs)
	}
}

func parseIdName(idName string) (string, string) {
	pos := strings.Index(idName, "\\")
	if pos > 0 {
		return idName[:pos], idName[pos+1:]
	} else {
		return "", idName
	}
}

var (
	domainCache = make(map[string]string)
)

func getIdentityId(s *mcclient.ClientSession, idName string, manager modulebase.Manager) (string, error) {
	domain, idName := parseIdName(idName)

	query := jsonutils.NewDict()
	if len(domain) > 0 {
		domainId, ok := domainCache[domain]
		if !ok {
			var err error
			domainId, err = identity.Domains.GetId(s, domain, nil)
			if err != nil {
				log.Errorf("fail to find domainId for domain %s: %s", domain, err)
				return "", err
			}
			domainCache[domain] = domainId
		}
		query.Add(jsonutils.NewString(domainId), "domain_id")
	}
	return manager.GetId(s, idName, query)
}
