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
	serverName            string
	serverVia             string
	ready                 func()
}

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,
		m.handleProcessingError,
		m.serverName,
		m.serverVia,
	)

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

	m.ready()
	return srv.Run(ctx, lis)
}

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