// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package peridotimplv1

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
	"go.temporal.io/sdk/client"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/wrapperspb"
	peridotpb "peridot.resf.org/peridot/pb"
	"peridot.resf.org/utils"
	"strings"
)

func (s *Server) CreateProject(ctx context.Context, req *peridotpb.CreateProjectRequest) (*peridotpb.CreateProjectResponse, error) {
	if err := req.Validate(); err != nil {
		return nil, err
	}
	if err := req.Project.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectGlobal, ObjectIdPeridot, PermissionManage); err != nil {
		return nil, err
	}
	user, err := utils.UserFromContext(ctx)
	if err != nil {
		return nil, err
	}

	beginTx, err := s.db.Begin()
	if err != nil {
		s.log.Errorf("CreateProject: beginTx: %v", err)
		return nil, utils.InternalError
	}
	tx := s.db.UseTransaction(beginTx)

	p, err := tx.CreateProject(req.GetProject())
	if err != nil {
		if strings.Contains(err.Error(), "unique") {
			return nil, status.Error(codes.AlreadyExists, "project with name already exists")
		}
		s.log.Errorf("CreateProject: %v", err)
		return nil, status.Error(codes.Internal, "failed to create project")
	}

	_, err = s.authz.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{
		Updates: []*v1.RelationshipUpdate{
			{
				Operation: v1.RelationshipUpdate_OPERATION_CREATE,
				Relationship: &v1.Relationship{
					Resource: &v1.ObjectReference{
						ObjectType: ObjectProject,
						ObjectId:   p.ID.String(),
					},
					Relation: "manager",
					Subject: &v1.SubjectReference{
						Object: &v1.ObjectReference{
							ObjectType: SubjectUser,
							ObjectId:   user.ID,
						},
					},
				},
			},
		},
	})
	if err != nil {
		_ = beginTx.Rollback()
		s.log.Errorf("CreateProject: authz-create-relationships: %v", err)
		return nil, status.Error(codes.Internal, "failed to create project")
	}

	_, err = tx.CreateRepositoryWithPackages("all", p.ID.String(), false, []string{})
	if err != nil {
		_ = beginTx.Rollback()
		s.log.Errorf("CreateProject: db-create-repository: %v", err)
		return nil, status.Error(codes.Internal, "failed to create project")
	}

	err = beginTx.Commit()
	if err != nil {
		s.log.Errorf("CreateProject: commit: %v", err)
		return nil, status.Error(codes.Internal, "failed to create project")
	}

	return &peridotpb.CreateProjectResponse{
		Project: p.ToProto(),
	}, nil
}

func (s *Server) UpdateProject(ctx context.Context, req *peridotpb.UpdateProjectRequest) (*peridotpb.UpdateProjectResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := req.Project.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId, PermissionManage); err != nil {
		return nil, err
	}

	p, err := s.db.UpdateProject(req.ProjectId, req.GetProject())
	if err != nil {
		s.log.Errorf("UpdateProject: %v", err)
		return nil, status.Error(codes.Internal, "failed to update project")
	}

	return &peridotpb.UpdateProjectResponse{
		Project: p.ToProto(),
	}, nil
}

func (s *Server) ListProjects(ctx context.Context, req *peridotpb.ListProjectsRequest) (*peridotpb.ListProjectsResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	resources, err := s.lookupResources(ctx, ObjectProject, PermissionView)
	if err != nil {
		return nil, err
	}
	// Subject doesn't have access to any projects
	if len(resources) == 0 {
		return &peridotpb.ListProjectsResponse{}, nil
	}

	projects, err := s.db.ListProjects(&peridotpb.ProjectFilters{
		Ids: utils.Take[string](resources, "global"),
	})
	if err != nil {
		s.log.Errorf("could not list projects: %v", err)
		return nil, utils.CouldNotRetrieveObjects
	}

	return &peridotpb.ListProjectsResponse{
		Projects: projects.ToProto(),
		Total:    int64(len(projects)),
		Current:  int64(len(projects)),
		Page:     0,
	}, nil
}

