package activity

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"time"

	"github.com/qor5/admin/presets"
	vuetify "github.com/qor5/ui/vuetify"
	"github.com/qor5/web"
	"github.com/qor5/x/i18n"
	h "github.com/theplant/htmlgo"
	"gorm.io/gorm"
)

// @snippet_begin(ActivityModelBuilder)
// a unique model builder is consist of typ and presetModel
type ModelBuilder struct {
	typ      reflect.Type     // model type
	activity *ActivityBuilder // activity builder

	presetModel *presets.ModelBuilder // preset model builder
	skip        uint8                 // skip the prefined data operator of the presetModel

	keys          []string                     // primary keys
	ignoredFields []string                     // ignored fields
	typeHanders   map[reflect.Type]TypeHandler // type handlers
	link          func(interface{}) string     // display the model link on the admin detail page
}

// @snippet_end

// GetType get ModelBuilder type
func (mb *ModelBuilder) GetType() reflect.Type {
	return mb.typ
}

// AddKeys add keys to the model builder
func (mb *ModelBuilder) AddKeys(keys ...string) *ModelBuilder {
	for _, key := range keys {
		var find bool
		for _, mkey := range mb.keys {
			if mkey == key {
				find = true
				break
			}
		}
		if !find {
			mb.keys = append(mb.keys, key)
		}
	}
	return mb
}

// SetKeys set keys for the model builder
func (mb *ModelBuilder) SetKeys(keys ...string) *ModelBuilder {
	mb.keys = keys
	return mb
}

// SetLink set the link that linked to the modified record
func (mb *ModelBuilder) SetLink(f func(interface{}) string) *ModelBuilder {
	mb.link = f
	return mb
}

// SkipCreate skip the create action for preset.ModelBuilder
func (mb *ModelBuilder) SkipCreate() *ModelBuilder {
	if mb.presetModel == nil {
		return mb
	}

	if mb.skip&Create == 0 {
		mb.skip |= Create
	}
	return mb
}

// SkipUpdate skip the update action for preset.ModelBuilder
func (mb *ModelBuilder) SkipUpdate() *ModelBuilder {
	if mb.presetModel == nil {
		return mb
	}

	if mb.skip&Update == 0 {
		mb.skip |= Update
	}
	return mb
}

// SkipDelete skip the delete action for preset.ModelBuilder
func (mb *ModelBuilder) SkipDelete() *ModelBuilder {
	if mb.presetModel == nil {
		return mb
	}

	if mb.skip&Delete == 0 {
		mb.skip |= Delete
	}
	return mb
}


// EnableActivityInfoTab enable activity info tab on the given model's editing page
func (mb *ModelBuilder) EnableActivityInfoTab() *ModelBuilder {
	if mb.presetModel == nil {
		return mb
	}

	editing := mb.presetModel.Editing()
	editing.AppendTabsPanelFunc(func(obj interface{}, ctx *web.EventContext) (c h.HTMLComponent) {
		logs := mb.activity.GetCustomizeActivityLogs(obj, mb.activity.getDBFromContext(ctx.R.Context()))
		msgr := i18n.MustGetModuleMessages(ctx.R, I18nActivityKey, Messages_en_US).(*Messages)

		logsvalues := reflect.Indirect(reflect.ValueOf(logs))
		var panels []h.HTMLComponent

		for i := 0; i < logsvalues.Len(); i++ {
			log := logsvalues.Index(i).Interface().(ActivityLogInterface)
			var headerText string
			if mb.activity.tabHeading != nil {
				headerText = mb.activity.tabHeading(log)
			} else {
				headerText = fmt.Sprintf("%s %s at %s", log.GetCreator(), strings.ToLower(log.GetAction()), log.GetCreatedAt().Format("2006-01-02 15:04:05 MST"))
			}

			panels = append(panels, vuetify.VExpansionPanel(
				vuetify.VExpansionPanelHeader(h.Span(headerText)),
				vuetify.VExpansionPanelContent(DiffComponent(log.GetModelDiffs(), ctx.R)),
			))
		}

		return h.Components(
			vuetify.VTab(h.Text(msgr.Activities)),
			vuetify.VTabItem(
				vuetify.VExpansionPanels(panels...).Attr("style", "padding:10px;"),
			),
		)
	})

	return mb
}

// AddIgnoredFields append ignored fields to the default ignored fields, this would not overwrite the default ignored fields
func (mb *ModelBuilder) AddIgnoredFields(fields ...string) *ModelBuilder {
	mb.ignoredFields = append(mb.ignoredFields, fields...)
	return mb
}

// SetIgnoredFields set ignored fields to replace the default ignored fields with the new set.
func (mb *ModelBuilder) SetIgnoredFields(fields ...string) *ModelBuilder {
	mb.ignoredFields = fields
	return mb
}

// AddTypeHanders add type handers for the model builder
func (mb *ModelBuilder) AddTypeHanders(v interface{}, f TypeHandler) *ModelBuilder {
	if mb.typeHanders == nil {
		mb.typeHanders = map[reflect.Type]TypeHandler{}
	}
	mb.typeHanders[reflect.Indirect(reflect.ValueOf(v)).Type()] = f
	return mb
}

