package service

import (
	"sort"
	"strings"
	"time"
	"voltaserve/cache"
	"voltaserve/config"
	"voltaserve/errorpkg"
	"voltaserve/guard"
	"voltaserve/infra"
	"voltaserve/model"
	"voltaserve/repo"
)

type Invitation struct {
	ID           string        `json:"id"`
	Owner        *User         `json:"owner,omitempty"`
	Email        string        `json:"email"`
	Organization *Organization `json:"organization,omitempty"`
	Status       string        `json:"status"`
	CreateTime   string        `json:"createTime"`
	UpdateTime   *string       `json:"updateTime"`
}

type InvitationList struct {
	Data          []*Invitation `json:"data"`
	TotalPages    uint          `json:"totalPages"`
	TotalElements uint          `json:"totalElements"`
	Page          uint          `json:"page"`
	Size          uint          `json:"size"`
}

type InvitationCreateOptions struct {
	OrganizationID string   `json:"organizationId" validate:"required"`
	Emails         []string `json:"emails" validate:"required,dive,email"`
}

type InvitationListOptions struct {
	Page      uint
	Size      uint
	SortBy    string
	SortOrder string
}

type InvitationService struct {
	orgRepo          repo.OrganizationRepo
	orgMapper        *organizationMapper
	invitationRepo   repo.InvitationRepo
	invitationMapper *invitationMapper
	orgCache         *cache.OrganizationCache
	orgGuard         *guard.OrganizationGuard
	userRepo         repo.UserRepo
	mailTmpl         *infra.MailTemplate
	config           config.Config
}

func NewInvitationService() *InvitationService {
	return &InvitationService{
		orgRepo:          repo.NewOrganizationRepo(),
		orgCache:         cache.NewOrganizationCache(),
		orgGuard:         guard.NewOrganizationGuard(),
		invitationRepo:   repo.NewInvitationRepo(),
		invitationMapper: newInvitationMapper(),
		userRepo:         repo.NewUserRepo(),
		mailTmpl:         infra.NewMailTemplate(),
		orgMapper:        newOrganizationMapper(),
		config:           config.GetConfig(),
	}
}

func (svc *InvitationService) Create(opts InvitationCreateOptions, userID string) error {
	for i := range opts.Emails {
		opts.Emails[i] = strings.ToLower(opts.Emails[i])
	}
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return err
	}
	org, err := svc.orgCache.Get(opts.OrganizationID)
	if err != nil {
		return err
	}
	if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil {
		return err
	}
	orgMembers, err := svc.orgRepo.GetMembers(opts.OrganizationID)
	if err != nil {
		return err
	}
	outgoingInvitations, err := svc.invitationRepo.GetOutgoing(opts.OrganizationID, userID)
	if err != nil {
		return err
	}

	var emails []string

	/* Collect emails of non existing members and outgoing invitations */
	for _, e := range opts.Emails {
		existing := false
		for _, u := range orgMembers {
			if e == u.GetEmail() {
				existing = true
				break
			}
		}
		for _, i := range outgoingInvitations {
			if e == i.GetEmail() && i.GetStatus() == model.InvitationStatusPending {
				existing = true
				break
			}
		}
		if !existing {
			emails = append(emails, e)
		}
	}

	/* Persist invitations */
	invitations, err := svc.invitationRepo.Insert(repo.InvitationInsertOptions{
		UserID:         userID,
		OrganizationID: opts.OrganizationID,
		Emails:         emails,
	})
	if err != nil {
		return err
	}

	/* Send emails */
	for _, inv := range invitations {
		variables := map[string]string{
			"USER_FULL_NAME":    user.GetFullName(),
			"ORGANIZATION_NAME": org.GetName(),
			"UI_URL":            svc.config.PublicUIURL,
		}
		_, err := svc.userRepo.FindByEmail(inv.GetEmail())
		var templateName string
		if err == nil {
			templateName = "join-organization"
		} else {
			templateName = "signup-and-join-organization"
		}
		if err := svc.mailTmpl.Send(templateName, inv.GetEmail(), variables); err != nil {
			return err
		}
	}
	return nil
}