func (s *Server) GetProject(ctx context.Context, req *peridotpb.GetProjectRequest) (*peridotpb.GetProjectResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.Id.Value, PermissionView); err != nil {
		return nil, err
	}

	projects, err := s.db.ListProjects(&peridotpb.ProjectFilters{
		Id: wrapperspb.String(req.Id.Value),
	})
	if err != nil {
		return nil, utils.CouldNotFindObject
	}
	if len(projects) == 0 {
		return nil, utils.CouldNotFindObject
	}

	return &peridotpb.GetProjectResponse{
		Project: projects[0].ToProto(),
	}, nil
}

func (s *Server) ListRepositories(ctx context.Context, req *peridotpb.ListRepositoriesRequest) (*peridotpb.ListRepositoriesResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionView); err != nil {
		return nil, err
	}

	repos, err := s.db.FindRepositoriesForProject(req.ProjectId.Value, nil, false)
	if err != nil {
		s.log.Errorf("could not list repositories: %v", err)
		return nil, utils.CouldNotRetrieveObjects
	}

	return &peridotpb.ListRepositoriesResponse{
		Repositories: repos.ToProto(),
	}, nil
}

func (s *Server) GetRepository(ctx context.Context, req *peridotpb.GetRepositoryRequest) (*peridotpb.GetRepositoryResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionView); err != nil {
		return nil, err
	}

	repos, err := s.db.FindRepositoriesForProject(req.ProjectId.Value, &req.Id.Value, false)
	if err != nil {
		s.log.Errorf("could not list repositories: %v", err)
		return nil, utils.CouldNotRetrieveObjects
	}
	if len(repos) == 0 {
		return nil, utils.CouldNotFindObject
	}

	return &peridotpb.GetRepositoryResponse{
		Repository: repos[0].ToProto(),
	}, nil
}

func (s *Server) GetProjectCredentials(ctx context.Context, req *peridotpb.GetProjectCredentialsRequest) (*peridotpb.GetProjectCredentialsResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionManage); err != nil {
		return nil, err
	}

	creds, err := s.db.GetProjectKeys(req.ProjectId.Value)
	if err != nil && err != sql.ErrNoRows {
		s.log.Errorf("could not get project credentials: %v", err)
		return nil, utils.CouldNotRetrieveObjects
	}
	if err == sql.ErrNoRows {
		return &peridotpb.GetProjectCredentialsResponse{}, nil
	}

	return &peridotpb.GetProjectCredentialsResponse{
		GitlabUsername: wrapperspb.String(creds.GitlabUsername),
	}, nil
}

func (s *Server) SetProjectCredentials(ctx context.Context, req *peridotpb.SetProjectCredentialsRequest) (*peridotpb.SetProjectCredentialsResponse, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionManage); err != nil {
		return nil, err
	}

	err := s.db.SetProjectKeys(req.ProjectId.Value, req.GitlabUsername.Value, req.GitlabPassword.Value)
	if err != nil {
		s.log.Errorf("could not set project credentials: %v", err)
		return nil, utils.CouldNotUpdateObject
	}

	return &peridotpb.SetProjectCredentialsResponse{
		GitlabUsername: req.GitlabUsername,
	}, nil
}

func (s *Server) SyncCatalog(ctx context.Context, req *peridotpb.SyncCatalogRequest) (*peridotpb.AsyncTask, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionManage); err != nil {
		return nil, err
	}
	user, err := utils.UserFromContext(ctx)
	if err != nil {
		return nil, err
	}

	rollback := true
	beginTx, err := s.db.Begin()
	if err != nil {
		s.log.Error(err)
		return nil, utils.InternalError
	}
	defer func() {
		if rollback {
			_ = beginTx.Rollback()
		}
	}()
	tx := s.db.UseTransaction(beginTx)

	task, err := tx.CreateTask(user, "noarch", peridotpb.TaskType_TASK_TYPE_SYNC_CATALOG, &req.ProjectId.Value, nil)
	if err != nil {
		s.log.Errorf("could not create task: %v", err)
		return nil, utils.InternalError
	}

	taskProto, err := task.ToProto(false)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "could not marshal task: %v", err)
	}

	rollback = false
	err = beginTx.Commit()
	if err != nil {
		return nil, status.Error(codes.Internal, "could not save, try again")
	}

	_, err = s.temporal.ExecuteWorkflow(
		context.Background(),
		client.StartWorkflowOptions{
			ID:        task.ID.String(),
			TaskQueue: MainTaskQueue,
		},
		s.temporalWorker.WorkflowController.SyncCatalogWorkflow,
		req,
		task,
	)
	if err != nil {
		s.log.Errorf("could not start sync workflow in SyncCatalog: %v", err)
		_ = s.db.SetTaskStatus(task.ID.String(), peridotpb.TaskStatus_TASK_STATUS_FAILED)
		return nil, err
	}

	return &peridotpb.AsyncTask{
		TaskId:   task.ID.String(),
		Subtasks: []*peridotpb.Subtask{taskProto},
		Done:     false,
	}, nil
}

