// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package taskrunner

import (
	"context"
	"errors"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"sync"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/nomad/client/allocdir"
	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
	"github.com/hashicorp/nomad/client/config"
	"github.com/hashicorp/nomad/helper/users"
)

// apiHook exposes the Task API. The Task API allows task's to access the Nomad
// HTTP API without having to discover and connect to an agent's address.
// Instead a unix socket is provided in a standard location. To prevent access
// by untrusted workloads the Task API always requires authentication even when
// ACLs are disabled.
//
// The Task API hook largely soft-fails as there are a number of ways creating
// the unix socket could fail (the most common one being path length
// restrictions), and it is assumed most tasks won't require access to the Task
// API anyway. Tasks that do require access are expected to crash and get
// rescheduled should they land on a client who Task API hook soft-fails.
type apiHook struct {
	shutdownCtx context.Context
	srv         config.APIListenerRegistrar
	logger      hclog.Logger

	// Lock listener as it is updated from multiple hooks.
	lock sync.Mutex

	// Listener is the unix domain socket of the task api for this taks.
	ln net.Listener
}

func newAPIHook(shutdownCtx context.Context, srv config.APIListenerRegistrar, logger hclog.Logger) *apiHook {
	h := &apiHook{
		shutdownCtx: shutdownCtx,
		srv:         srv,
	}
	h.logger = logger.Named(h.Name())
	return h
}

func (*apiHook) Name() string {
	return "api"
}

func (h *apiHook) Prestart(_ context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error {
	h.lock.Lock()
	defer h.lock.Unlock()

	if h.ln != nil {
		// Listener already set. Task is probably restarting.
		return nil
	}

	udsPath := apiSocketPath(req.TaskDir)
	udsln, err := users.SocketFileFor(h.logger, udsPath, req.Task.User)
	if err != nil {
		// Soft-fail and let the task fail if it requires the task api.
		h.logger.Warn("error creating task api socket", "path", udsPath, "error", err)
		return nil
	}

	go func() {
		// Cannot use Prestart's context as it is closed after all prestart hooks
		// have been closed, but we do want to try to cleanup on shutdown.
		if err := h.srv.Serve(h.shutdownCtx, udsln); err != nil {
			if errors.Is(err, http.ErrServerClosed) {
				return
			}
			if errors.Is(err, net.ErrClosed) {
				return
			}
			h.logger.Error("error serving task api", "error", err)
		}
	}()

	h.ln = udsln
	return nil
}

func (h *apiHook) Stop(ctx context.Context, req *interfaces.TaskStopRequest, resp *interfaces.TaskStopResponse) error {
	h.lock.Lock()
	defer h.lock.Unlock()

	if h.ln != nil {
		if err := h.ln.Close(); err != nil {
			if !errors.Is(err, net.ErrClosed) {
				h.logger.Debug("error closing task listener: %v", err)
			}
		}
		h.ln = nil
	}

	// Best-effort at cleaining things up. Alloc dir cleanup will remove it if
	// this fails for any reason.
	_ = os.RemoveAll(apiSocketPath(req.TaskDir))

	return nil
}

// apiSocketPath returns the path to the Task API socket.
//
// The path needs to be as short as possible because of the low limits on the
// sun_path char array imposed by the syscall used to create unix sockets.
//
// See https://github.com/hashicorp/nomad/pull/13971 for an example of the
// sadness this causes.
func apiSocketPath(taskDir *allocdir.TaskDir) string {
	return filepath.Join(taskDir.SecretsDir, "api.sock")
}
