package impl

import (
	"context"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/go-sonic/sonic/config"
	"github.com/go-sonic/sonic/dal"
	"github.com/go-sonic/sonic/event"
	"github.com/go-sonic/sonic/model/dto"
	"github.com/go-sonic/sonic/model/entity"
	"github.com/go-sonic/sonic/model/property"
	"github.com/go-sonic/sonic/service"
	"github.com/go-sonic/sonic/service/theme"
	"github.com/go-sonic/sonic/util"
	"github.com/go-sonic/sonic/util/xerr"
)

type themeServiceImpl struct {
	OptionService            service.OptionService
	Config                   *config.Config
	Event                    event.Bus
	PropertyScanner          theme.PropertyScanner
	FileScanner              theme.FileScanner
	MultipartZipThemeFetcher theme.MultipartZipThemeFetcher
}

func NewThemeService(optionService service.OptionService, config *config.Config, event event.Bus, propertyScanner theme.PropertyScanner, fileScanner theme.FileScanner, multipartZipThemeFetcher theme.MultipartZipThemeFetcher) service.ThemeService {
	return &themeServiceImpl{
		OptionService:            optionService,
		Config:                   config,
		Event:                    event,
		PropertyScanner:          propertyScanner,
		FileScanner:              fileScanner,
		MultipartZipThemeFetcher: multipartZipThemeFetcher,
	}
}

func (t *themeServiceImpl) GetActivateTheme(ctx context.Context) (*dto.ThemeProperty, error) {
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return nil, err
	}
	return t.GetThemeByID(ctx, activatedThemeID)
}

func (t *themeServiceImpl) GetThemeByID(ctx context.Context, themeID string) (*dto.ThemeProperty, error) {
	themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
	if err != nil {
		return nil, err
	}
	if themeProperty == nil {
		return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
	}
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return nil, err
	}
	if themeProperty.ID == activatedThemeID {
		themeProperty.Activated = true
	}
	return themeProperty, nil
}

func (t *themeServiceImpl) ListAllTheme(ctx context.Context) ([]*dto.ThemeProperty, error) {
	themes, err := t.PropertyScanner.ListAll(ctx, t.Config.Sonic.ThemeDir)
	if err != nil {
		return nil, err
	}
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return nil, err
	}

	for _, t := range themes {
		if t.ID == activatedThemeID {
			t.Activated = true
		}
	}
	return themes, nil
}

func (t *themeServiceImpl) ListThemeFiles(ctx context.Context, themeID string) ([]*dto.ThemeFile, error) {
	themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
	if err != nil {
		return nil, err
	}
	if themeProperty == nil {
		return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
	}
	return t.FileScanner.ListThemeFiles(ctx, themeProperty.ThemePath)
}

func (t *themeServiceImpl) GetThemeFileContent(ctx context.Context, themeID, absPath string) (string, error) {
	themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
	if err != nil {
		return "", err
	}
	if themeProperty == nil {
		return "", xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
	}

	if err = t.checkPathValid(themeProperty.ThemePath, absPath); err != nil {
		return "", err
	}

	return t.ReadThemeFile(ctx, absPath)
}

func (t *themeServiceImpl) ReadThemeFile(ctx context.Context, absPath string) (content string, err error) {
	file, err := os.Open(absPath)
	defer func() {
		cerr := file.Close()
		if err == nil && cerr != nil {
			err = xerr.WithStatus(cerr, xerr.StatusInternalServerError).WithMsg("close file err")
		}
	}()

	if os.IsNotExist(err) {
		return "", xerr.WithMsg(err, "file not exist").WithStatus(xerr.StatusBadRequest)
	} else if os.IsPermission(err) {
		return "", xerr.WithMsg(err, "file permission err").WithStatus(xerr.StatusForbidden)
	}

	contentBytes, err := io.ReadAll(file)
	if err != nil {
		err = xerr.WithMsg(err, "read file error").WithStatus(xerr.StatusInternalServerError)
		return
	}
	content = util.BytesToString(contentBytes)
	return
}