func (svc *InvitationService) GetIncoming(opts InvitationListOptions, userID string) (*InvitationList, error) {
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return nil, err
	}
	invitations, err := svc.invitationRepo.GetIncoming(user.GetEmail())
	if err != nil {
		return nil, err
	}
	if opts.SortBy == "" {
		opts.SortBy = SortByDateCreated
	}
	if opts.SortOrder == "" {
		opts.SortOrder = SortOrderAsc
	}
	sorted := svc.doSorting(invitations, opts.SortBy, opts.SortOrder)
	paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
	mapped, err := svc.invitationMapper.mapMany(paged, userID)
	if err != nil {
		return nil, err
	}
	return &InvitationList{
		Data:          mapped,
		TotalPages:    totalPages,
		TotalElements: totalElements,
		Page:          opts.Page,
		Size:          uint(len(mapped)),
	}, nil
}

func (svc *InvitationService) GetOutgoing(orgID string, opts InvitationListOptions, userID string) (*InvitationList, error) {
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return nil, err
	}
	invitations, err := svc.invitationRepo.GetOutgoing(orgID, user.GetID())
	if err != nil {
		return nil, err
	}
	if opts.SortBy == "" {
		opts.SortBy = SortByDateCreated
	}
	if opts.SortOrder == "" {
		opts.SortOrder = SortOrderAsc
	}
	sorted := svc.doSorting(invitations, opts.SortBy, opts.SortOrder)
	paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
	mapped, err := svc.invitationMapper.mapMany(paged, userID)
	if err != nil {
		return nil, err
	}
	return &InvitationList{
		Data:          mapped,
		TotalPages:    totalPages,
		TotalElements: totalElements,
		Page:          opts.Page,
		Size:          uint(len(mapped)),
	}, nil
}

func (svc *InvitationService) Accept(id string, userID string) error {
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return err
	}
	invitation, err := svc.invitationRepo.Find(id)
	if err != nil {
		return err
	}
	if invitation.GetStatus() != model.InvitationStatusPending {
		return errorpkg.NewCannotAcceptNonPendingInvitationError(invitation)
	}
	if user.GetEmail() != invitation.GetEmail() {
		return errorpkg.NewUserNotAllowedToAcceptInvitationError(user, invitation)
	}
	org, err := svc.orgCache.Get(invitation.GetOrganizationID())
	if err != nil {
		return err
	}
	for _, u := range org.GetUsers() {
		if u == userID {
			return errorpkg.NewUserAlreadyMemberOfOrganizationError(user, org)
		}
	}
	invitation.SetStatus(model.InvitationStatusAccepted)
	if err := svc.invitationRepo.Save(invitation); err != nil {
		return err
	}
	if err := svc.orgRepo.AddUser(invitation.GetOrganizationID(), userID); err != nil {
		return err
	}
	if err := svc.orgRepo.GrantUserPermission(invitation.GetOrganizationID(), userID, model.PermissionViewer); err != nil {
		return err
	}
	if _, err := svc.orgCache.Refresh(invitation.GetOrganizationID()); err != nil {
		return err
	}
	return nil
}

func (svc *InvitationService) Decline(id string, userID string) error {
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return err
	}
	invitation, err := svc.invitationRepo.Find(id)
	if err != nil {
		return err
	}
	if invitation.GetStatus() != model.InvitationStatusPending {
		return errorpkg.NewCannotDeclineNonPendingInvitationError(invitation)
	}
	if user.GetEmail() != invitation.GetEmail() {
		return errorpkg.NewUserNotAllowedToDeclineInvitationError(user, invitation)
	}
	invitation.SetStatus(model.InvitationStatusDeclined)
	if err := svc.invitationRepo.Save(invitation); err != nil {
		return err
	}
	return nil
}

func (svc *InvitationService) Resend(id string, userID string) error {
	user, err := svc.userRepo.Find(userID)
	if err != nil {
		return err
	}
	invitation, err := svc.invitationRepo.Find(id)
	if err != nil {
		return err
	}
	if invitation.GetStatus() != model.InvitationStatusPending {
		return errorpkg.NewCannotResendNonPendingInvitationError(invitation)
	}
	org, err := svc.orgCache.Get(invitation.GetOrganizationID())
	if err != nil {
		return err
	}
	variables := map[string]string{
		"USER_FULL_NAME":    user.GetFullName(),
		"ORGANIZATION_NAME": org.GetName(),
		"UI_URL":            svc.config.PublicUIURL,
	}
	_, err = svc.userRepo.FindByEmail(invitation.GetEmail())
	var templateName string
	if err == nil {
		templateName = "join-organization"
	} else {
		templateName = "signup-and-join-organization"
	}
	if err := svc.mailTmpl.Send(templateName, invitation.GetEmail(), variables); err != nil {
		return err
	}
	return nil
}