// KeysValue get model keys value
func (mb *ModelBuilder) KeysValue(v interface{}) string {
	var (
		stringBuilder = strings.Builder{}
		reflectValue  = reflect.Indirect(reflect.ValueOf(v))
		reflectType   = reflectValue.Type()
	)

	for _, key := range mb.keys {
		if fields, ok := reflectType.FieldByName(key); ok {
			if reflectValue.FieldByName(key).IsZero() {
				continue
			}
			if fields.Anonymous {
				stringBuilder.WriteString(fmt.Sprintf("%v:", reflectValue.FieldByName(key).FieldByName(key).Interface()))
			} else {
				stringBuilder.WriteString(fmt.Sprintf("%v:", reflectValue.FieldByName(key).Interface()))
			}
		}
	}

	return strings.TrimRight(stringBuilder.String(), ":")
}

// AddRecords add records log
func (mb *ModelBuilder) AddRecords(action string, ctx context.Context, vs ...interface{}) error {
	if len(vs) == 0 {
		return errors.New("data are empty")
	}

	var (
		creator = mb.activity.getCreatorFromContext(ctx)
		db      = mb.activity.getDBFromContext(ctx)
	)

	switch action {
	case ActivityView:
		for _, v := range vs {
			err := mb.AddViewRecord(creator, v, db)
			if err != nil {
				return err
			}
		}

	case ActivityDelete:
		for _, v := range vs {
			err := mb.AddDeleteRecord(creator, v, db)
			if err != nil {
				return err
			}
		}
	case ActivityCreate:
		for _, v := range vs {
			err := mb.AddCreateRecord(creator, v, db)
			if err != nil {
				return err
			}
		}
	case ActivityEdit:
		for _, v := range vs {
			err := mb.AddEditRecord(creator, v, db)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// AddCustomizedRecord add customized record
func (mb *ModelBuilder) AddCustomizedRecord(action string, diff bool, ctx context.Context, obj interface{}) error {
	var (
		creator = mb.activity.getCreatorFromContext(ctx)
		db      = mb.activity.getDBFromContext(ctx)
	)

	if !diff {
		return mb.save(creator, action, obj, db, "")
	}

	old, ok := findOld(obj, db)
	if !ok {
		return fmt.Errorf("can't find old data for %+v ", obj)
	}
	return mb.addDiff(action, creator, old, obj, db)
}

// AddViewRecord add view record
func (mb *ModelBuilder) AddViewRecord(creator interface{}, v interface{}, db *gorm.DB) error {
	return mb.save(creator, ActivityView, v, db, "")
}

// AddDeleteRecord	add delete record
func (mb *ModelBuilder) AddDeleteRecord(creator interface{}, v interface{}, db *gorm.DB) error {
	return mb.save(creator, ActivityDelete, v, db, "")
}

// AddSaverRecord will save a create log or a edit log
func (mb *ModelBuilder) AddSaveRecord(creator interface{}, now interface{}, db *gorm.DB) error {
	old, ok := findOld(now, db)
	if !ok {
		return mb.AddCreateRecord(creator, now, db)
	}
	return mb.AddEditRecordWithOld(creator, old, now, db)
}

// AddCreateRecord add create record
func (mb *ModelBuilder) AddCreateRecord(creator interface{}, v interface{}, db *gorm.DB) error {
	return mb.save(creator, ActivityCreate, v, db, "")
}

// AddEditRecord add edit record
func (mb *ModelBuilder) AddEditRecord(creator interface{}, now interface{}, db *gorm.DB) error {
	old, ok := findOld(now, db)
	if !ok {
		return fmt.Errorf("can't find old data for %+v ", now)
	}
	return mb.AddEditRecordWithOld(creator, old, now, db)
}

// AddEditRecord add edit record
func (mb *ModelBuilder) AddEditRecordWithOld(creator interface{}, old, now interface{}, db *gorm.DB) error {
	return mb.addDiff(ActivityEdit, creator, old, now, db)
}

func (mb *ModelBuilder) addDiff(action string, creator, old, now interface{}, db *gorm.DB) error {
	diffs, err := mb.Diff(old, now)
	if err != nil {
		return err
	}

	if len(diffs) == 0 {
		return nil
	}

	b, err := json.Marshal(diffs)
	if err != nil {
		return err
	}

	return mb.save(creator, ActivityEdit, now, db, string(b))
}

// Diff get diffs between old and now value
func (mb *ModelBuilder) Diff(old, now interface{}) ([]Diff, error) {
	return NewDiffBuilder(mb).Diff(old, now)
}

// save log into db
func (mb *ModelBuilder) save(creator interface{}, action string, v interface{}, db *gorm.DB, diffs string) error {
	var m = mb.activity.NewLogModelData()
	log, ok := m.(ActivityLogInterface)
	if !ok {
		return fmt.Errorf("model %T is not implement ActivityLogInterface", m)
	}

	log.SetCreatedAt(time.Now())
	switch user := creator.(type) {
	case string:
		log.SetCreator(user)
	case CreatorInterface:
		log.SetCreator(user.GetName())
		log.SetUserID(user.GetID())
	default:
		log.SetCreator("unknown")
	}

	log.SetAction(action)
	log.SetModelName(mb.typ.Name())
	log.SetModelKeys(mb.KeysValue(v))

	if mb.presetModel != nil && mb.presetModel.Info().URIName() != "" {
		log.SetModelLabel(mb.presetModel.Info().URIName())
	} else {
		log.SetModelLabel("-")
	}

	if f := mb.link; f != nil {
		log.SetModelLink(f(v))
	}

	if diffs == "" && action == ActivityEdit {
		return nil
	}

	if action == ActivityEdit {
		log.SetModelDiffs(diffs)
	}

	if db.Save(log).Error != nil {
		return db.Error
	}
	return nil
}