// Package app wires the backend process lifecycle and coordinates component
// startup and graceful shutdown.
package app

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"
)

// Component is a long-lived backend subsystem that participates in coordinated
// startup and graceful shutdown.
type Component interface {
	// Run starts the component and blocks until it stops.
	Run(context.Context) error

	// Shutdown stops the component within the provided timeout-bounded context.
	Shutdown(context.Context) error
}

// App owns the process-level lifecycle of the backend and its registered
// components.
type App struct {
	shutdownTimeout time.Duration
	components      []Component
}

// New constructs an App with the supplied shutdown timeout and a defensive
// copy of the supplied components.
func New(shutdownTimeout time.Duration, components ...Component) *App {
	clonedComponents := append([]Component(nil), components...)

	return &App{
		shutdownTimeout: shutdownTimeout,
		components:      clonedComponents,
	}
}

// Run starts all configured components, waits for cancellation or the first
// component failure, and then executes best-effort graceful shutdown for every
// component.
func (a *App) Run(ctx context.Context) error {
	if ctx == nil {
		return errors.New("run backend app: nil context")
	}
	if err := a.validate(); err != nil {
		return err
	}
	if len(a.components) == 0 {
		<-ctx.Done()
		return nil
	}

	runCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	results := make(chan componentResult, len(a.components))
	var runWG sync.WaitGroup

	for idx, component := range a.components {
		runWG.Add(1)

		go func(index int, component Component) {
			defer runWG.Done()
			results <- componentResult{
				index: index,
				err:   component.Run(runCtx),
			}
		}(idx, component)
	}

	var runErr error

	select {
	case <-ctx.Done():
	case result := <-results:
		runErr = classifyComponentResult(ctx, result)
	}

	cancel()

	shutdownErr := a.shutdownComponents()
	waitErr := a.waitForComponents(&runWG)

	return errors.Join(runErr, shutdownErr, waitErr)
}

// componentResult captures the first observed exit from a running component.
type componentResult struct {
	index int
	err   error
}

// validate confirms that the App has a safe shutdown budget and no nil
// components before goroutines are started.
func (a *App) validate() error {
	if a.shutdownTimeout <= 0 {
		return fmt.Errorf("run backend app: shutdown timeout must be positive, got %s", a.shutdownTimeout)
	}

	for idx, component := range a.components {
		if component == nil {
			return fmt.Errorf("run backend app: component %d is nil", idx)
		}
	}

	return nil
}

// classifyComponentResult maps the first component exit into the error that
// should control the application result.
func classifyComponentResult(parentCtx context.Context, result componentResult) error {
	switch {
	case result.err == nil:
		if parentCtx.Err() != nil {
			return nil
		}
		return fmt.Errorf("run backend app: component %d exited without error before shutdown", result.index)
	case errors.Is(result.err, context.Canceled) && parentCtx.Err() != nil:
		return nil
	default:
		return fmt.Errorf("run backend app: component %d: %w", result.index, result.err)
	}
}

// shutdownComponents calls Shutdown on every registered component using a fresh
// timeout-bounded context per component and joins any shutdown failures.
func (a *App) shutdownComponents() error {
	var shutdownWG sync.WaitGroup
	errs := make(chan error, len(a.components))

	for idx, component := range a.components {
		shutdownWG.Add(1)

		go func(index int, component Component) {
			defer shutdownWG.Done()

			shutdownCtx, cancel := context.WithTimeout(context.Background(), a.shutdownTimeout)
			defer cancel()

			if err := component.Shutdown(shutdownCtx); err != nil {
				errs <- fmt.Errorf("shutdown backend component %d: %w", index, err)
			}
		}(idx, component)
	}

	shutdownWG.Wait()
	close(errs)

	var joined error
	for err := range errs {
		joined = errors.Join(joined, err)
	}

	return joined
}

// waitForComponents waits for running components to return after shutdown and
// reports when they outlive the configured shutdown budget.
func (a *App) waitForComponents(runWG *sync.WaitGroup) error {
	done := make(chan struct{})
	go func() {
		runWG.Wait()
		close(done)
	}()

	waitCtx, cancel := context.WithTimeout(context.Background(), a.shutdownTimeout)
	defer cancel()

	select {
	case <-done:
		return nil
	case <-waitCtx.Done():
		return fmt.Errorf("wait for backend components: %w", waitCtx.Err())
	}
}
