// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 License 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 License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package models

import (
	"errors"

	"code.vikunja.io/api/pkg/user"
	"code.vikunja.io/api/pkg/utils"
	"code.vikunja.io/api/pkg/web"

	"xorm.io/xorm"
)

// CanWrite return whether the user can write on that project or not
func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {

	// The favorite project can't be edited
	if p.ID == FavoritesPseudoProject.ID {
		return false, nil
	}

	if isInstanceAdmin(s, a) {
		return true, nil
	}

	// Get the project and check the permission
	originalProject, err := GetProjectSimpleByID(s, p.ID)
	if err != nil {
		return false, err
	}

	// We put the result of the is archived check in a separate variable to be able to return it later without
	// needing to recheck it again
	errIsArchived := originalProject.CheckIsArchived(s)

	var canWrite bool

	// Check if we're dealing with a share auth
	shareAuth, ok := a.(*LinkSharing)
	if ok {
		return originalProject.ID == shareAuth.ProjectID &&
			(shareAuth.Permission == PermissionWrite || shareAuth.Permission == PermissionAdmin), errIsArchived
	}

	u := &user.User{ID: a.GetID()}

	// Check if the user is either owner or can write to the project
	if originalProject.isOwner(u) {
		canWrite = true
	}

	if canWrite {
		return canWrite, errIsArchived
	}

	canWrite, _, err = originalProject.checkPermission(s, u, PermissionWrite, PermissionAdmin)
	if err != nil {
		return false, err
	}
	return canWrite, errIsArchived
}

// CanRead checks if a user has read access to a project
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {

	if isInstanceAdmin(s, a) {
		originalProject, err := GetProjectSimpleByID(s, p.ID)
		if err != nil {
			return false, 0, err
		}
		*p = *originalProject
		return true, int(PermissionAdmin), nil
	}

	// The favorite project needs a special treatment
	if p.ID == FavoritesPseudoProject.ID {
		owner, err := user.GetFromAuth(a)
		if err != nil {
			return false, 0, err
		}

		*p = FavoritesPseudoProject
		p.Owner = owner
		return true, int(PermissionRead), nil
	}

	// Saved Filter Projects need a special case
	if GetSavedFilterIDFromProjectID(p.ID) > 0 {
		sf := &SavedFilter{ID: GetSavedFilterIDFromProjectID(p.ID)}
		return sf.CanRead(s, a)
	}

	// Check if the user is either owner or can read
	var err error
	originalProject, err := GetProjectSimpleByID(s, p.ID)
	if err != nil {
		return false, 0, err
	}

	*p = *originalProject

	// Check if we're dealing with a share auth
	shareAuth, ok := a.(*LinkSharing)
	if ok {
		return p.ID == shareAuth.ProjectID &&
			(shareAuth.Permission == PermissionRead || shareAuth.Permission == PermissionWrite || shareAuth.Permission == PermissionAdmin), int(shareAuth.Permission), nil
	}

	return p.checkPermission(s, &user.User{ID: a.GetID()}, PermissionRead, PermissionWrite, PermissionAdmin)
}

// CanUpdate checks if the user can update a project
func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
	// The favorite project can't be edited
	if p.ID == FavoritesPseudoProject.ID {
		return false, nil
	}

	if isInstanceAdmin(s, a) {
		return true, nil
	}

	fid := GetSavedFilterIDFromProjectID(p.ID)
	if fid > 0 {
		sf, err := GetSavedFilterSimpleByID(s, fid)
		if err != nil {
			return false, err
		}

		return sf.CanUpdate(s, a)
	}

	// Get the project
	ol, err := GetProjectSimpleByID(s, p.ID)
	if err != nil {
		return false, err
	}

	// Check if we're moving the project to a different parent project.
	// If that is the case, we need to verify permissions to do so.
	//
	// The reparent Admin gate for GHSA-2vq4-854f-5c72 / CVE-2026-35595
	// lives in UpdateProject, not here: CanUpdate is reused by
	// permission-check-only callers (buckets, webhooks, task ops) that
	// pass stub &Project{ID: ...} values with ParentProjectID=0 and never
	// commit a reparent, which would spuriously trip the gate.
	if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
		newProject := &Project{ID: p.ParentProjectID}
		can, err := newProject.CanWrite(s, a)
		if err != nil {
			return false, err
		}
		if !can {
			return false, ErrGenericForbidden{}
		}
	}

	canUpdate, err = p.CanWrite(s, a)
	// If the project is archived and the user tries to un-archive it, let the request through
	archivedErr := ErrProjectIsArchived{}
	is := errors.As(err, &archivedErr)
	if is && !p.IsArchived && archivedErr.ProjectID == p.ID {
		err = nil
	}
	return canUpdate, err
}

// CanDelete checks if the user can delete a project
func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
	if isInstanceAdmin(s, a) {
		return true, nil
	}
	return p.IsAdmin(s, a)
}

