package server

import (
	"context"
	"errors"
	"fmt"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent_configuration"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/managed_resources"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/managed_resources/rpc"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modshared"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/fieldz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/git"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"sigs.k8s.io/yaml"
)

type server struct {
	rpc.UnsafeProvisionerServer
	gitaly              gitaly.PoolInterface
	agentClient         func(agentKey api.AgentKey, rpcAPI modserver.RPCAPI) (dynamic.Interface, error)
	defaultTemplate     *managed_resources.EnvironmentTemplate
	mapper              meta.RESTMapper
	renderer            *managed_resources.Renderer
	fieldManager        string
	maxTemplateFileSize int64
}

func (s *server) GetDefaultEnvironmentTemplate(_ context.Context, _ *rpc.GetDefaultEnvironmentTemplateRequest) (*rpc.GetDefaultEnvironmentTemplateResponse, error) {
	return &rpc.GetDefaultEnvironmentTemplateResponse{
		Template: s.defaultTemplate,
	}, nil
}

func (s *server) ListEnvironmentTemplates(ctx context.Context, r *rpc.ListEnvironmentTemplatesRequest) (*rpc.ListEnvironmentTemplatesResponse, error) {
	pf, err := s.gitaly.PathFetcher(ctx, r.GitalyInfo)
	if err != nil {
		rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
		rpcAPI.HandleProcessingError(rpcAPI.Log(), "PathFetcher", err)
		return nil, status.Errorf(codes.Unavailable, "PathFetcher: %v", err)
	}
	var repoPath string
	if r.AgentName == nil {
		repoPath = agent_configuration.Directory
	} else {
		repoPath = managed_resources.EnvironmentTemplatesDirectoryForAgent(*r.AgentName)
	}
	var v templateVisitor
	err = pf.Visit(
		ctx,
		r.GitalyRepository.ToGitalyRepository(),
		[]byte(git.ExplicitRefOrHead(r.DefaultBranch)),
		[]byte(repoPath),
		true,
		&v,
	)
	if err != nil {
		switch gitaly.ErrorCodeFromError(err) { //nolint:exhaustive
		case gitaly.NotFound:
			return nil, status.Error(codes.NotFound, err.Error())
		default:
			rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
			rpcAPI.HandleProcessingError(rpcAPI.Log(), "Error listing environment templates", err)
			return nil, status.Errorf(codes.Unavailable, "Error listing environment templates: %v", err)
		}
	}
	return &rpc.ListEnvironmentTemplatesResponse{
		Infos: v.infos,
	}, nil
}

func (s *server) GetEnvironmentTemplate(ctx context.Context, r *rpc.GetEnvironmentTemplateRequest) (*rpc.GetEnvironmentTemplateResponse, error) {
	pf, err := s.gitaly.PathFetcher(ctx, r.GitalyInfo)
	if err != nil {
		rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
		rpcAPI.HandleProcessingError(rpcAPI.Log(), "PathFetcher", err)
		return nil, status.Errorf(codes.Unavailable, "PathFetcher: %v", err)
	}
	templateYAML, err := pf.FetchFile(
		ctx,
		r.GitalyRepository.ToGitalyRepository(),
		[]byte(git.ExplicitRefOrHead(r.DefaultBranch)),
		[]byte(managed_resources.EnvironmentTemplateFile(r.AgentName, r.TemplateName)),
		s.maxTemplateFileSize,
	)
	if err != nil {
		switch gitaly.ErrorCodeFromError(err) { //nolint:exhaustive
		case gitaly.NotFound:
			return nil, status.Error(codes.NotFound, err.Error())
		case gitaly.FileTooBig, gitaly.UnexpectedTreeEntryType:
			return nil, status.Error(codes.FailedPrecondition, err.Error())
		default:
			rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
			rpcAPI.HandleProcessingError(rpcAPI.Log(), "Error fetching environment template file", err)
			return nil, status.Errorf(codes.Unavailable, "Error fetching environment template file: %v", err)
		}
	}
	return &rpc.GetEnvironmentTemplateResponse{
		Template: &managed_resources.EnvironmentTemplate{
			Name: r.TemplateName,
			Data: templateYAML,
		},
	}, nil
}

func (s *server) RenderEnvironmentTemplate(ctx context.Context, r *rpc.RenderEnvironmentTemplateRequest) (*rpc.RenderEnvironmentTemplateResponse, error) {
	rendered, err := s.renderer.RenderEnvironmentTemplate(r.Template, r.Info, s.processObject)
	agentKey := api.AgentKey{ID: r.Info.Agent.Id, Type: api.AgentTypeKubernetes}
	if err != nil {
		var re *managed_resources.RendererError
		if errors.As(err, &re) {
			var code codes.Code
			msg := re.Msg
			switch re.Code {
			case managed_resources.InvalidArgument:
				code = codes.InvalidArgument
			case managed_resources.Internal:
				code = codes.Internal
				rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
				rpcAPI.HandleProcessingError(rpcAPI.Log(), "Managed resources render error", err, fieldz.AgentKey(agentKey))
			default:
				code = codes.Unknown
				msg = re.Error() // use the full error from re to get the code printed properly.
				rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
				rpcAPI.HandleProcessingError(rpcAPI.Log(), "Managed resources render error", err, fieldz.AgentKey(agentKey))
			}
			return nil, status.Error(code, msg)
		}
		rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
		rpcAPI.HandleProcessingError(rpcAPI.Log(), "Managed resources render error", err, fieldz.AgentKey(agentKey))
		return nil, status.Error(codes.Internal, err.Error()) // this shouldn't normally happen
	}
	return &rpc.RenderEnvironmentTemplateResponse{
		Template: rendered,
	}, nil
}

