package services

import (
	"context"
	"encoding/json"
	"fmt"
	"history-api/internal/dtos/request"
	"history-api/internal/dtos/response"
	"history-api/internal/gen/sqlc"
	"history-api/internal/models"
	"history-api/internal/repositories"
	"history-api/pkg/cache"
	"history-api/pkg/constants"
	"history-api/pkg/convert"
	"history-api/pkg/storage"
	"io"
	"mime/multipart"
	"net/url"
	"path/filepath"
	"slices"
	"strings"

	"github.com/gofiber/fiber/v3"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/rs/zerolog/log"
	"golang.org/x/sync/errgroup"
)

type MediaService interface {
	GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, *fiber.Error)
	GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, *fiber.Error)
	SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, *fiber.Error)
	DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) *fiber.Error
	BulkDeleteMedia(ctx context.Context, claims *response.JWTClaims, dto *request.MediaBulkDeleteDto) *fiber.Error
	UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, *fiber.Error)
	GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, *fiber.Error)
	PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, *fiber.Error)
}

type mediaService struct {
	mediaRepo repositories.MediaRepository
	tokenRepo repositories.TokenRepository
	s         storage.Storage
	c         cache.Cache
}

func NewMediaService(
	mediaRepo repositories.MediaRepository,
	tokenRepo repositories.TokenRepository,
	s storage.Storage,
	c cache.Cache,
) MediaService {
	return &mediaService{
		mediaRepo: mediaRepo,
		tokenRepo: tokenRepo,
		s:         s,
		c:         c,
	}
}

func (m *mediaService) DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) *fiber.Error {
	mediaIdUUID, err := convert.StringToUUID(mediaId)
	if err != nil {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid media ID format")
	}

	media, err := m.mediaRepo.GetByID(ctx, mediaIdUUID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, "Media not found")
	}

	shoudDelete := false
	if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) || media.UserID == claims.UId {
		shoudDelete = true
	}

	if !shoudDelete {
		return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this media")
	}

	err = m.mediaRepo.Delete(ctx, mediaIdUUID)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete media from database")
	}

	m.c.PublishTask(ctx, constants.StreamStorageName, constants.TaskTypeDeleteMedia, media.ToStorageEntity())

	return nil
}

func (m *mediaService) BulkDeleteMedia(ctx context.Context, claims *response.JWTClaims, dto *request.MediaBulkDeleteDto) *fiber.Error {
	listMedia, err := m.mediaRepo.GetByIDs(ctx, dto.MediaIDs)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch media list")
	}
	shoudDelete := false
	if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) {
		shoudDelete = true
	}
	listMediaIds := make([]pgtype.UUID, 0)
	listMediaStorageEntities := make([]*models.MediaStorageEntity, 0)
	for _, media := range listMedia {
		if media.UserID != claims.UId && !shoudDelete {
			return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete media "+media.ID)
		}
		id, err := convert.StringToUUID(media.ID)
		if err != nil {
			continue
		}
		listMediaIds = append(listMediaIds, id)
		listMediaStorageEntities = append(listMediaStorageEntities, media.ToStorageEntity())
	}

	err = m.mediaRepo.BulkDelete(ctx, listMediaIds)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to bulk delete media from database")
	}

	m.c.PublishTask(ctx, constants.StreamStorageName, constants.TaskTypeBulkDeleteMedia, listMediaStorageEntities)

	return nil
}

func (m *mediaService) GetMediaByID(ctx context.Context, id string) (*response.MediaResponse, *fiber.Error) {
	mediaId, err := convert.StringToUUID(id)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid media ID format")
	}
	media, err := m.mediaRepo.GetByID(ctx, mediaId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Media not found")
	}
	return media.ToResponse(), nil
}

func (m *mediaService) GetMediaByUserID(ctx context.Context, id string) ([]*response.MediaResponse, *fiber.Error) {
	userId, err := convert.StringToUUID(id)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID format")
	}
	medias, err := m.mediaRepo.GetByUserID(ctx, userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user media")
	}
	return models.MediaEntitiesToResponse(medias), nil
}

func (m *mediaService) fillSearchArgs(arg *sqlc.SearchMediasParams, dto *request.SearchMediaDto) {
	if dto.Sort != "" {
		arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
	} else {
		arg.Sort = pgtype.Text{String: "id", Valid: true}
	}

	arg.Order = pgtype.Text{String: "asc", Valid: true}
	if dto.Order == "desc" {
		arg.Order = pgtype.Text{String: "desc", Valid: true}
	}

	if dto.MimeType != "" {
		arg.MimeType = pgtype.Text{String: dto.MimeType, Valid: true}
	}

	if dto.MaxSize != nil {
		arg.MaxSize = pgtype.Int8{Int64: *dto.MaxSize, Valid: true}
	}

	if dto.MinSize != nil {
		arg.MinSize = pgtype.Int8{Int64: *dto.MinSize, Valid: true}
	}

	if len(dto.UserIDs) > 0 {
		for _, id := range dto.UserIDs {
			if u, err := convert.StringToUUID(id); err == nil {
				arg.UserIds = append(arg.UserIds, u)
			}
		}
	}

	if dto.Search != "" {
		arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
	}
}