// CanCreate checks if the user can create a project
func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
	if isInstanceAdmin(s, a) {
		return true, nil
	}
	if p.ParentProjectID != 0 {
		parent := &Project{ID: p.ParentProjectID}
		return parent.CanWrite(s, a)
	}
	// Check if we're dealing with a share auth
	_, is := a.(*LinkSharing)
	if is {
		return false, nil
	}
	return true, nil
}

// IsAdmin returns whether the user has admin permissions on the project or not
func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
	// The favorite project can't be edited
	if p.ID == FavoritesPseudoProject.ID {
		return false, nil
	}

	if isInstanceAdmin(s, a) {
		return true, nil
	}

	originalProject, err := GetProjectSimpleByID(s, p.ID)
	if err != nil {
		return false, err
	}

	// Check if we're dealing with a share auth
	shareAuth, ok := a.(*LinkSharing)
	if ok {
		return originalProject.ID == shareAuth.ProjectID && shareAuth.Permission == PermissionAdmin, nil
	}

	u := &user.User{ID: a.GetID()}

	// Check all the things
	// Check if the user is either owner or can write to the project
	// Owners are always admins
	if originalProject.isOwner(u) {
		return true, nil
	}
	is, _, err := originalProject.checkPermission(s, u, PermissionAdmin)
	return is, err
}

// Little helper function to check if a user is project owner
func (p *Project) isOwner(u *user.User) bool {
	return p.OwnerID == u.ID
}

// Checks n different permissions for any given user
func (p *Project) checkPermission(s *xorm.Session, u *user.User, permissions ...Permission) (bool, int, error) {
	projectPermissions, err := checkPermissionsForProjects(s, u, []int64{p.ID})
	if err != nil {
		return false, 0, err
	}
	permission, has := projectPermissions[p.ID]
	if !has {
		return false, 0, nil
	}

	for _, r := range permissions {
		if r == permission.MaxPermission {
			return true, int(permission.MaxPermission), nil
		}
	}

	return false, 0, nil
}

type projectPermission struct {
	ID            int64 `xorm:"pk autoincr"`
	MaxPermission Permission
}

func checkPermissionsForProjects(s *xorm.Session, u *user.User, projectIDs []int64) (projectPermissionMap map[int64]*projectPermission, err error) {
	projectPermissionMap = make(map[int64]*projectPermission)

	if len(projectIDs) < 1 {
		return
	}

	args := []interface{}{
		u.ID,
		u.ID,
		u.ID,
		u.ID,
		u.ID,
		u.ID,
	}

	err = s.SQL(`
WITH RECURSIVE
    project_hierarchy AS (
        -- Base case: Start with the specified projects
        SELECT id,
               parent_project_id,
               0  AS level,
               id AS original_project_id
        FROM projects
        WHERE id IN (`+utils.JoinInt64Slice(projectIDs, ", ")+`)

        UNION ALL

        -- Recursive case: Traverse up the hierarchy
        SELECT p.id,
               p.parent_project_id,
               ph.level + 1,
               ph.original_project_id
        FROM projects p
                 INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id),

    -- Calculate max team permission for each project/user combination
    max_team_permissions AS (
        SELECT tl.project_id,
               MAX(tl.permission) AS max_team_permission
        FROM team_projects tl
                 INNER JOIN team_members tm ON tm.team_id = tl.team_id AND tm.user_id = ?
        GROUP BY tl.project_id
    ),

    project_permissions AS (SELECT ph.id,
                                   ph.original_project_id,
                                   CASE
                                       WHEN p.owner_id = ? THEN 2
                                       WHEN COALESCE(ul.permission, 0) > COALESCE(mtp.max_team_permission, 0) THEN ul.permission
                                       ELSE COALESCE(mtp.max_team_permission, 0)
                                       END AS project_permission,
            CASE
                WHEN p.owner_id = ? THEN 1  -- Direct project ownership
                ELSE ph.level + 1  -- Derived from parent project
            END AS priority
                            FROM project_hierarchy ph
                                LEFT JOIN projects p
                            ON ph.id = p.id
                                LEFT JOIN users_projects ul ON ul.project_id = ph.id AND ul.user_id = ?
                                LEFT JOIN max_team_permissions mtp ON mtp.project_id = ph.id
                            WHERE p.owner_id = ? OR ul.user_id = ? OR mtp.max_team_permission IS NOT NULL)

SELECT ph.original_project_id AS id,
       COALESCE(MAX(pp.project_permission), -1) AS max_permission
FROM project_hierarchy ph
         LEFT JOIN (SELECT *,
                           ROW_NUMBER() OVER (PARTITION BY original_project_id ORDER BY priority) AS rn
                    FROM project_permissions) pp ON ph.id = pp.id AND pp.rn = 1
GROUP BY ph.original_project_id`, args...).
		Find(&projectPermissionMap)
	return
}
