package service

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"math/rand"
	"mime"
	"narasi-ahli-be/app/database/entity"
	articleApprovalsRepository "narasi-ahli-be/app/module/article_approvals/repository"
	articleCategoriesRepository "narasi-ahli-be/app/module/article_categories/repository"
	articleCategoryDetailsRepository "narasi-ahli-be/app/module/article_category_details/repository"
	articleCategoryDetailsReq "narasi-ahli-be/app/module/article_category_details/request"
	articleFilesRepository "narasi-ahli-be/app/module/article_files/repository"
	"narasi-ahli-be/app/module/articles/mapper"
	"narasi-ahli-be/app/module/articles/repository"
	"narasi-ahli-be/app/module/articles/request"
	"narasi-ahli-be/app/module/articles/response"
	usersRepository "narasi-ahli-be/app/module/users/repository"
	config "narasi-ahli-be/config/config"
	minioStorage "narasi-ahli-be/config/config"
	"narasi-ahli-be/utils/paginator"
	utilSvc "narasi-ahli-be/utils/service"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/minio/minio-go/v7"
	"github.com/rs/zerolog"
)

// ArticlesService
type articlesService struct {
	Repo                       repository.ArticlesRepository
	ArticleCategoriesRepo      articleCategoriesRepository.ArticleCategoriesRepository
	ArticleFilesRepo           articleFilesRepository.ArticleFilesRepository
	ArticleApprovalsRepo       articleApprovalsRepository.ArticleApprovalsRepository
	ArticleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository
	Log                        zerolog.Logger
	Cfg                        *config.Config
	UsersRepo                  usersRepository.UsersRepository
	MinioStorage               *minioStorage.MinioStorage
}

// ArticlesService define interface of IArticlesService
type ArticlesService interface {
	All(req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error)
	Show(id uint) (articles *response.ArticlesResponse, err error)
	ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error)
	Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error)
	SaveThumbnail(c *fiber.Ctx) (err error)
	Update(id uint, req request.ArticlesUpdateRequest) (err error)
	Delete(id uint) error
	UpdateActivityCount(id uint, activityTypeId int) (err error)
	UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error)
	UpdateBanner(id uint, isBanner bool) error
	Viewer(c *fiber.Ctx) error
	SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error)
	ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error)
	ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error)
	PublishScheduling(id uint, publishSchedule string) error
}

// NewArticlesService init ArticlesService
func NewArticlesService(
	repo repository.ArticlesRepository,
	articleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository,
	articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository,
	articleFilesRepo articleFilesRepository.ArticleFilesRepository,
	articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository,
	log zerolog.Logger,
	cfg *config.Config,
	usersRepo usersRepository.UsersRepository,
	minioStorage *minioStorage.MinioStorage) ArticlesService {

	return &articlesService{
		Repo:                       repo,
		ArticleCategoriesRepo:      articleCategoriesRepo,
		ArticleCategoryDetailsRepo: articleCategoryDetailsRepo,
		ArticleFilesRepo:           articleFilesRepo,
		ArticleApprovalsRepo:       articleApprovalsRepo,
		Log:                        log,
		UsersRepo:                  usersRepo,
		MinioStorage:               minioStorage,
		Cfg:                        cfg,
	}
}

// All implement interface of ArticlesService
func (_i *articlesService) All(req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) {
	if req.Category != nil {
		findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(*req.Category)
		if err != nil {
			return nil, paging, err
		}
		req.CategoryId = &findCategory.ID
	}

	results, paging, err := _i.Repo.GetAll(req)
	if err != nil {
		return
	}

	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:articlesService", "Methods:All").
		Interface("results", results).Msg("")

	host := _i.Cfg.App.Domain

	for _, result := range results {
		articleRes := mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo)
		articless = append(articless, articleRes)
	}

	return
}

func (_i *articlesService) Show(id uint) (articles *response.ArticlesResponse, err error) {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return nil, err
	}

	host := _i.Cfg.App.Domain

	return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil
}

func (_i *articlesService) ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error) {
	result, err := _i.Repo.FindByOldId(oldId)
	if err != nil {
		return nil, err
	}

	host := _i.Cfg.App.Domain

	return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil
}

