// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package database

import (
	"context"

	gitnessAppStore "github.com/harness/gitness/app/store"
	"github.com/harness/gitness/store"
	"github.com/harness/gitness/store/database"
	"github.com/harness/gitness/store/database/dbtx"
	"github.com/harness/gitness/types"

	"github.com/Masterminds/squirrel"
	"github.com/jmoiron/sqlx"
)

var _ gitnessAppStore.UserGroupStore = (*UserGroupStore)(nil)

func NewUserGroupStore(db *sqlx.DB) *UserGroupStore {
	return &UserGroupStore{
		db: db,
	}
}

type UserGroupStore struct {
	db *sqlx.DB
}

type UserGroup struct {
	SpaceID     int64  `db:"usergroup_space_id"`
	ID          int64  `db:"usergroup_id"`
	Identifier  string `db:"usergroup_identifier"`
	Name        string `db:"usergroup_name"`
	Description string `db:"usergroup_description"`
	Created     int64  `db:"usergroup_created"`
	Updated     int64  `db:"usergroup_updated"`
}

const (
	userGroupColumns = `
	usergroup_id
	,usergroup_identifier
	,usergroup_name
	,usergroup_description
	,usergroup_space_id
	,usergroup_created
	,usergroup_updated`

	userGroupSelectBase = `SELECT ` + userGroupColumns + ` FROM usergroups`
)

func mapUserGroup(ug *UserGroup) *types.UserGroup {
	return &types.UserGroup{
		ID:          ug.ID,
		Identifier:  ug.Identifier,
		Name:        ug.Name,
		Description: ug.Description,
		SpaceID:     ug.SpaceID,
		Created:     ug.Created,
		Updated:     ug.Updated,
	}
}

// FindByIdentifier returns a usergroup by its identifier.
func (s *UserGroupStore) FindByIdentifier(
	ctx context.Context,
	spaceID int64,
	identifier string,
) (*types.UserGroup, error) {
	const sqlQuery = userGroupSelectBase + ` WHERE usergroup_identifier = $1 AND usergroup_space_id = $2`
	db := dbtx.GetAccessor(ctx, s.db)
	dst := &UserGroup{}
	if err := db.GetContext(ctx, dst, sqlQuery, identifier, spaceID); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find usergroup by identifier %s", identifier)
	}
	return mapUserGroup(dst), nil
}

// Find returns a usergroup by its id.
func (s *UserGroupStore) Find(ctx context.Context, id int64) (*types.UserGroup, error) {
	const sqlQuery = userGroupSelectBase + ` WHERE usergroup_id = $1`
	db := dbtx.GetAccessor(ctx, s.db)
	dst := &UserGroup{}
	if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find usergroup by id %d", id)
	}
	return mapUserGroup(dst), nil
}

func (s *UserGroupStore) Map(ctx context.Context, ids []int64) (map[int64]*types.UserGroup, error) {
	result, err := s.FindManyByIDs(ctx, ids)
	if err != nil {
		return nil, err
	}
	if len(result) == 0 {
		return nil, store.ErrResourceNotFound
	}
	mapResult := make(map[int64]*types.UserGroup, len(result))
	for _, r := range result {
		mapResult[r.ID] = r
	}
	return mapResult, nil
}

func (s *UserGroupStore) FindManyByIDs(ctx context.Context, ids []int64) ([]*types.UserGroup, error) {
	stmt := database.Builder.
		Select(userGroupColumns).
		From("usergroups").
		Where(squirrel.Eq{"usergroup_id": ids})
	db := dbtx.GetAccessor(ctx, s.db)

	sqlQuery, params, err := stmt.ToSql()
	if err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "failed to generate find many usergroups by ids query")
	}

	var dst []*UserGroup
	if err := db.SelectContext(ctx, &dst, sqlQuery, params...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "find many by ids for usergroups query failed")
	}

	var result = make([]*types.UserGroup, len(dst))

	for i, u := range dst {
		result[i] = u.toUserGroupType()
	}

	return result, nil
}

func (s *UserGroupStore) FindManyByIdentifiersAndSpaceID(
	ctx context.Context,
	identifiers []string,
	spaceID int64,
) ([]*types.UserGroup, error) {
	stmt := database.Builder.
		Select(userGroupColumns).
		From("usergroups").
		Where(squirrel.Eq{"usergroup_identifier": identifiers}).Where(squirrel.Eq{"usergroup_space_id": spaceID})
	db := dbtx.GetAccessor(ctx, s.db)

	sqlQuery, params, err := stmt.ToSql()
	if err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "failed to generate find many usergroups query")
	}

	dst := []*UserGroup{}
	if err := db.SelectContext(ctx, &dst, sqlQuery, params...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "find many by identifiers for usergroups query failed")
	}
	result := make([]*types.UserGroup, len(dst))
	for i, u := range dst {
		result[i] = mapUserGroup(u)
	}
	return result, nil
}

// Create Creates a usergroup in the database.
func (s *UserGroupStore) Create(
	ctx context.Context,
	spaceID int64,
	userGroup *types.UserGroup,
) error {
	const sqlQuery = `
	INSERT INTO usergroups (
		usergroup_identifier
		,usergroup_name
		,usergroup_description	
		,usergroup_space_id	
		,usergroup_created
		,usergroup_updated
	) values (	
		:usergroup_identifier
		,:usergroup_name
		,:usergroup_description
		,:usergroup_space_id	
		,:usergroup_created
		,:usergroup_updated
	) RETURNING usergroup_id`

	db := dbtx.GetAccessor(ctx, s.db)

	query, arg, err := db.BindNamed(sqlQuery, mapInternalUserGroup(userGroup, spaceID))
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to bind usergroup object")
	}

	if err = db.QueryRowContext(ctx, query, arg...).Scan(&userGroup.ID); err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to insert usergroup")
	}

	return nil
}

func (s *UserGroupStore) CreateOrUpdate(
	ctx context.Context,
	spaceID int64,
	userGroup *types.UserGroup,
) error {
	const sqlQuery = `
	INSERT INTO usergroups (
		usergroup_identifier
		,usergroup_name
		,usergroup_description	
		,usergroup_space_id	
		,usergroup_created
		,usergroup_updated
	) values (	
		:usergroup_identifier
		,:usergroup_name
		,:usergroup_description
		,:usergroup_space_id	
		,:usergroup_created
		,:usergroup_updated
	) ON CONFLICT (usergroup_space_id, LOWER(usergroup_identifier)) DO UPDATE SET
		usergroup_name = EXCLUDED.usergroup_name,
		usergroup_description = EXCLUDED.usergroup_description,
		usergroup_updated = EXCLUDED.usergroup_updated
	`

	db := dbtx.GetAccessor(ctx, s.db)

	query, arg, err := db.BindNamed(sqlQuery, mapInternalUserGroup(userGroup, spaceID))
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to bind usergroup object")
	}

	if _, err = db.ExecContext(ctx, query, arg...); err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to insert usergroup")
	}

	return nil
}

func mapInternalUserGroup(u *types.UserGroup, spaceID int64) *UserGroup {
	return &UserGroup{
		ID:          u.ID,
		SpaceID:     spaceID,
		Identifier:  u.Identifier,
		Name:        u.Name,
		Description: u.Description,
		Created:     u.Created,
		Updated:     u.Updated,
	}
}

func (u *UserGroup) toUserGroupType() *types.UserGroup {
	return &types.UserGroup{
		ID:          u.ID,
		Identifier:  u.Identifier,
		Name:        u.Name,
		Description: u.Description,
		Created:     u.Created,
		Updated:     u.Updated,
	}
}