func (t *themeServiceImpl) UpdateThemeFile(ctx context.Context, themeID, absPath, content string) error {
	themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
	if err != nil {
		return err
	}
	if themeProperty == nil {
		return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg(themeID + " not exist")
	}

	if err = t.checkPathValid(themeProperty.ThemePath, absPath); err != nil {
		return err
	}
	file, err := os.OpenFile(absPath, os.O_WRONLY, 0)
	if err != nil {
		return xerr.WithMsg(err, "open file error")
	}
	defer file.Close()
	_, err = file.WriteString(content)
	if err != nil {
		return xerr.WithMsg(err, "write to file err")
	}
	return nil
}

func (t *themeServiceImpl) checkPathValid(themePath, absPath string) error {
	absPath = filepath.Clean(absPath)
	if !filepath.IsAbs(absPath) {
		return xerr.WithMsg(nil, "path error").WithStatus(xerr.StatusForbidden)
	}
	if !strings.HasPrefix(absPath, themePath) {
		return xerr.WithMsg(nil, "path error").WithStatus(xerr.StatusForbidden)
	}
	return nil
}

func (t *themeServiceImpl) ListCustomTemplates(ctx context.Context, themeID, prefix string) ([]string, error) {
	themeProperty, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeID)
	if err != nil {
		return nil, err
	}
	if themeProperty == nil {
		return nil, xerr.WithMsg(nil, "theme does not exist").WithStatus(xerr.StatusInternalServerError)
	}
	files, err := os.ReadDir(themeProperty.ThemePath)
	if err != nil {
		return nil, xerr.WithMsg(err, "read theme files error").WithStatus(xerr.StatusInternalServerError)
	}
	result := make([]string, 0)
	for _, file := range files {
		if file.IsDir() {
			continue
		}
		fileName := file.Name()
		if !strings.HasPrefix(fileName, prefix) {
			continue
		}
		customName := strings.TrimPrefix(fileName, prefix)
		customName = strings.TrimSuffix(customName, ".ftl")
		result = append(result, customName)
	}
	return result, nil
}

func (t *themeServiceImpl) ActivateTheme(ctx context.Context, themeID string) (*dto.ThemeProperty, error) {
	err := t.OptionService.Save(ctx, map[string]string{property.Theme.KeyValue: themeID})
	if err != nil {
		return nil, err
	}
	t.Event.Publish(ctx, &event.ThemeActivatedEvent{})
	return t.GetThemeByID(ctx, themeID)
}

func (t *themeServiceImpl) GetThemeConfig(ctx context.Context, themeID string) ([]*dto.ThemeConfigGroup, error) {
	themeProperty, err := t.GetThemeByID(ctx, themeID)
	if err != nil {
		return nil, err
	}
	return t.PropertyScanner.ReadThemeConfig(ctx, themeProperty.ThemePath)
}

func (t *themeServiceImpl) GetThemeSettingMap(ctx context.Context, themeID string) (map[string]interface{}, error) {
	itemMap, err := t.getThemeConfigItemMap(ctx, themeID)
	if err != nil {
		return nil, err
	}
	return t.getThemeSettingMapByItemMap(ctx, themeID, itemMap)
}

func (t *themeServiceImpl) GetThemeGroupSettingMap(ctx context.Context, themeID, group string) (map[string]interface{}, error) {
	themeConfig, err := t.GetThemeConfig(ctx, themeID)
	if err != nil {
		return nil, err
	}

	var groupConfig *dto.ThemeConfigGroup
	for _, themeGroup := range themeConfig {
		if themeGroup.Name == group {
			groupConfig = themeGroup
			break
		}
	}
	if groupConfig == nil {
		return nil, nil
	}
	itemMap := make(map[string]*dto.ThemeConfigItem)
	for _, item := range groupConfig.Items {
		itemMap[item.Name] = item
	}
	return t.getThemeSettingMapByItemMap(ctx, themeID, itemMap)
}