func (svc *InvitationService) Delete(id string, userID string) error {
	invitation, err := svc.invitationRepo.Find(id)
	if err != nil {
		return err
	}
	if userID != invitation.GetOwnerID() {
		user, err := svc.userRepo.Find(userID)
		if err != nil {
			return err
		}
		return errorpkg.NewUserNotAllowedToDeleteInvitationError(user, invitation)
	}
	if err := svc.invitationRepo.Delete(invitation.GetID()); err != nil {
		return err
	}
	return nil
}

func (svc *InvitationService) doSorting(data []model.Invitation, sortBy string, sortOrder string) []model.Invitation {
	if sortBy == SortByEmail {
		sort.Slice(data, func(i, j int) bool {
			if sortOrder == SortOrderDesc {
				return data[i].GetEmail() > data[j].GetEmail()
			} else {
				return data[i].GetEmail() < data[j].GetEmail()
			}
		})
		return data
	} else if sortBy == SortByDateCreated {
		sort.Slice(data, func(i, j int) bool {
			a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime())
			b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime())
			if sortOrder == SortOrderDesc {
				return a.UnixMilli() > b.UnixMilli()
			} else {
				return a.UnixMilli() < b.UnixMilli()
			}
		})
		return data
	} else if sortBy == SortByDateModified {
		sort.Slice(data, func(i, j int) bool {
			if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil {
				a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime())
				b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime())
				if sortOrder == SortOrderDesc {
					return a.UnixMilli() > b.UnixMilli()
				} else {
					return a.UnixMilli() < b.UnixMilli()
				}
			} else {
				return false
			}
		})
		return data
	}
	return data
}

func (svc *InvitationService) doPagination(data []model.Invitation, page, size uint) ([]model.Invitation, uint, uint) {
	totalElements := uint(len(data))
	totalPages := (totalElements + size - 1) / size
	if page > totalPages {
		return nil, totalElements, totalPages
	}
	startIndex := (page - 1) * size
	endIndex := startIndex + size
	if endIndex > totalElements {
		endIndex = totalElements
	}
	pageData := data[startIndex:endIndex]
	return pageData, totalElements, totalPages
}

type invitationMapper struct {
	orgCache   *cache.OrganizationCache
	userRepo   repo.UserRepo
	userMapper *userMapper
	orgMapper  *organizationMapper
}

func newInvitationMapper() *invitationMapper {
	return &invitationMapper{
		orgCache:   cache.NewOrganizationCache(),
		userRepo:   repo.NewUserRepo(),
		userMapper: newUserMapper(),
		orgMapper:  newOrganizationMapper(),
	}
}

func (mp *invitationMapper) mapOne(m model.Invitation, userID string) (*Invitation, error) {
	owner, err := mp.userRepo.Find(m.GetOwnerID())
	if err != nil {
		return nil, err
	}
	org, err := mp.orgCache.Get(m.GetOrganizationID())
	if err != nil {
		return nil, err
	}
	v, err := mp.orgMapper.mapOne(org, userID)
	if err != nil {
		return nil, err
	}
	return &Invitation{
		ID:           m.GetID(),
		Owner:        mp.userMapper.mapOne(owner),
		Email:        m.GetEmail(),
		Organization: v,
		Status:       m.GetStatus(),
		CreateTime:   m.GetCreateTime(),
		UpdateTime:   m.GetUpdateTime(),
	}, nil
}

func (mp *invitationMapper) mapMany(invitations []model.Invitation, userID string) ([]*Invitation, error) {
	res := make([]*Invitation, 0)
	for _, m := range invitations {
		v, err := mp.mapOne(m, userID)
		if err != nil {
			return nil, err
		}
		res = append(res, v)
	}
	return res, nil
}
