package ualert

import (
	"fmt"
	"time"

	"xorm.io/xorm"

	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/services/dashboards"
	dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
	"github.com/grafana/grafana/pkg/util"

	"github.com/grafana/grafana/pkg/infra/metrics"
	"github.com/grafana/grafana/pkg/models"
)

type roleType string

const (
	RoleViewer roleType = "Viewer"
	RoleEditor roleType = "Editor"
	RoleAdmin  roleType = "Admin"
)

func (r roleType) IsValid() bool {
	return r == RoleViewer || r == RoleAdmin || r == RoleEditor
}

type permissionType int

type dashboardACL struct {
	// nolint:stylecheck
	Id          int64
	OrgID       int64 `xorm:"org_id"`
	DashboardID int64 `xorm:"dashboard_id"`

	UserID     int64     `xorm:"user_id"`
	TeamID     int64     `xorm:"team_id"`
	Role       *roleType // pointer to be nullable
	Permission permissionType

	Created time.Time
	Updated time.Time
}

func (p dashboardACL) TableName() string { return "dashboard_acl" }

type folderHelper struct {
	sess *xorm.Session
	mg   *migrator.Migrator
}

// getOrCreateGeneralFolder returns the general folder under the specific organisation
// If the general folder does not exist it creates it.
func (m *folderHelper) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) {
	// there is a unique constraint on org_id, folder_id, title
	// there are no nested folders so the parent folder id is always 0
	dashboard := dashboard{OrgId: orgID, FolderId: 0, Title: GENERAL_FOLDER}
	has, err := m.sess.Get(&dashboard)
	if err != nil {
		return nil, err
	} else if !has {
		// create folder
		return m.createGeneralFolder(orgID)
	}
	return &dashboard, nil
}

func (m *folderHelper) createGeneralFolder(orgID int64) (*dashboard, error) {
	return m.createFolder(orgID, GENERAL_FOLDER)
}

// returns the folder of the given dashboard (if exists)
func (m *folderHelper) getFolder(dash dashboard, da dashAlert) (dashboard, error) {
	// get folder if exists
	folder := dashboard{}
	if dash.FolderId > 0 {
		exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
		if err != nil {
			return folder, fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err)
		}
		if !exists {
			return folder, fmt.Errorf("folder with id %v not found", dash.FolderId)
		}
		if !folder.IsFolder {
			return folder, fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId)
		}
	}
	return folder, nil
}

// based on sqlstore.saveDashboard()
// it should be called from inside a transaction
func (m *folderHelper) createFolder(orgID int64, title string) (*dashboard, error) {
	cmd := saveFolderCommand{
		OrgId:    orgID,
		FolderId: 0,
		IsFolder: true,
		Dashboard: simplejson.NewFromAny(map[string]interface{}{
			"title": title,
		}),
	}
	dash := cmd.getDashboardModel()

	uid, err := m.generateNewDashboardUid(dash.OrgId)
	if err != nil {
		return nil, err
	}
	dash.setUid(uid)

	parentVersion := dash.Version
	dash.setVersion(1)
	dash.Created = time.Now()
	dash.CreatedBy = FOLDER_CREATED_BY
	dash.Updated = time.Now()
	dash.UpdatedBy = FOLDER_CREATED_BY
	metrics.MApiDashboardInsert.Inc()

	if _, err = m.sess.Insert(dash); err != nil {
		return nil, err
	}

	dashVersion := &dashver.DashboardVersion{
		DashboardID:   dash.Id,
		ParentVersion: parentVersion,
		RestoredFrom:  cmd.RestoredFrom,
		Version:       dash.Version,
		Created:       time.Now(),
		CreatedBy:     dash.UpdatedBy,
		Message:       cmd.Message,
		Data:          dash.Data,
	}

	// insert version entry
	if _, err := m.sess.Insert(dashVersion); err != nil {
		return nil, err
	}
	return dash, nil
}

func (m *folderHelper) generateNewDashboardUid(orgId int64) (string, error) {
	for i := 0; i < 3; i++ {
		uid := util.GenerateShortUID()

		exists, err := m.sess.Where("org_id=? AND uid=?", orgId, uid).Get(&models.Dashboard{})
		if err != nil {
			return "", err
		}

		if !exists {
			return uid, nil
		}
	}

	return "", dashboards.ErrDashboardFailedGenerateUniqueUid
}