func (t *themeServiceImpl) getThemeSettingMapByItemMap(ctx context.Context, themeID string, itemMap map[string]*dto.ThemeConfigItem) (map[string]interface{}, error) {
	themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
	themeSettings, err := themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Find()
	if err != nil {
		return nil, WrapDBErr(err)
	}
	result := make(map[string]interface{})
	for _, themeSetting := range themeSettings {
		item, ok := itemMap[themeSetting.SettingKey]
		if !ok {
			continue
		}
		value, err := item.DataType.Convert(themeSetting.SettingValue)
		if err != nil {
			return nil, xerr.WithStatus(err, xerr.StatusInternalServerError)
		}
		result[themeSetting.SettingKey] = value
	}

	for _, item := range itemMap {
		if _, ok := result[item.Name]; ok || item.DefaultValue == nil {
			continue
		}
		result[item.Name] = item.DefaultValue
	}
	return result, nil
}

func (t *themeServiceImpl) SaveThemeSettings(ctx context.Context, themeID string, settings map[string]interface{}) error {
	if len(settings) == 0 {
		return nil
	}

	itemMap, err := t.getThemeConfigItemMap(ctx, themeID)
	if err != nil {
		return err
	}

	themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
	allThemeSetting, err := themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Find()
	if err != nil {
		return WrapDBErr(err)
	}
	allThemeSettingMap := make(map[string]*entity.ThemeSetting, len(allThemeSetting))
	for _, themeSetting := range allThemeSetting {
		allThemeSettingMap[themeSetting.SettingKey] = themeSetting
	}

	toDeleteIDs := make([]int32, 0)
	toCreate := make([]*entity.ThemeSetting, 0)
	toUpdate := make([]*entity.ThemeSetting, 0)
	now := time.Now()

	for name, value := range settings {
		item, ok := itemMap[name]
		if !ok {
			return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("setting name invalid: " + name)
		}
		if themeSetting, ok := allThemeSettingMap[name]; ok {
			if value == "" {
				toDeleteIDs = append(toDeleteIDs, themeSetting.ID)
			} else {
				valueStr, err := item.DataType.FormatToStr(value)
				if err != nil {
					return xerr.WithErrMsgf(err, "value=%s type invalid ,setting name=%s", value, name).WithStatus(xerr.StatusBadRequest)
				}

				if value != themeSetting.SettingValue {
					themeSetting.SettingValue = valueStr
					themeSetting.UpdateTime = util.TimePtr(now)
					toUpdate = append(toUpdate, themeSetting)
				}
			}
		} else {
			if value != "" {
				valueStr, err := item.DataType.FormatToStr(value)
				if err != nil {
					return xerr.WithErrMsgf(err, "value=%s type invalid ,setting name=%s", value, name).WithStatus(xerr.StatusBadRequest)
				}
				toCreate = append(toCreate, &entity.ThemeSetting{SettingKey: name, SettingValue: valueStr, ThemeID: themeID})
			}
		}
	}
	err = dal.Transaction(ctx, func(txCtx context.Context) error {
		themeSettingDAL := dal.GetQueryByCtx(txCtx).ThemeSetting
		_, err := themeSettingDAL.WithContext(txCtx).Where(themeSettingDAL.ID.In(toDeleteIDs...)).Delete()
		if err != nil {
			return WrapDBErr(err)
		}
		err = themeSettingDAL.WithContext(txCtx).Save(toUpdate...)
		if err != nil {
			return WrapDBErr(err)
		}
		err = themeSettingDAL.WithContext(txCtx).Create(toCreate...)
		return WrapDBErr(err)
	})
	if err != nil {
		return err
	}
	t.Event.Publish(ctx, &event.ThemeUpdateEvent{})
	return nil
}

func (t *themeServiceImpl) DeleteThemeSettings(ctx context.Context, themeID string) error {
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return err
	}
	if activatedThemeID == themeID {
		return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("can not delete the theme being used")
	}

	themeSettingDAL := dal.GetQueryByCtx(ctx).ThemeSetting
	_, err = themeSettingDAL.WithContext(ctx).Where(themeSettingDAL.ThemeID.Eq(themeID)).Delete()
	return WrapDBErr(err)
}