func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, *fiber.Error) {
	if dto.Page < 1 {
		dto.Page = 1
	}
	if dto.Limit == 0 {
		dto.Limit = 20
	}
	offset := (dto.Page - 1) * dto.Limit

	arg := sqlc.SearchMediasParams{
		Limit:  int32(dto.Limit),
		Offset: int32(offset),
	}

	m.fillSearchArgs(&arg, dto)

	var rows []*models.MediaEntity
	var totalRecords int64

	g, gCtx := errgroup.WithContext(ctx)

	g.Go(func() error {
		var err error
		rows, err = m.mediaRepo.Search(gCtx, arg)
		return err
	})

	g.Go(func() error {
		countArg := sqlc.CountMediasParams{
			UserIds:    arg.UserIds,
			MimeType:   arg.MimeType,
			MinSize:    arg.MinSize,
			MaxSize:    arg.MaxSize,
			SearchText: arg.SearchText,
		}
		var err error
		totalRecords, err = m.mediaRepo.Count(gCtx, countArg)
		return err
	})

	if err := g.Wait(); err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search media")
	}
	media := models.MediaEntitiesToResponse(rows)

	return response.BuildPaginatedResponse(media, totalRecords, dto.Page, dto.Limit), nil
}

func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, *fiber.Error) {
	userIdUUID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID format")
	}
	file, err := fileHeader.Open()
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Cannot open file")
	}
	defer file.Close()
	var reader io.Reader = file
	fileExt := filepath.Ext(fileHeader.Filename)
	contentType := fileHeader.Header.Get("Content-Type")
	mid, err := uuid.NewV7()
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
	}
	newFileName := mid.String() + fileExt
	originalName := fileHeader.Filename
	encodedName := url.QueryEscape(originalName)

	dispositionType := "attachment"
	if strings.HasPrefix(contentType, "image/") || contentType == "application/pdf" {
		dispositionType = "inline"
	}

	contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
		dispositionType,
		"file"+fileExt,
		encodedName,
	)

	metadata := map[string]string{
		"original-name": encodedName,
	}

	mdByte, err := json.Marshal(metadata)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
	}

	err = m.s.Upload(ctx, newFileName, reader, fileHeader.Size, storage.UploadOptions{
		ContentType:        contentType,
		ContentDisposition: contentDisposition,
		Metadata:           metadata,
	})
	if err != nil {
		log.Err(err).Msg("Failed to upload file to storage")
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload file to storage")
	}

	media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
		UserID:       userIdUUID,
		StorageKey:   newFileName,
		OriginalName: originalName,
		MimeType:     contentType,
		Size:         fileHeader.Size,
		FileMetadata: mdByte,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create media record")
	}
	return media.ToResponse(), nil
}

func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, *fiber.Error) {
	fileExt := filepath.Ext(dto.FileName)
	mid, err := uuid.NewV7()
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
	}
	newFileName := mid.String() + fileExt
	encodedName := url.QueryEscape(dto.FileName)

	dispositionType := "attachment"
	if dto.ContentType == "application/pdf" || (len(dto.ContentType) > 6 && dto.ContentType[:6] == "image/") {
		dispositionType = "inline"
	}

	contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
		dispositionType, "file"+fileExt, encodedName)

	metadata := map[string]string{
		"original-name": encodedName,
	}
	mdByte, err := json.Marshal(metadata)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
	}

	presignedURL, err := m.s.PresignUpload(ctx, newFileName, constants.PreSignedURLDuration, storage.UploadOptions{
		ContentType:        dto.ContentType,
		ContentDisposition: contentDisposition,
		Metadata:           metadata,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate presigned URL")
	}

	tokenId := uuid.New().String()
	err = m.tokenRepo.CreateUploadToken(
		ctx,
		userId,
		&models.TokenUploadEntity{
			ID:           tokenId,
			UserID:       userId,
			StorageKey:   newFileName,
			OriginalName: dto.FileName,
			MimeType:     dto.ContentType,
			Size:         dto.Size,
			FileMetadata: mdByte,
		},
	)

	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create upload token")
	}

	return &response.PreSignedResponse{
		TokenID:    tokenId,
		UploadUrl:  presignedURL,
		StorageKey: newFileName,
		SignedHeaders: map[string]string{
			"x-amz-meta-original-name": encodedName,
			"Content-Disposition":      contentDisposition,
		},
	}, nil
}

func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, *fiber.Error) {
	token, err := m.tokenRepo.GetUploadToken(ctx, userId, dto.TokenID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get upload token")
	}
	if token == nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
	}
	userIdUUID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID format")
	}

	err = m.s.Move(
		ctx,
		&storage.MoveOptions{
			Bucket: m.s.GetTempBucket(),
			Key:    token.StorageKey,
		},
		&storage.MoveOptions{
			Bucket: m.s.GetMainBucket(),
			Key:    token.StorageKey,
		},
	)

	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to move file to main storage")
	}

	media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
		UserID:       userIdUUID,
		StorageKey:   token.StorageKey,
		OriginalName: token.OriginalName,
		MimeType:     token.MimeType,
		Size:         token.Size,
		FileMetadata: token.FileMetadata,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create media record")
	}

	_ = m.tokenRepo.DeleteUploadToken(ctx, userId, dto.TokenID)

	return media.ToResponse(), nil
}
