package server

import (
	"context"
	"log/slog"
	"net"
	"net/url"
	"time"

	"buf.build/go/protovalidate"
	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/redis/rueidis"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab"
	gapi "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/workspaces"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/retry"
	"golang.org/x/oauth2"
	"google.golang.org/grpc"
)

const (
	idleTimeout       = 1 * time.Minute
	readHeaderTimeout = 10 * time.Second
)

type module struct {
	log                   *slog.Logger
	listener              func(context.Context) (net.Listener, error)
	gitLabClient          gitlab.ClientInterface
	pollConfig            retry.PollConfigFactory
	gitlabExternalURL     string
	listenerGracePeriod   time.Duration
	shutdownTimeout       time.Duration
	agentConnPool         func(agentKey api.AgentKey) grpc.ClientConnInterface
	redisClient           rueidis.Client
	redisPrefix           string
	handleProcessingError func(string, error)
	validator             protovalidate.Validator
}

func (m *module) Run(ctx context.Context) error {
	lis, err := m.listener(ctx)
	if err != nil {
		return err
	}

	// Error is ignored because m.httpServer.Run() closes the listener and a second close always produces an error.
	defer lis.Close() //nolint:errcheck

	m.log.Info("Workspaces HTTP server is up",
		logz.NetNetworkFromAddr(lis.Addr()),
		logz.NetAddressFromAddr(lis.Addr()),
	)

	var srvCfg *gapi.GetWorkspacesServerConfigResponse
	var apiExternalURL *url.URL
	err = retry.PollWithBackoff(ctx, m.pollConfig(), func(ctx context.Context) (error, retry.AttemptResult) { //nolint:staticcheck
		srvCfg, err = gapi.GetWorkspacesServerConfig(ctx, m.gitLabClient)
		if err != nil {
			if !errz.ContextDone(err) {
				m.log.Error("Failed to get workspaces server config", logz.Error(err))
			}
			return nil, retry.Backoff
		}
		apiExternalURL, err = url.Parse(srvCfg.ApiExternalUrl)
		if err != nil {
			m.log.Error("Failed to parse oauth redirect url", logz.Error(err))
			return nil, retry.Backoff //nolint:nilerr
		}
		return nil, retry.Done
	})
	if err != nil { // This can only be a context error, which we don't need to return
		return nil
	}

	var oidcCfg *gapi.GetOpenIDConfigurationResponse
	err = retry.PollWithBackoff(ctx, m.pollConfig(), func(ctx context.Context) (error, retry.AttemptResult) { //nolint:staticcheck
		oidcCfg, err = gapi.GetOpenIDConfiguration(ctx, m.gitLabClient)
		if err != nil {
			if !errz.ContextDone(err) {
				m.log.Error("Failed to get openid configuration", logz.Error(err))
			}
			return nil, retry.Backoff
		}
		return nil, retry.Done
	})
	if err != nil { // This can only be a context error, which we don't need to return
		return nil
	}

	providerCfg := &oidc.ProviderConfig{
		IssuerURL:     oidcCfg.Issuer,
		AuthURL:       oidcCfg.AuthorizationEndpoint,
		TokenURL:      oidcCfg.TokenEndpoint,
		DeviceAuthURL: oidcCfg.DeviceAuthorizationEndpoint,
		UserInfoURL:   oidcCfg.UserinfoEndpoint,
		JWKSURL:       oidcCfg.JwksUri,
		Algorithms:    oidcCfg.IdTokenSigningAlgValuesSupported,
	}
	provider := providerCfg.NewProvider(ctx)

	oauthConfig := &oauth2.Config{
		ClientID:    srvCfg.OauthClientId,
		RedirectURL: srvCfg.OauthRedirectUrl,
		Endpoint:    provider.Endpoint(),
		Scopes:      []string{oidc.ScopeOpenID},
	}

	auth := newAuthHandler(
		m.log,
		m.gitLabClient,
		provider,
		oauthConfig,
		m.redisClient,
		m.redisPrefix,
		m.handleProcessingError,
		m.validator,
	)

	tunnel := newTunnelHandler(
		m.log,
		m.gitLabClient,
		m.agentConnPool,
	)

	srv := &httpServer{
		log:                 m.log,
		authHandler:         auth,
		tunnelHandler:       tunnel,
		idleTimeout:         idleTimeout,
		listenerGracePeriod: m.listenerGracePeriod,
		readHeaderTimeout:   readHeaderTimeout,
		shutdownTimeout:     m.shutdownTimeout,
		apiExternalURL:      apiExternalURL,
	}

	return srv.Run(ctx, lis)
}

func (m *module) Name() string {
	return workspaces.ModuleName
}
