package admin

import (
	"context"
	"errors"
	"strconv"

	"git.dmitriygnatenko.ru/dima/go-common/db"
	"git.dmitriygnatenko.ru/dima/go-common/logger"
	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/gofiber/fiber/v2"

	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/dto"
	customErrors "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helper/errors"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helper/request"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/model"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/service/handler"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/service/i18n"
)

type editArticlePage struct {
	tm                   TransactionManager
	articleRepository    ArticleRepository
	tagRepository        TagRepository
	articleTagRepository ArticleTagRepository
	languageRepository   LanguageRepository
	cacheService         CacheService
}

func NewEditArticlePageHandler(
	tm TransactionManager,
	articleRepository ArticleRepository,
	tagRepository TagRepository,
	articleTagRepository ArticleTagRepository,
	languageRepository LanguageRepository,
	cacheService CacheService,
) fiber.Handler {
	h := editArticlePage{
		tm:                   tm,
		articleRepository:    articleRepository,
		tagRepository:        tagRepository,
		articleTagRepository: articleTagRepository,
		languageRepository:   languageRepository,
		cacheService:         cacheService,
	}

	return h.handler()
}

func (h editArticlePage) handler() fiber.Handler {
	return func(fctx *fiber.Ctx) error {
		ctx := fctx.Context()
		lang := i18n.LanguageFromContext(fctx)
		validationErrors := make(map[string]string)

		articleID, err := request.ConvertToUint64(fctx, "id")
		if err != nil {
			logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get id from request").Error())
			return err
		}

		tags, err := h.getTags(ctx)
		if err != nil {
			logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get tags").Error())
			return err
		}

		languages, err := h.getLanguages(ctx)
		if err != nil {
			logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get languages").Error())
			return err
		}

		article, err := h.getArticle(ctx, articleID)
		if err != nil {
			return err // logging in the getArticle method
		}

		articleTags, err := h.tagRepository.GetByArticleID(ctx, articleID)
		if err != nil {
			logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get article tags").Error())
			return err
		}

		var form dto.ArticleForm

		if fctx.Method() == fiber.MethodGet {
			form = mapper.ToArticleForm(*article, articleTags)
		} else if fctx.Method() == fiber.MethodPost {
			form = dto.ArticleForm{
				ID:         articleID,
				ActiveTags: make(map[uint64]bool),
			}

			if err = fctx.BodyParser(&form); err != nil {
				logger.Info(ctx, customErrors.Wrap(err, "edit article page: can't parse body").Error())
				return err
			}

			if err = h.validateArticleForm(ctx, form, *article); err != nil {
				logger.Info(ctx, customErrors.Wrap(err, "edit article page: validation").Error())
				validationErrors = mapper.ToErrorsMap(err)
			}

			tagIDs, err := h.formatTags(form.Tags)
			if err != nil {
				logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't format tags").Error())
				return err
			}

			for i := range tagIDs {
				form.ActiveTags[tagIDs[i]] = true
			}

			if len(validationErrors) == 0 {
				articleModel, err := mapper.ToArticleModel(form)
				if err != nil {
					logger.Error(
						ctx,
						customErrors.Wrap(err, "edit article page: can't create article model").Error(),
					)

					return err
				}

				var tagsToAdd, tagsToDelete []uint64

				oldTagsMap := make(map[int]struct{}, len(articleTags))
				for i := range articleTags {
					oldTagsMap[int(articleTags[i].ID)] = struct{}{}
					if _, ok := form.ActiveTags[articleTags[i].ID]; !ok {
						tagsToDelete = append(tagsToDelete, articleTags[i].ID)
					}
				}

				for i := range tagIDs {
					if _, ok := oldTagsMap[int(tagIDs[i])]; !ok {
						tagsToAdd = append(tagsToAdd, tagIDs[i])
					}
				}

				err = h.tm.ReadCommitted(ctx, func(ctx context.Context) error {
					txErr := h.articleRepository.Update(ctx, *articleModel)
					if txErr != nil {
						return customErrors.Wrap(txErr, "can't update article")
					}

					if len(tagsToAdd) > 0 {
						if txErr = h.articleTagRepository.Add(ctx, articleID, tagsToAdd); txErr != nil {
							return customErrors.Wrap(txErr, "can't add tags")
						}
					}

					if len(tagsToDelete) > 0 {
						if txErr = h.articleTagRepository.Delete(ctx, articleID, tagsToDelete); txErr != nil {
							return customErrors.Wrap(txErr, "can't delete tags")
						}
					}

					return nil
				})

				if err != nil {
					logger.Error(ctx, customErrors.Wrap(err, "edit article page").Error())
					return err
				}

				h.cacheService.Delete(handler.ArticleCacheKey + form.URL + form.Language)
				h.cacheService.Delete(handler.RecentArticlesCacheKey + form.Language)
				h.cacheService.Delete(handler.UsedTagsCacheKey + form.Language)
				h.cacheService.Delete(handler.AllPreviewArticlesCacheKey + form.Language)

				if fctx.FormValue("action", "save") == "save" {
					return fctx.Redirect("/admin")
				}
			}
		}

		return fctx.Render("admin/article_edit", fiber.Map{
			"lang":       lang,
			"title":      i18n.T(lang, "admin_article_edit_title"),
			"section":    "article",
			"show_apply": true,
			"form":       form,
			"errors":     validationErrors,
			"tags":       tags,
			"languages":  languages,
		}, "admin/_layout")
	}
}