func (s *server) EnsureEnvironment(ctx context.Context, r *rpc.EnsureEnvironmentRequest) (respRet *rpc.EnsureEnvironmentResponse, errRet error) {
	var tpl managed_resources.EnvironmentTemplateData[*unstructured.Unstructured]
	err := yaml.UnmarshalStrict(r.Template.Data, &tpl)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "Invalid template: %v", err)
	}

	rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
	agentKey := api.AgentKey{ID: r.Info.Agent.Id, Type: api.AgentTypeKubernetes}
	m, err := s.newObjectManager(rpcAPI, agentKey, r.Info.Project.Id, r.Info.Environment.Slug)
	if err != nil {
		return nil, err // no wrapping
	}
	objsRet, errs := m.Apply(ctx, tpl.Objects)
	return &rpc.EnsureEnvironmentResponse{
		Objects: applied2retObjs(objsRet),
		Errors:  errors2rpcErrors(errs),
	}, nil
}

func (s *server) DeleteEnvironment(ctx context.Context, r *rpc.DeleteEnvironmentRequest) (*rpc.DeleteEnvironmentResponse, error) {
	rpcAPI := modshared.RPCAPIFromContext[modserver.RPCAPI](ctx)
	agentKey := api.AgentKey{ID: r.AgentId, Type: api.AgentTypeKubernetes}
	m, err := s.newObjectManager(rpcAPI, agentKey, r.ProjectId, r.EnvironmentSlug)
	if err != nil {
		return nil, err // no wrapping
	}
	inProgress, errs := m.Delete(ctx, managed_resources.DeleteOpts{
		AgentKey:        api.AgentKey{ID: r.AgentId, Type: api.AgentTypeKubernetes},
		ProjectID:       r.ProjectId,
		EnvironmentSlug: r.EnvironmentSlug,
		Objects:         rpcObjs2objs(r.Objects),
	})
	return &rpc.DeleteEnvironmentResponse{
		InProgress: objs2rpcObjs(inProgress),
		Errors:     errors2rpcErrors(errs),
	}, nil
}

func (s *server) newObjectManager(rpcAPI modserver.RPCAPI, agentKey api.AgentKey, projectID int64, environmentSlug string) (*managed_resources.ObjectManager, error) {
	dc, err := s.agentClient(agentKey, rpcAPI)
	if err != nil { // shouldn't happen
		log := rpcAPI.Log().With(logz.AgentKey(agentKey))
		rpcAPI.HandleProcessingError(log, "AgentClient", err, fieldz.AgentKey(agentKey))
		return nil, status.Error(codes.Internal, err.Error())
	}
	return &managed_resources.ObjectManager{
		Client:   dc,
		Mapper:   s.mapper,
		Renderer: s.renderer,
		// We must use per-environment field managers so that objects that belong to multiple environments are merged
		// by server-side apply. If we don't do this, apply for an environment unsets all the fields of the object
		// set in all other environments (if they are not set in this environment).
		// This includes labels and annotations the renderer adds to identify to which environment(s) the object belongs.
		// "Last one wins" is not the semantics we want, we want corresponding objects, including their annotations and labels, to be merged.
		// Format here somewhat matches renderer's annotation/label key for consistency.
		FieldManager: fmt.Sprintf("%s/%s-%d-%d", s.fieldManager, environmentSlug, projectID, agentKey.ID),
	}, nil
}

func applied2retObjs(objs []*unstructured.Unstructured) []*rpc.Object {
	ret := make([]*rpc.Object, 0, len(objs))
	for _, obj := range objs {
		// If we ever return the object bodies, make sure to erase the data from the Secret objects!!!!
		gvk := obj.GetObjectKind().GroupVersionKind()
		ret = append(ret, &rpc.Object{
			Group:     gvk.Group,
			Version:   gvk.Version,
			Kind:      gvk.Kind,
			Namespace: obj.GetNamespace(),
			Name:      obj.GetName(),
		})
	}
	return ret
}

func objs2rpcObjs(objs []managed_resources.Object) []*rpc.Object {
	ret := make([]*rpc.Object, 0, len(objs))
	for _, obj := range objs {
		ret = append(ret, &rpc.Object{
			Group:     obj.GVK.Group,
			Version:   obj.GVK.Version,
			Kind:      obj.GVK.Kind,
			Namespace: obj.Namespace,
			Name:      obj.Name,
		})
	}
	return ret
}

func rpcObjs2objs(objs []*rpc.Object) []managed_resources.Object {
	ret := make([]managed_resources.Object, 0, len(objs))
	for _, obj := range objs {
		ret = append(ret, managed_resources.Object{
			GVK: schema.GroupVersionKind{
				Group:   obj.Group,
				Version: obj.Version,
				Kind:    obj.Kind,
			},
			Namespace: obj.Namespace,
			Name:      obj.Name,
		})
	}
	return ret
}

func errors2rpcErrors(errs []managed_resources.ObjectError) []*rpc.ObjectError {
	ret := make([]*rpc.ObjectError, 0, len(errs))

	for _, err := range errs {
		ret = append(ret, &rpc.ObjectError{
			Object: &rpc.Object{
				Group:     err.GVK.Group,
				Version:   err.GVK.Version,
				Kind:      err.GVK.Kind,
				Namespace: err.Namespace,
				Name:      err.Name,
			},
			Error: err.Error.Error(),
		})
	}
	return ret
}

func (s *server) processObject(u *unstructured.Unstructured) error {
	gvk := u.GroupVersionKind()
	_, err := s.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		var unsupported *meta.NoKindMatchError
		if errors.As(err, &unsupported) {
			return fmt.Errorf("unsupported object kind %s", gvk)
		}
		return fmt.Errorf("mapper: %w", err)
	}
	return nil
}