func (_i *articlesService) Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) {
	_i.Log.Info().Interface("data", req).Msg("")
	newReq := req.ToEntity()

	var userLevelNumber int
	var userParentLevelId int
	if req.CreatedById != nil {
		createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
		if err != nil {
			return nil, fmt.Errorf("User not found")
		}
		newReq.CreatedById = &createdBy.ID
		userLevelNumber = createdBy.UserLevel.LevelNumber
		if createdBy.UserLevel.ParentLevelId != nil {
			userParentLevelId = *createdBy.UserLevel.ParentLevelId
		}
	} else {
		createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
		newReq.CreatedById = &createdBy.ID
		userLevelNumber = createdBy.UserLevel.LevelNumber
		if createdBy.UserLevel.ParentLevelId != nil {
			userParentLevelId = *createdBy.UserLevel.ParentLevelId
		}
	}

	isDraft := true
	if req.IsDraft == &isDraft {
		draftedAt := time.Now()
		newReq.IsDraft = &isDraft
		newReq.DraftedAt = &draftedAt
		isPublishFalse := false
		newReq.IsPublish = &isPublishFalse
		newReq.PublishedAt = nil
	}

	isPublish := true
	if req.IsPublish == &isPublish {
		publishedAt := time.Now()
		newReq.IsPublish = &isPublish
		newReq.PublishedAt = &publishedAt
		isDraftFalse := false
		newReq.IsDraft = &isDraftFalse
		newReq.DraftedAt = nil
	}

	if req.CreatedAt != nil {
		layout := "2006-01-02 15:04:05"
		parsedTime, err := time.Parse(layout, *req.CreatedAt)
		if err != nil {
			return nil, fmt.Errorf("Error parsing time:", err)
		}
		newReq.CreatedAt = parsedTime
	}

	// Approval
	statusIdOne := 1
	statusIdTwo := 2
	isPublishFalse := false
	createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
	if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false {
		newReq.NeedApprovalFrom = nil
		newReq.StatusId = &statusIdTwo
	} else {
		newReq.NeedApprovalFrom = &userParentLevelId
		newReq.StatusId = &statusIdOne
		newReq.IsPublish = &isPublishFalse
		newReq.PublishedAt = nil
	}

	saveArticleRes, err := _i.Repo.Create(newReq)
	if err != nil {
		return nil, err
	}

	// Approval
	var articleApproval *entity.ArticleApprovals

	if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true {
		articleApproval = &entity.ArticleApprovals{
			ArticleId:       saveArticleRes.ID,
			ApprovalBy:      *newReq.CreatedById,
			StatusId:        statusIdOne,
			Message:         "Need Approval",
			ApprovalAtLevel: &userLevelNumber,
		}
	} else {
		articleApproval = &entity.ArticleApprovals{
			ArticleId:       saveArticleRes.ID,
			ApprovalBy:      *newReq.CreatedById,
			StatusId:        statusIdTwo,
			Message:         "Publish Otomatis",
			ApprovalAtLevel: nil,
		}
	}

	_, err = _i.ArticleApprovalsRepo.Create(articleApproval)
	if err != nil {
		return nil, err
	}

	var categoryIds []string
	if req.CategoryIds != "" {
		categoryIds = strings.Split(req.CategoryIds, ",")
	}

	_i.Log.Info().Interface("categoryIds", categoryIds).Msg("")

	for _, categoryId := range categoryIds {
		categoryIdInt, _ := strconv.Atoi(categoryId)

		_i.Log.Info().Interface("categoryIdUint", uint(categoryIdInt)).Msg("")

		findCategory, err := _i.ArticleCategoriesRepo.FindOne(uint(categoryIdInt))

		_i.Log.Info().Interface("findCategory", findCategory).Msg("")

		if err != nil {
			return nil, err
		}

		if findCategory == nil {
			return nil, errors.New("category not found")
		}

		categoryReq := articleCategoryDetailsReq.ArticleCategoryDetailsCreateRequest{
			ArticleId:  saveArticleRes.ID,
			CategoryId: categoryIdInt,
			IsActive:   true,
		}
		newCategoryReq := categoryReq.ToEntity()

		err = _i.ArticleCategoryDetailsRepo.Create(newCategoryReq)
		if err != nil {
			return nil, err
		}
	}

	return saveArticleRes, nil
}