func (h editArticlePage) getArticle(ctx context.Context, id uint64) (*model.Article, error) {
	article, err := h.articleRepository.GetByID(ctx, id)
	if err != nil {
		if db.IsNotFoundError(err) {
			return nil, fiber.ErrNotFound
		}

		logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get article").Error())
		return nil, err
	}

	return article, nil
}

func (h editArticlePage) getTags(ctx context.Context) ([]dto.Tag, error) {
	tagModels, err := h.tagRepository.GetAll(ctx)
	if err != nil {
		return nil, err
	}

	return mapper.ToTagsList(tagModels), nil
}

func (h editArticlePage) getLanguages(ctx context.Context) ([]string, error) {
	langModels, err := h.languageRepository.GetAll(ctx)
	if err != nil {
		return nil, err
	}

	res := make([]string, 0, len(langModels))
	for i := range langModels {
		res = append(res, langModels[i].URL)
	}

	return res, nil
}

func (h editArticlePage) formatTags(tags []string) ([]uint64, error) {
	ids := make([]uint64, 0, len(tags))

	for i := range tags {
		id, err := strconv.ParseUint(tags[i], 10, 64)
		if err != nil {
			return nil, err
		}

		ids = append(ids, id)
	}

	return ids, nil
}

func (h editArticlePage) validateArticleForm(
	ctx context.Context,
	req dto.ArticleForm,
	article model.Article,
) error {
	lang := i18n.DefaultLanguage()
	requiredMsg := i18n.T(lang, "admin_err_required")
	lengthMsg := i18n.T(lang, "admin_err_length_255")

	return validation.ValidateStructWithContext(
		ctx,
		&req,
		validation.Field(
			&req.Title,
			validation.Required.Error(requiredMsg),
			validation.Length(1, 255).Error(lengthMsg),
		),
		validation.Field(
			&req.Image,
			validation.Required.Error(requiredMsg),
			validation.Length(1, 255).Error(lengthMsg),
		),
		validation.Field(
			&req.URL,
			validation.Required.Error(requiredMsg),
			validation.Length(1, 255).Error(lengthMsg),
			validation.By(h.validateDuplicateURL(article)),
		),
		validation.Field(
			&req.Text,
			validation.Required.Error(requiredMsg),
		),
		validation.Field(
			&req.MetaKeywords,
			validation.Length(1, 255).Error(lengthMsg),
		),
		validation.Field(
			&req.MetaDescription,
			validation.Length(1, 255).Error(lengthMsg),
		),
		validation.Field(
			&req.PublishTime,
			validation.Required.Error(requiredMsg),
		),
		validation.Field(
			&req.Language,
			validation.Required.Error(requiredMsg),
		),
	)
}

func (h editArticlePage) validateDuplicateURL(
	article model.Article,
) func(value interface{}) error {
	return func(value interface{}) error {
		url, _ := value.(string)
		if url == article.URL {
			return nil
		}

		ctx := context.Background()

		_, err := h.articleRepository.GetByURL(ctx, url, article.Language)
		if err != nil {
			if !db.IsNotFoundError(err) {
				logger.Error(ctx, err.Error())
			}

			return nil
		}

		return errors.New(i18n.T(i18n.DefaultLanguage(), "admin_err_article_duplicate", url))
	}
}