func (s *Server) CreateHashedRepositories(ctx context.Context, req *peridotpb.CreateHashedRepositoriesRequest) (*peridotpb.AsyncTask, error) {
	if err := req.ValidateAll(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectProject, req.ProjectId.Value, PermissionManage); err != nil {
		return nil, err
	}
	user, err := utils.UserFromContext(ctx)
	if err != nil {
		return nil, err
	}

	rollback := true
	beginTx, err := s.db.Begin()
	if err != nil {
		s.log.Error(err)
		return nil, utils.InternalError
	}
	defer func() {
		if rollback {
			_ = beginTx.Rollback()
		}
	}()
	tx := s.db.UseTransaction(beginTx)

	task, err := tx.CreateTask(user, "noarch", peridotpb.TaskType_TASK_TYPE_CREATE_HASHED_REPOSITORIES, &req.ProjectId.Value, nil)
	if err != nil {
		s.log.Errorf("could not create task: %v", err)
		return nil, utils.InternalError
	}

	taskProto, err := task.ToProto(false)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "could not marshal task: %v", err)
	}

	rollback = false
	err = beginTx.Commit()
	if err != nil {
		return nil, status.Error(codes.Internal, "could not save, try again")
	}

	_, err = s.temporal.ExecuteWorkflow(
		context.Background(),
		client.StartWorkflowOptions{
			ID:        task.ID.String(),
			TaskQueue: MainTaskQueue,
		},
		s.temporalWorker.WorkflowController.CreateHashedRepositoriesWorkflow,
		req,
		task,
	)
	if err != nil {
		s.log.Errorf("could not start workflow: %v", err)
		_ = s.db.SetTaskStatus(task.ID.String(), peridotpb.TaskStatus_TASK_STATUS_FAILED)
		return nil, err
	}

	return &peridotpb.AsyncTask{
		TaskId:   task.ID.String(),
		Subtasks: []*peridotpb.Subtask{taskProto},
		Done:     false,
	}, nil
}

func (s *Server) LookasideFileUpload(ctx context.Context, req *peridotpb.LookasideFileUploadRequest) (*peridotpb.LookasideFileUploadResponse, error) {
	if err := req.Validate(); err != nil {
		return nil, err
	}
	if err := s.checkPermission(ctx, ObjectGlobal, ObjectIdPeridot, PermissionManage); err != nil {
		return nil, err
	}

	base64DecodedFile, err := base64.StdEncoding.DecodeString(req.File)
	if err != nil {
		return nil, err
	}

	hasher := sha256.New()
	_, err = hasher.Write(base64DecodedFile)
	if err != nil {
		return nil, err
	}
	sha256Sum := hex.EncodeToString(hasher.Sum(nil))

	exists, err := s.storage.Exists(sha256Sum)
	if err != nil {
		if !strings.Contains(err.Error(), "NotFound") {
			return nil, err
		}
	}
	if exists {
		return &peridotpb.LookasideFileUploadResponse{
			Digest: sha256Sum,
		}, nil
	}

	_, err = s.storage.PutObjectBytes(sha256Sum, base64DecodedFile)
	if err != nil {
		return nil, err
	}

	return &peridotpb.LookasideFileUploadResponse{
		Digest: sha256Sum,
	}, nil
}