func (_i *articlesService) SaveThumbnail(c *fiber.Ctx) (err error) {

	id, err := strconv.ParseUint(c.Params("id"), 10, 0)
	if err != nil {
		return err
	}

	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:articlesService", "Methods:SaveThumbnail").
		Interface("id", id).Msg("")

	bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName

	form, err := c.MultipartForm()
	if err != nil {
		return err
	}
	files := form.File["files"]

	// Create minio connection.
	minioClient, err := _i.MinioStorage.ConnectMinio()
	if err != nil {
		// Return status 500 and minio connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Iterasi semua file yang diunggah
	for _, file := range files {

		_i.Log.Info().Str("timestamp", time.Now().
			Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1").
			Interface("data", file).Msg("")

		src, err := file.Open()
		if err != nil {
			return err
		}
		defer src.Close()

		filename := filepath.Base(file.Filename)
		filename = strings.ReplaceAll(filename, " ", "")
		filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
		extension := filepath.Ext(file.Filename)[1:]

		now := time.Now()
		rand.New(rand.NewSource(now.UnixNano()))
		randUniqueId := rand.Intn(1000000)

		newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
		newFilename := newFilenameWithoutExt + "." + extension
		objectName := fmt.Sprintf("articles/thumbnail/%d/%d/%s", now.Year(), now.Month(), newFilename)

		findCategory, err := _i.Repo.FindOne(uint(id))
		findCategory.ThumbnailName = &newFilename
		findCategory.ThumbnailPath = &objectName
		err = _i.Repo.Update(uint(id), findCategory)
		if err != nil {
			return err
		}

		// Upload file ke MinIO
		_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{})
		if err != nil {
			return err
		}
	}

	return
}

func (_i *articlesService) Update(id uint, req request.ArticlesUpdateRequest) (err error) {
	_i.Log.Info().Interface("data", req).Msg("")
	newReq := req.ToEntity()

	if req.CreatedAt != nil {
		layout := "2006-01-02 15:04:05"
		parsedTime, err := time.Parse(layout, *req.CreatedAt)
		if err != nil {
			return fmt.Errorf("Error parsing time:", err)
		}
		newReq.CreatedAt = parsedTime
	}

	return _i.Repo.UpdateSkipNull(id, newReq)
}

func (_i *articlesService) Delete(id uint) error {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return err
	}

	isActive := false
	result.IsActive = &isActive
	return _i.Repo.Update(id, result)
}

func (_i *articlesService) Viewer(c *fiber.Ctx) (err error) {
	thumbnailName := c.Params("thumbnailName")

	emptyImage := "empty-image.jpg"
	searchThumbnail := emptyImage
	if thumbnailName != emptyImage {
		result, err := _i.Repo.FindByFilename(thumbnailName)
		if err != nil {
			return err
		}
		_i.Log.Info().Str("timestamp", time.Now().
			Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer").
			Interface("resultThumbnail", result.ThumbnailPath).Msg("")

		if result.ThumbnailPath != nil {
			searchThumbnail = *result.ThumbnailPath
		} else {
			searchThumbnail = "articles/thumbnail/" + emptyImage
		}
	} else {
		searchThumbnail = "articles/thumbnail/" + emptyImage
	}

	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer").
		Interface("searchThumbnail", searchThumbnail).Msg("")

	ctx := context.Background()
	bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
	objectName := searchThumbnail

	// Create minio connection.
	minioClient, err := _i.MinioStorage.ConnectMinio()
	if err != nil {
		// Return status 500 and minio connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
	if err != nil {
		log.Fatalln(err)
	}
	defer fileContent.Close()

	contentType := mime.TypeByExtension("." + getFileExtension(objectName))
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	c.Set("Content-Type", contentType)

	if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
		return err
	}

	return
}

func (_i *articlesService) UpdateActivityCount(id uint, activityTypeId int) error {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return err
	}

	viewCount := 0
	if result.ViewCount != nil {
		viewCount = *result.ViewCount
	}
	shareCount := 0
	if result.ShareCount != nil {
		shareCount = *result.ShareCount
	}
	commentCount := 0
	if result.CommentCount != nil {
		commentCount = *result.CommentCount
	}

	if activityTypeId == 2 {
		viewCount++
	} else if activityTypeId == 3 {
		shareCount++
	} else if activityTypeId == 4 {
		commentCount++
	}
	result.ViewCount = &viewCount
	result.ShareCount = &shareCount
	result.CommentCount = &commentCount
	return _i.Repo.Update(id, result)
}