// based on SQLStore.UpdateDashboardACL()
// it should be called from inside a transaction
func (m *folderHelper) setACL(orgID int64, dashboardID int64, items []*dashboardACL) error {
	if dashboardID <= 0 {
		return fmt.Errorf("folder id must be greater than zero for a folder permission")
	}

	// userPermissionsMap is a map keeping the highest permission per user
	// for handling conficting inherited (folder) and non-inherited (dashboard) user permissions
	userPermissionsMap := make(map[int64]*dashboardACL, len(items))
	// teamPermissionsMap is a map keeping the highest permission per team
	// for handling conficting inherited (folder) and non-inherited (dashboard) team permissions
	teamPermissionsMap := make(map[int64]*dashboardACL, len(items))
	for _, item := range items {
		if item.UserID != 0 {
			acl, ok := userPermissionsMap[item.UserID]
			if !ok {
				userPermissionsMap[item.UserID] = item
			} else {
				if item.Permission > acl.Permission {
					// the higher permission wins
					userPermissionsMap[item.UserID] = item
				}
			}
		}

		if item.TeamID != 0 {
			acl, ok := teamPermissionsMap[item.TeamID]
			if !ok {
				teamPermissionsMap[item.TeamID] = item
			} else {
				if item.Permission > acl.Permission {
					// the higher permission wins
					teamPermissionsMap[item.TeamID] = item
				}
			}
		}
	}

	type keyType struct {
		UserID     int64 `xorm:"user_id"`
		TeamID     int64 `xorm:"team_id"`
		Role       roleType
		Permission permissionType
	}
	// seen keeps track of inserted perrmissions to avoid duplicates (due to inheritance)
	seen := make(map[keyType]struct{}, len(items))
	for _, item := range items {
		if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) {
			return models.ErrDashboardACLInfoMissing
		}

		// ignore duplicate user permissions
		if item.UserID != 0 {
			acl, ok := userPermissionsMap[item.UserID]
			if ok {
				if acl.Id != item.Id {
					continue
				}
			}
		}

		// ignore duplicate team permissions
		if item.TeamID != 0 {
			acl, ok := teamPermissionsMap[item.TeamID]
			if ok {
				if acl.Id != item.Id {
					continue
				}
			}
		}

		key := keyType{UserID: item.UserID, TeamID: item.TeamID, Role: "", Permission: item.Permission}
		if item.Role != nil {
			key.Role = *item.Role
		}
		if _, ok := seen[key]; ok {
			continue
		}

		// unset Id so that the new record will get a different one
		item.Id = 0
		item.OrgID = orgID
		item.DashboardID = dashboardID
		item.Created = time.Now()
		item.Updated = time.Now()

		m.sess.Nullable("user_id", "team_id")
		if _, err := m.sess.Insert(item); err != nil {
			return err
		}
		seen[key] = struct{}{}
	}

	// Update dashboard HasACL flag
	dashboard := models.Dashboard{HasACL: true}
	_, err := m.sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard)
	return err
}

// based on SQLStore.GetDashboardACLInfoList()
func (m *folderHelper) getACL(orgID, dashboardID int64) ([]*dashboardACL, error) {
	var err error

	falseStr := m.mg.Dialect.BooleanStr(false)

	result := make([]*dashboardACL, 0)
	rawSQL := `
			-- get distinct permissions for the dashboard and its parent folder
			SELECT DISTINCT
				da.id,
				da.user_id,
				da.team_id,
				da.permission,
				da.role
			FROM dashboard as d
				LEFT JOIN dashboard folder on folder.id = d.folder_id
				LEFT JOIN dashboard_acl AS da ON
				da.dashboard_id = d.id OR
				da.dashboard_id = d.folder_id  OR
				(
					-- include default permissions --
					da.org_id = -1 AND (
					  (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
					  (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
					)
				)
			WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
			ORDER BY da.id ASC
			`
	err = m.sess.SQL(rawSQL, orgID, dashboardID).Find(&result)
	return result, err
}

// getOrgsThatHaveFolders returns a unique list of organization ID that have at least one folder
func (m *folderHelper) getOrgsIDThatHaveFolders() (map[int64]struct{}, error) {
	// get folder if exists
	var rows []int64
	err := m.sess.Table(&dashboard{}).Where("is_folder=?", true).Distinct("org_id").Find(&rows)
	if err != nil {
		return nil, err
	}
	result := make(map[int64]struct{}, len(rows))
	for _, s := range rows {
		result[s] = struct{}{}
	}
	return result, nil
}
