# frozen_string_literal: true

module Gitlab
  module Kas
    class Client
      JWT_AUDIENCE = 'gitlab-kas'

      STUB_CLASSES = {
        server_info: Gitlab::Agent::ServerInfo::Rpc::ServerInfo::Stub,
        agent_tracker: Gitlab::Agent::AgentTracker::Rpc::AgentTracker::Stub,
        configuration_project: Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub,
        autoflow: Gitlab::Agent::AutoFlow::Rpc::AutoFlow::Stub,
        notifications: Gitlab::Agent::Notifications::Rpc::Notifications::Stub,
        managed_resources: Gitlab::Agent::ManagedResources::Rpc::Provisioner::Stub
      }.freeze

      AUTOFLOW_CI_VARIABLE_ENV_SCOPE = 'autoflow/internal-use'

      ConfigurationError = Class.new(StandardError)

      def initialize
        raise ConfigurationError, 'GitLab KAS is not enabled' unless Gitlab::Kas.enabled?
        raise ConfigurationError, 'KAS internal URL is not configured' unless Gitlab::Kas.internal_url.present?
      end

      # Return GitLab KAS server info
      # This method only returns information about a single KAS server instance without taking into account
      # that there are potentially multiple KAS replicas running, which may not have the same server info.
      # This is particularly the case during a rollout.
      def get_server_info
        request = Gitlab::Agent::ServerInfo::Rpc::GetServerInfoRequest.new

        stub_for(:server_info)
          .get_server_info(request, metadata: metadata)
          .current_server_info
      end

      def get_connected_agentks_by_agent_ids(agent_ids:)
        request = Gitlab::Agent::AgentTracker::Rpc::GetConnectedAgentksByAgentIDsRequest.new(agent_ids: agent_ids)

        stub_for(:agent_tracker)
         .get_connected_agentks_by_agent_i_ds(request, metadata: metadata)
         .agents
         .to_a
      end

      def list_agent_config_files(project:)
        request = Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest.new(
          repository: repository(project),
          gitaly_info: gitaly_info(project)
        )

        stub_for(:configuration_project)
          .list_agent_config_files(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(project),
            group: ::Feature::Kas.group_actor(project)
          ))
          .config_files
          .to_a
      end

      def send_git_push_event(project:)
        request = Gitlab::Agent::Notifications::Rpc::GitPushEventRequest.new(
          event: Gitlab::Agent::Event::GitPushEvent.new(
            project: Gitlab::Agent::Event::Project.new(
              id: project.id,
              full_path: project.full_path
            )
          )
        )

        stub_for(:notifications)
          .git_push_event(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(project),
            group: ::Feature::Kas.group_actor(project)
          ))
      end

      def send_autoflow_event(project:, type:, id:, data:)
        # We only want to send events if AutoFlow is enabled and no-op otherwise
        return unless Feature.enabled?(:autoflow_enabled, project)

        # retrieve all AutoFlow-relevant variables
        variables = project.variables.by_environment_scope(AUTOFLOW_CI_VARIABLE_ENV_SCOPE)

        project_proto = Gitlab::Agent::Event::Project.new(
          id: project.id,
          full_path: project.full_path
        )
        request = Gitlab::Agent::AutoFlow::Rpc::CloudEventRequest.new(
          event: Gitlab::Agent::Event::CloudEvent.new(
            id: id,
            source: "GitLab",
            spec_version: "v1",
            type: type,
            attributes: {
              datacontenttype: Gitlab::Agent::Event::CloudEvent::CloudEventAttributeValue.new(
                ce_string: "application/json"
              )
            },
            text_data: data.to_json
          ),
          flow_project: project_proto,
          variables: variables.to_h { |v| [v.key, v.value] }
        )

        stub_for(:autoflow)
          .cloud_event(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(project),
            group: ::Feature::Kas.group_actor(project)
          ))
      end

      def get_environment_template(agent:, template_name:)
        project = agent.project

        request = Gitlab::Agent::ManagedResources::Rpc::GetEnvironmentTemplateRequest.new(
          template_name: template_name,
          agent_name: agent.name,
          gitaly_info: gitaly_info(project),
          gitaly_repository: repository(project),
          default_branch: project.default_branch_or_main
        )

        stub_for(:managed_resources)
          .get_environment_template(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(project),
            group: ::Feature::Kas.group_actor(project)
          ))
          .template
      end

      def get_default_environment_template
        request = Gitlab::Agent::ManagedResources::Rpc::GetDefaultEnvironmentTemplateRequest.new
        stub_for(:managed_resources)
          .get_default_environment_template(request, metadata: metadata)
          .template
      end

      def render_environment_template(template:, environment:, build:)
        request = Gitlab::Agent::ManagedResources::Rpc::RenderEnvironmentTemplateRequest.new(
          template: Gitlab::Agent::ManagedResources::EnvironmentTemplate.new(
            name: template.name,
            data: template.data),
          info: templating_info(environment:, build:))
        stub_for(:managed_resources)
          .render_environment_template(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(environment.project),
            group: ::Feature::Kas.group_actor(environment.project)
          ))
          .template
      end

      def ensure_environment(template:, environment:, build:)
        request = Gitlab::Agent::ManagedResources::Rpc::EnsureEnvironmentRequest.new(
          template: Gitlab::Agent::ManagedResources::RenderedEnvironmentTemplate.new(
            name: template.name,
            data: template.data),
          info: templating_info(environment:, build:))
        stub_for(:managed_resources)
          .ensure_environment(request, metadata: metadata(
            project: ::Feature::Kas.project_actor(environment.project),
            group: ::Feature::Kas.group_actor(environment.project)
          ))
      end

      def delete_environment(managed_resource:)
        request = ::Gitlab::Agent::ManagedResources::Rpc::DeleteEnvironmentRequest.new(
          agent_id: managed_resource.cluster_agent_id,
          project_id: managed_resource.project_id,
          environment_slug: managed_resource.environment.slug,
          objects: managed_resource.tracked_objects
        )

        stub_for(:managed_resources).delete_environment(request, metadata: metadata(
          project: ::Feature::Kas.project_actor(managed_resource.project),
          group: ::Feature::Kas.group_actor(managed_resource.project)
        ))
      end

      private

      def stub_for(service)
        @stubs ||= {}
        @stubs[service] ||= STUB_CLASSES.fetch(service).new(kas_endpoint_url, credentials, timeout: timeout)
      end

      def repository(project)
        gitaly_repository = project.repository.gitaly_repository

        Gitlab::Agent::Entity::GitalyRepository.new(gitaly_repository.to_h)
      end

      def gitaly_info(project)
        gitaly_features = Feature::Gitaly.server_feature_flags
        connection_data = Gitlab::GitalyClient.connection_data(project.repository_storage)
          .merge(features: gitaly_features)

        Gitlab::Agent::Entity::GitalyInfo.new(connection_data)
      end

      def kas_endpoint_url
        Gitlab::Kas.internal_url.sub(%r{^grpcs?://}, '')
      end

      def credentials
        if URI(Gitlab::Kas.internal_url).scheme == 'grpcs'
          GRPC::Core::ChannelCredentials.new(::Gitlab::X509::Certificate.ca_certs_bundle)
        else
          :this_channel_is_insecure
        end
      end

      def metadata(**feature_flag_actors)
        {
          'authorization' => "bearer #{token}",
          **::Feature::Kas.server_feature_flags_for_grpc_request(**feature_flag_actors)
        }
      end

      def token
        JSONWebToken::HMACToken.new(Gitlab::Kas.secret).tap do |token|
          token.issuer = Settings.gitlab.host
          token.audience = JWT_AUDIENCE
        end.encoded
      end

      def timeout
        Gitlab::Kas.client_timeout_seconds.seconds
      end

      def templating_info(environment:, build:)
        agent = environment.cluster_agent
        project = environment.project
        return unless agent && project && build && build.user

        # compatible with Gitlab::Kubernetes::DefaultNamespace
        suffix = "-#{project.id}-#{environment.slug}"
        sanitized_project_path = Gitlab::NamespaceSanitizer.sanitize(project.path.downcase).first(63 - suffix.length)
        legacy_namespace = "#{sanitized_project_path}#{suffix}"

        Gitlab::Agent::ManagedResources::TemplatingInfo.new(
          agent: Gitlab::Agent::ManagedResources::Agent.new(
            id: agent.id,
            name: agent.name,
            url: agent_url(agent.project, agent.name)),
          environment: Gitlab::Agent::ManagedResources::Environment.new(
            id: environment.id,
            name: environment.name,
            slug: environment.slug,
            page_url: environment_url(project, environment),
            url: environment.external_url,
            tier: environment.tier),
          project: Gitlab::Agent::ManagedResources::Project.new(
            id: project.id,
            slug: project.path,
            path: project.full_path,
            url: project.web_url),
          pipeline: Gitlab::Agent::ManagedResources::Pipeline.new(id: build.pipeline_id),
          job: Gitlab::Agent::ManagedResources::Job.new(id: build.id),
          user: Gitlab::Agent::ManagedResources::User.new(id: build.user_id, username: build.user.username),
          legacy_namespace: legacy_namespace
        )
      end

      def agent_url(project, agent_name)
        Gitlab::Routing.url_helpers.project_cluster_agent_url(project, agent_name)
      end

      def environment_url(project, environment)
        Gitlab::Routing.url_helpers.project_environment_url(project, environment)
      end
    end
  end
end
