//
// Copyright 2023 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 service

import (
	"context"
	"fmt"

	pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
	"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
	errors "github.com/go-kratos/kratos/v2/errors"
	"github.com/google/uuid"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type IntegrationsService struct {
	pb.UnimplementedIntegrationsServiceServer
	*service

	integrationUC *biz.IntegrationUseCase
	workflowUC    *biz.WorkflowUseCase
	integrations  sdk.AvailablePlugins
}

func NewIntegrationsService(uc *biz.IntegrationUseCase, wuc *biz.WorkflowUseCase, integrations sdk.AvailablePlugins, opts ...NewOpt) *IntegrationsService {
	return &IntegrationsService{
		service:       newService(opts...),
		integrationUC: uc,
		workflowUC:    wuc,
		integrations:  integrations,
	}
}

func (s *IntegrationsService) ListAvailable(_ context.Context, _ *pb.IntegrationsServiceListAvailableRequest) (*pb.IntegrationsServiceListAvailableResponse, error) {
	result := make([]*pb.IntegrationAvailableItem, 0, len(s.integrations))

	for _, i := range s.integrations {
		d := i.Describe()

		var subscribedMaterials = make([]string, 0)
		for _, m := range i.GetSubscribedMaterials() {
			subscribedMaterials = append(subscribedMaterials, m.Type.String())
		}

		item := &pb.IntegrationAvailableItem{
			Name:        d.ID,
			Version:     d.Version,
			Description: d.Description,
			Type: &pb.IntegrationAvailableItem_Fanout{
				Fanout: &pb.PluginFanout{
					AttachmentSchema:    d.AttachmentJSONSchema,
					RegistrationSchema:  d.RegistrationJSONSchema,
					SubscribedMaterials: subscribedMaterials,
				},
			},
		}

		result = append(result, item)
	}

	return &pb.IntegrationsServiceListAvailableResponse{Result: result}, nil
}

func (s *IntegrationsService) Register(ctx context.Context, req *pb.IntegrationsServiceRegisterRequest) (*pb.IntegrationsServiceRegisterResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	// lookup the integration
	integration, err := s.integrations.FindByID(req.PluginId)
	if err != nil {
		return nil, errors.NotFound("not found", err.Error())
	}

	i, err := s.integrationUC.RegisterAndSave(ctx, org.ID, req.Name, req.Description, integration, req.Config)
	if err != nil {
		if biz.IsNotFound(err) {
			return nil, errors.NotFound("not found", err.Error())
		} else if biz.IsErrValidation(err) {
			return nil, errors.BadRequest("wrong validation", err.Error())
		}

		return nil, handleUseCaseErr(err, s.log)
	}

	return &pb.IntegrationsServiceRegisterResponse{Result: bizIntegrationToPb(i)}, nil
}

func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsServiceAttachRequest) (*pb.IntegrationsServiceAttachResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	// Find the integration and workflow by name
	integration, err := s.integrationUC.FindByNameInOrg(ctx, org.ID, req.IntegrationName)
	if err != nil {
		if biz.IsNotFound(err) {
			return nil, errors.NotFound("not found", err.Error())
		}
		return nil, handleUseCaseErr(err, s.log)
	}

	// lookup the integration
	attachable, err := s.integrations.FindByID(integration.Kind)
	if err != nil {
		return nil, fmt.Errorf("loading integration: %w", err)
	}

	wf, err := s.workflowUC.FindByNameInOrg(ctx, org.ID, req.ProjectName, req.WorkflowName)
	if err != nil {
		if biz.IsNotFound(err) {
			return nil, errors.NotFound("not found", err.Error())
		}
		return nil, handleUseCaseErr(err, s.log)
	}

	// Apply RBAC if needed
	if err = s.authorizeResource(ctx, authz.PolicyAttachedIntegrationAttach, authz.ResourceTypeProject, wf.ProjectID); err != nil {
		return nil, err
	}

	res, err := s.integrationUC.AttachToWorkflow(ctx, &biz.AttachOpts{
		OrgID: org.ID, IntegrationID: integration.ID.String(), WorkflowID: wf.ID.String(),
		AttachmentConfig:  req.Config,
		FanOutIntegration: attachable,
	})

	if err != nil {
		if biz.IsNotFound(err) {
			return nil, errors.NotFound("not found", err.Error())
		} else if biz.IsErrValidation(err) {
			return nil, errors.BadRequest("wrong validation", err.Error())
		}

		return nil, handleUseCaseErr(err, s.log)
	}

	result, err := s.bizIntegrationAttachmentToPb(ctx, res, org.ID)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	return &pb.IntegrationsServiceAttachResponse{Result: result}, nil
}

func (s *IntegrationsService) ListRegistrations(ctx context.Context, _ *pb.IntegrationsServiceListRegistrationsRequest) (*pb.IntegrationsServiceListRegistrationsResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	integrations, err := s.integrationUC.List(ctx, org.ID)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	result := make([]*pb.RegisteredIntegrationItem, 0, len(integrations))
	for _, i := range integrations {
		result = append(result, bizIntegrationToPb(i))
	}

	return &pb.IntegrationsServiceListRegistrationsResponse{Result: result}, nil
}

