//
// Copyright 2023-2025 The Chainloop Authors.
//
// 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 data

import (
	"context"
	"fmt"
	"time"

	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/google/uuid"
)

type userRepo struct {
	data *Data
	log  *log.Helper
}

func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
	return &userRepo{
		data: data,
		log:  log.NewHelper(logger),
	}
}

func (r *userRepo) FindByEmail(ctx context.Context, email string) (*biz.User, error) {
	u, err := r.data.DB.User.Query().
		Where(user.Email(email)).
		Only(ctx)
	if err != nil && !ent.IsNotFound(err) {
		return nil, err
	} else if u == nil {
		return nil, nil
	}

	return entUserToBizUser(u), nil
}

func (r *userRepo) CreateByEmail(ctx context.Context, email string, firstName, lastName *string) (*biz.User, error) {
	u, err := r.data.DB.User.Create().SetEmail(email).SetNillableFirstName(firstName).SetNillableLastName(lastName).Save(ctx)
	if err != nil {
		return nil, err
	}

	// Query it to load the fully formed object, including proper casted dates that come from the DB
	return r.FindByID(ctx, u.ID)
}

// UpdateNameAndLastName updates the first and last name of a user
func (r *userRepo) UpdateNameAndLastName(ctx context.Context, userID uuid.UUID, firstName, lastName *string) (*biz.User, error) {
	u, err := r.data.DB.User.UpdateOneID(userID).SetNillableFirstName(firstName).SetNillableLastName(lastName).SetUpdatedAt(time.Now()).Save(ctx)
	if err != nil {
		return nil, fmt.Errorf("error updating user name: %w", err)
	}

	return entUserToBizUser(u), nil
}

// Find by ID, returns nil if not found
func (r *userRepo) FindByID(ctx context.Context, userID uuid.UUID) (*biz.User, error) {
	u, err := r.data.DB.User.Get(ctx, userID)
	if err != nil && !ent.IsNotFound(err) {
		return nil, err
	} else if u == nil {
		return nil, nil
	}

	return entUserToBizUser(u), nil
}

func (r *userRepo) Delete(ctx context.Context, userID uuid.UUID) (err error) {
	return r.data.DB.User.DeleteOneID(userID).Exec(ctx)
}

// UpdateAccess updates the access restriction for a user
func (r *userRepo) UpdateAccess(ctx context.Context, userID uuid.UUID, isAccessRestricted bool) (*biz.User, error) {
	u, err := r.data.DB.User.UpdateOneID(userID).SetHasRestrictedAccess(isAccessRestricted).SetUpdatedAt(time.Now()).Save(ctx)
	if err != nil {
		return nil, fmt.Errorf("error updating user access: %w", err)
	}

	return entUserToBizUser(u), nil
}

// FindAll get all users in the system using pagination
func (r *userRepo) FindAll(ctx context.Context, pagination *pagination.OffsetPaginationOpts) ([]*biz.User, int, error) {
	if pagination == nil {
		return nil, 0, fmt.Errorf("pagination options is required")
	}

	baseQuery := r.data.DB.User.Query()

	count, err := baseQuery.Count(ctx)
	if err != nil {
		return nil, 0, err
	}

	users, err := baseQuery.
		Order(ent.Desc(workflow.FieldCreatedAt)).
		Limit(pagination.Limit()).
		Offset(pagination.Offset()).
		All(ctx)
	if err != nil {
		return nil, 0, err
	}

	result := make([]*biz.User, 0, len(users))
	for _, u := range users {
		result = append(result, entUserToBizUser(u))
	}

	return result, count, nil
}

// HasUsersWithAccessPropertyNotSet returns the number of users with restricted access or unset access
func (r *userRepo) HasUsersWithAccessPropertyNotSet(ctx context.Context) (bool, error) {
	return r.data.DB.User.Query().Where(user.HasRestrictedAccessIsNil()).Exist(ctx)
}

func (r *userRepo) FindUsersWithAccessPropertyNotSet(ctx context.Context) ([]*biz.User, error) {
	users, err := r.data.DB.User.Query().Where(user.HasRestrictedAccessIsNil()).All(ctx)
	if err != nil {
		return nil, fmt.Errorf("find users with access property not set: %w", err)
	}

	result := make([]*biz.User, 0, len(users))
	for _, u := range users {
		result = append(result, entUserToBizUser(u))
	}

	return result, nil
}

func entUserToBizUser(eu *ent.User) *biz.User {
	base := &biz.User{
		ID:        eu.ID.String(),
		Email:     eu.Email,
		FirstName: eu.FirstName,
		LastName:  eu.LastName,
		CreatedAt: toTimePtr(eu.CreatedAt),
		UpdatedAt: toTimePtr(eu.UpdatedAt),
	}

	if eu.HasRestrictedAccess != nil {
		base.HasRestrictedAccess = eu.HasRestrictedAccess
	}

	return base
}