func (_i *articlesService) SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) {
	user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)

	result, err := _i.Repo.SummaryStats(user.ID)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (_i *articlesService) ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) {
	user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)

	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats").
		Interface("startDate", startDate).Msg("")
	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats").
		Interface("endDate", endDate).Msg("")

	var userLevelId *uint
	var userLevelNumber *int

	if user != nil {
		userLevelId = &user.UserLevelId
		userLevelNumber = &user.UserLevel.LevelNumber
	}

	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats").
		Interface("userLevelId", userLevelId).Msg("")
	_i.Log.Info().Str("timestamp", time.Now().
		Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats").
		Interface("userLevelNumber", userLevelNumber).Msg("")

	result, err := _i.Repo.ArticlePerUserLevelStats(userLevelId, userLevelNumber, nil, nil)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (_i *articlesService) ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) {
	user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)

	var userLevelId *uint
	var userLevelNumber *int

	if user != nil {
		userLevelId = &user.UserLevelId
		userLevelNumber = &user.UserLevel.LevelNumber
	}

	result, err := _i.Repo.ArticleMonthlyStats(userLevelId, userLevelNumber, *year)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (_i *articlesService) UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return err
	}

	_i.Log.Info().Interface("statusId", statusId).Msg("")

	statusIdOne := 1
	statusIdTwo := 2
	statusIdThree := 3
	isPublish := true
	isDraftFalse := false

	if statusId == 2 {
		if userLevelNumber == 2 || userLevelNumber == 3 {
			result.NeedApprovalFrom = &userParentLevelId
			result.StatusId = &statusIdOne
		} else {
			result.NeedApprovalFrom = nil
			result.StatusId = &statusIdTwo

			result.IsPublish = &isPublish
			publishedAt := time.Now()
			result.PublishedAt = &publishedAt

			result.IsDraft = &isDraftFalse
			result.DraftedAt = nil
		}

		userLevelIdStr := strconv.Itoa(userLevelId)
		if result.HasApprovedBy == nil {
			result.HasApprovedBy = &userLevelIdStr
		} else {
			hasApprovedBySlice := strings.Split(*result.HasApprovedBy, ",")
			hasApprovedBySlice = append(hasApprovedBySlice, userLevelIdStr)
			hasApprovedByJoin := strings.Join(hasApprovedBySlice, ",")
			result.HasApprovedBy = &hasApprovedByJoin
		}
	} else if statusId == 3 {
		result.StatusId = &statusIdThree
		result.NeedApprovalFrom = nil
		result.HasApprovedBy = nil
	}

	err = _i.Repo.Update(id, result)
	if err != nil {
		return err
	}

	return
}

func (_i *articlesService) PublishScheduling(id uint, publishSchedule string) error {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return err
	}
	result.PublishSchedule = &publishSchedule
	return _i.Repo.Update(id, result)
}

func (_i *articlesService) UpdateBanner(id uint, isBanner bool) error {
	result, err := _i.Repo.FindOne(id)
	if err != nil {
		return err
	}
	result.IsBanner = &isBanner
	return _i.Repo.Update(id, result)
}

func getFileExtension(filename string) string {
	// split file name
	parts := strings.Split(filename, ".")

	// jika tidak ada ekstensi, kembalikan string kosong
	if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
		return ""
	}

	// ambil ekstensi terakhir
	return parts[len(parts)-1]
}
