// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package models

import (
	"time"

	"xorm.io/builder"

	"code.vikunja.io/api/pkg/user"
	"code.vikunja.io/web"
	"xorm.io/xorm"
)

// SubscriptionEntityType represents all entities which can be subscribed to
type SubscriptionEntityType int

const (
	SubscriptionEntityUnknown = iota
	SubscriptionEntityNamespace
	SubscriptionEntityProject
	SubscriptionEntityTask
)

const (
	entityNamespace = `namespace`
	entityProject   = `project`
	entityTask      = `task`
)

// Subscription represents a subscription for an entity
type Subscription struct {
	// The numeric ID of the subscription
	ID int64 `xorm:"autoincr not null unique pk" json:"id"`

	EntityType SubscriptionEntityType `xorm:"index not null" json:"-"`
	Entity     string                 `xorm:"-" json:"entity" param:"entity"`
	// The id of the entity to subscribe to.
	EntityID int64 `xorm:"bigint index not null" json:"entity_id" param:"entityID"`

	// The user who made this subscription
	User   *user.User `xorm:"-" json:"user"`
	UserID int64      `xorm:"bigint index not null" json:"-"`

	// A timestamp when this subscription was created. You cannot change this value.
	Created time.Time `xorm:"created not null" json:"created"`

	web.CRUDable `xorm:"-" json:"-"`
	web.Rights   `xorm:"-" json:"-"`
}

// TableName gives us a better tabel name for the subscriptions table
func (sb *Subscription) TableName() string {
	return "subscriptions"
}

func getEntityTypeFromString(entityType string) SubscriptionEntityType {
	switch entityType {
	case entityNamespace:
		return SubscriptionEntityNamespace
	case entityProject:
		return SubscriptionEntityProject
	case entityTask:
		return SubscriptionEntityTask
	}

	return SubscriptionEntityUnknown
}

// String returns a human-readable string of an entity
func (et SubscriptionEntityType) String() string {
	switch et {
	case SubscriptionEntityNamespace:
		return entityNamespace
	case SubscriptionEntityProject:
		return entityProject
	case SubscriptionEntityTask:
		return entityTask
	}

	return ""
}

func (et SubscriptionEntityType) validate() error {
	if et == SubscriptionEntityNamespace ||
		et == SubscriptionEntityProject ||
		et == SubscriptionEntityTask {
		return nil
	}

	return &ErrUnknownSubscriptionEntityType{EntityType: et}
}

// Create subscribes the current user to an entity
// @Summary Subscribes the current user to an entity.
// @Description Subscribes the current user to an entity.
// @tags subscriptions
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `project` or `task`."
// @Param entityID path string true "The numeric id of the entity to subscribe to."
// @Success 201 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
// @Failure 412 {object} web.HTTPError "The subscription already exists."
// @Failure 412 {object} web.HTTPError "The subscription entity is invalid."
// @Failure 500 {object} models.Message "Internal error"
// @Router /subscriptions/{entity}/{entityID} [put]
func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) {
	// Rights method alread does the validation of the entity type so we don't need to do that here

	sb.UserID = auth.GetID()

	sub, err := GetSubscription(s, sb.EntityType, sb.EntityID, auth)
	if err != nil {
		return err
	}
	if sub != nil {
		return &ErrSubscriptionAlreadyExists{
			EntityID:   sb.EntityID,
			EntityType: sb.EntityType,
			UserID:     sb.UserID,
		}
	}

	_, err = s.Insert(sb)
	if err != nil {
		return
	}

	sb.User, err = user.GetFromAuth(auth)
	return
}

// Delete unsubscribes the current user to an entity
// @Summary Unsubscribe the current user from an entity.
// @Description Unsubscribes the current user to an entity.
// @tags subscriptions
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `project` or `task`."
// @Param entityID path string true "The numeric id of the subscribed entity to."
// @Success 200 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
// @Failure 404 {object} web.HTTPError "The subscription does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /subscriptions/{entity}/{entityID} [delete]
func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
	sb.UserID = auth.GetID()

	_, err = s.
		Where("entity_id = ? AND entity_type = ? AND user_id = ?", sb.EntityID, sb.EntityType, sb.UserID).
		Delete(&Subscription{})
	return
}

func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
	if entityType == SubscriptionEntityNamespace {
		cond = builder.And(
			builder.Eq{"entity_id": entityID},
			builder.Eq{"entity_type": SubscriptionEntityNamespace},
		)
	}

	if entityType == SubscriptionEntityProject {
		cond = builder.Or(
			builder.And(
				builder.Eq{"entity_id": entityID},
				builder.Eq{"entity_type": SubscriptionEntityProject},
			),
			builder.And(
				builder.Eq{"entity_id": builder.
					Select("namespace_id").
					From("projects").
					Where(builder.Eq{"id": entityID}),
				},
				builder.Eq{"entity_type": SubscriptionEntityNamespace},
			),
		)
	}

	if entityType == SubscriptionEntityTask {
		cond = builder.Or(
			builder.And(
				builder.Eq{"entity_id": entityID},
				builder.Eq{"entity_type": SubscriptionEntityTask},
			),
			builder.And(
				builder.Eq{"entity_id": builder.
					Select("namespace_id").
					From("projects").
					Join("INNER", "tasks", "projects.id = tasks.project_id").
					Where(builder.Eq{"tasks.id": entityID}),
				},
				builder.Eq{"entity_type": SubscriptionEntityNamespace},
			),
			builder.And(
				builder.Eq{"entity_id": builder.
					Select("project_id").
					From("tasks").
					Where(builder.Eq{"id": entityID}),
				},
				builder.Eq{"entity_type": SubscriptionEntityProject},
			),
		)
	}

	return
}

// GetSubscription returns a matching subscription for an entity and user.
// It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for
// that task, if there is none it will look for a subscription on the project the task belongs to and if that also
// doesn't exist it will check for a subscription for the namespace the project is belonging to.
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
	subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
	if err != nil || len(subs) == 0 {
		return nil, err
	}
	if sub, exists := subs[entityID]; exists {
		return sub, nil // Take exact match first, if available
	}
	for _, sub := range subs {
		return sub, nil // For parents, take next available
	}
	return nil, nil
}

// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64]*Subscription, err error) {
	u, is := a.(*user.User)
	if !is {
		return
	}
	if err := entityType.validate(); err != nil {
		return nil, err
	}

	var entitiesFilter builder.Cond
	for _, eID := range entityIDs {
		if entitiesFilter == nil {
			entitiesFilter = getSubscriberCondForEntity(entityType, eID)
			continue
		}
		entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
	}

	var subscriptions []*Subscription
	err = s.
		Where("user_id = ?", u.ID).
		And(entitiesFilter).
		Find(&subscriptions)
	if err != nil {
		return nil, err
	}

	projectsToSubscriptions = make(map[int64]*Subscription)
	for _, sub := range subscriptions {
		sub.Entity = sub.EntityType.String()
		projectsToSubscriptions[sub.EntityID] = sub
	}
	return projectsToSubscriptions, nil
}

func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
	if err := entityType.validate(); err != nil {
		return nil, err
	}

	cond := getSubscriberCondForEntity(entityType, entityID)
	err = s.
		Where(cond).
		Find(&subscriptions)
	if err != nil {
		return
	}

	userIDs := []int64{}
	for _, subscription := range subscriptions {
		userIDs = append(userIDs, subscription.UserID)
	}

	users, err := user.GetUsersByIDs(s, userIDs)
	if err != nil {
		return
	}

	for _, subscription := range subscriptions {
		subscription.User = users[subscription.UserID]
	}
	return
}