func (t *themeServiceImpl) DeleteTheme(ctx context.Context, themeID string, deleteSettings bool) error {
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return err
	}
	if activatedThemeID == themeID {
		return xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("can not delete the theme being used")
	}
	if deleteSettings {
		err = t.DeleteThemeSettings(ctx, themeID)
		if err != nil {
			return err
		}
	}
	themeProperty, err := t.GetThemeByID(ctx, themeID)
	if err != nil {
		return err
	}
	err = os.RemoveAll(themeProperty.ThemePath)
	if err != nil {
		return xerr.WithStatus(err, xerr.StatusInternalServerError).WithMsg("delete theme directory err")
	}
	return nil
}

func (t *themeServiceImpl) UploadTheme(ctx context.Context, file *multipart.FileHeader) (*dto.ThemeProperty, error) {
	themeProperty, err := t.MultipartZipThemeFetcher.FetchTheme(ctx, file)
	if err != nil {
		return nil, err
	}
	return t.addTheme(ctx, themeProperty)
}

func (t *themeServiceImpl) UpdateThemeByUpload(ctx context.Context, themeID string, file *multipart.FileHeader) (*dto.ThemeProperty, error) {
	oldThemeProperty, err := t.GetThemeByID(ctx, themeID)
	if err != nil {
		return nil, err
	}
	newThemeProperty, err := t.MultipartZipThemeFetcher.FetchTheme(ctx, file)
	if err != nil {
		return nil, err
	}
	err = os.RemoveAll(oldThemeProperty.ThemePath)
	if err != nil {
		return nil, xerr.WithMsg(err, "delete old theme err").WithStatus(xerr.StatusInternalServerError)
	}
	return t.addTheme(ctx, newThemeProperty)
}

func (t *themeServiceImpl) ReloadTheme(ctx context.Context) error {
	t.Event.Publish(ctx, &event.ThemeUpdateEvent{})
	t.Event.Publish(ctx, &event.ThemeFileUpdatedEvent{})
	return nil
}

func (t *themeServiceImpl) TemplateExist(ctx context.Context, template string) (bool, error) {
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return false, err
	}
	themeProperty, err := t.GetThemeByID(ctx, activatedThemeID)
	if err != nil {
		return false, err
	}
	return util.FileIsExisted(filepath.Join(themeProperty.ThemePath, template)), nil
}

func (t *themeServiceImpl) getThemeConfigItemMap(ctx context.Context, themeID string) (map[string]*dto.ThemeConfigItem, error) {
	themeConfigGroup, err := t.GetThemeConfig(ctx, themeID)
	if err != nil {
		return nil, err
	}

	itemMap := make(map[string]*dto.ThemeConfigItem)
	for _, themeConfig := range themeConfigGroup {
		for _, item := range themeConfig.Items {
			itemMap[item.Name] = item
		}
	}
	return itemMap, nil
}

func (t *themeServiceImpl) addTheme(ctx context.Context, themeProperty *dto.ThemeProperty) (*dto.ThemeProperty, error) {
	existTheme, err := t.PropertyScanner.GetThemeByThemeID(ctx, themeProperty.ID)
	if err != nil {
		return nil, err
	}
	if existTheme != nil {
		return nil, xerr.WithStatus(nil, xerr.StatusBadRequest).WithMsg("theme already exist")
	}
	err = util.CopyDir(themeProperty.ThemePath, filepath.Join(t.Config.Sonic.ThemeDir, themeProperty.FolderName))
	if err != nil {
		return nil, xerr.WithStatus(err, xerr.StatusBadRequest)
	}
	return t.PropertyScanner.ReadThemeProperty(ctx, filepath.Join(t.Config.Sonic.ThemeDir, themeProperty.FolderName))
}

func (t *themeServiceImpl) Render(ctx context.Context, name string) (string, error) {
	activatedThemeID, err := t.OptionService.GetActivatedThemeID(ctx)
	if err != nil {
		return "", err
	}
	return activatedThemeID + "/" + name, nil
}