func (s *IntegrationsService) DescribeRegistration(ctx context.Context, req *pb.IntegrationsServiceDescribeRegistrationRequest) (*pb.IntegrationsServiceDescribeRegistrationResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	i, err := s.integrationUC.FindByNameInOrg(ctx, org.ID, req.Name)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if i == nil {
		return nil, errors.NotFound("not found", "integration not found")
	}

	return &pb.IntegrationsServiceDescribeRegistrationResponse{Result: bizIntegrationToPb(i)}, nil
}

func (s *IntegrationsService) Deregister(ctx context.Context, req *pb.IntegrationsServiceDeregisterRequest) (*pb.IntegrationsServiceDeregisterResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	integration, err := s.integrationUC.FindByNameInOrg(ctx, org.ID, req.Name)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if integration == nil {
		return nil, errors.NotFound("not found", "integration not found")
	}

	err = s.integrationUC.Delete(ctx, org.ID, integration.ID.String())
	if err != nil && biz.IsNotFound(err) {
		return nil, errors.NotFound("not found", err.Error())
	} else if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	return &pb.IntegrationsServiceDeregisterResponse{}, nil
}

func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListAttachmentsRequest) (*pb.ListAttachmentsResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	// Translate workflow name to ID
	opts := &biz.ListAttachmentsOpts{}
	if req.GetWorkflowName() != "" {
		wf, err := s.workflowUC.FindByNameInOrg(ctx, org.ID, req.GetProjectName(), req.GetWorkflowName())
		if err != nil {
			if biz.IsNotFound(err) {
				return nil, errors.NotFound("not found", err.Error())
			}
			return nil, handleUseCaseErr(err, s.log)
		}
		opts.WorkflowID = &wf.ID
	}

	// apply RBAC if needed
	opts.ProjectIDs = s.visibleProjects(ctx)

	integrations, err := s.integrationUC.ListAttachments(ctx, org.ID, opts)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	result := make([]*pb.IntegrationAttachmentItem, 0, len(integrations))
	for _, i := range integrations {
		r, err := s.bizIntegrationAttachmentToPb(ctx, i.IntegrationAttachment, org.ID)
		if err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}
		result = append(result, r)
	}

	return &pb.ListAttachmentsResponse{Result: result}, nil
}

func (s *IntegrationsService) Detach(ctx context.Context, req *pb.IntegrationsServiceDetachRequest) (*pb.IntegrationsServiceDetachResponse, error) {
	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, err
	}

	// find the project it belongs to
	orgID, err := uuid.Parse(org.ID)
	if err != nil {
		return nil, errors.BadRequest("bad request", "invalid organization")
	}
	attID, err := uuid.Parse(req.Id)
	if err != nil {
		return nil, errors.BadRequest("bad request", "invalid integration attachment")
	}

	att, err := s.integrationUC.GetAttachment(ctx, orgID, attID)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	wf, err := s.workflowUC.FindByIDInOrg(ctx, org.ID, att.WorkflowID.String())
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Apply RBAC
	if err = s.authorizeResource(ctx, authz.PolicyAttachedIntegrationDetach, authz.ResourceTypeProject, wf.ProjectID); err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	if err := s.integrationUC.Detach(ctx, org.ID, req.Id); err != nil {
		if biz.IsNotFound(err) {
			return nil, errors.NotFound("not found", err.Error())
		}

		return nil, handleUseCaseErr(err, s.log)
	}

	return &pb.IntegrationsServiceDetachResponse{}, nil
}

func bizIntegrationToPb(e *biz.Integration) *pb.RegisteredIntegrationItem {
	return &pb.RegisteredIntegrationItem{
		Id:          e.ID.String(),
		Name:        e.Name,
		CreatedAt:   timestamppb.New(*e.CreatedAt),
		Description: e.Description,
		Kind:        e.Kind, Config: e.Config,
	}
}

func (s *IntegrationsService) bizIntegrationAttachmentToPb(ctx context.Context, e *biz.IntegrationAttachment, orgID string) (*pb.IntegrationAttachmentItem, error) {
	a := &pb.IntegrationAttachmentItem{
		Id: e.ID.String(), CreatedAt: timestamppb.New(*e.CreatedAt),
		Config: e.Config,
	}

	i, err := s.integrationUC.FindByIDInOrg(ctx, orgID, e.IntegrationID.String())
	if err != nil {
		return nil, err
	} else if i != nil {
		a.Integration = bizIntegrationToPb(i)
	}

	wf, err := s.workflowUC.FindByIDInOrg(ctx, orgID, e.WorkflowID.String())
	if err != nil {
		return nil, err
	} else if wf != nil {
		a.Workflow = bizWorkflowToPb(wf)
	}

	return a, nil
}
