package service

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/cmd"
	"github.com/1Panel-dev/1Panel/agent/utils/compose"
	dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/1Panel-dev/1Panel/agent/utils/re"
	"github.com/mattn/go-shellwords"
	"github.com/subosito/gotenv"
)

const (
	nginxModuleBuildDynamic = "dynamic"
	nginxModuleBuildStatic  = "static"

	nginxModuleProviderLocal = "local"

	nginxModuleStatusPending = "pending"
	nginxModuleStatusReady   = "ready"
	nginxModuleStatusFailed  = "failed"

	nginxModuleLoadEnabled  = "enabled"
	nginxModuleLoadDisabled = "disabled"

	nginxModuleOperateCreate = "create"
	nginxModuleOperateUpdate = "update"
	nginxModuleOperateDelete = "delete"

	nginxModuleBuildDir           = "build"
	nginxModuleModulesDir         = "modules"
	nginxModuleConfDir            = "conf"
	nginxModuleTmpDir             = "tmp"
	nginxModuleEnabledConfDir     = "modules-enabled"
	nginxModuleStagingDir         = ".staging"
	nginxModuleLibDir             = "lib"
	nginxModuleBuilderFile        = "Dockerfile.modules"
	nginxModuleStoreFile          = "module.json"
	nginxModuleCatalogFile        = "module.catalog.json"
	nginxModuleCatalogPendingFile = "module.catalog.pending.json"
	nginxModulePreScriptFile      = "module-pre.sh"
	nginxModuleConfigArgsFile     = "module-config.args"
	nginxModuleStaticPreScript    = "pre.sh"
	nginxModuleManifestFile       = "manifest.json"

	nginxModuleContainerRoot = "/usr/local/openresty/nginx/modules/1panel"
	nginxModuleConfigPrefix  = "1panel-module-"

	nginxModuleStaticBuildHint = "; switch the module to static build mode to compile it with the full image"
)

var errNginxModuleBuilderMissing = errors.New("dynamic module builder not found")

type nginxModuleBuildSpec struct {
	Install   model.AppInstall
	Module    dto.NginxModule
	Target    dto.NginxModuleTarget
	BuildPath string
	Force     bool
	Mirror    string
	Task      *task.Task
}

// nginxModuleArtifactProvider keeps local builds replaceable by a future
// precompiled-artifact resolver without changing module state or API contracts.
type nginxModuleArtifactProvider interface {
	Name() string
	Resolve(spec nginxModuleBuildSpec) (dto.NginxModuleBuild, error)
}

type localNginxModuleProvider struct{}

type nginxModuleState struct {
	Name      string                 `json:"name"`
	Custom    bool                   `json:"custom,omitempty"`
	Script    string                 `json:"script,omitempty"`
	Packages  []string               `json:"packages,omitempty"`
	Params    string                 `json:"params,omitempty"`
	Enable    bool                   `json:"enable"`
	BuildMode string                 `json:"buildMode,omitempty"`
	Provider  string                 `json:"provider,omitempty"`
	LoadOrder int                    `json:"loadOrder,omitempty"`
	Builds    []dto.NginxModuleBuild `json:"builds,omitempty"`
	LastError string                 `json:"lastError,omitempty"`
}

func (localNginxModuleProvider) Name() string {
	return nginxModuleProviderLocal
}

func resolveNginxModuleTarget(install model.AppInstall) (dto.NginxModuleTarget, string, error) {
	target, warning, err := resolveNginxRuntimeTarget(install)
	if err != nil {
		return target, warning, err
	}
	builderPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleBuilderFile)
	builderContent, err := os.ReadFile(builderPath)
	if err != nil {
		return target, warning, fmt.Errorf("%w: %v", errNginxModuleBuilderMissing, err)
	}
	builderSum := sha256.Sum256(builderContent)
	target.BuilderDigest = hex.EncodeToString(builderSum[:])
	setNginxModuleTargetKey(&target)
	return target, warning, nil
}

func resolveNginxRuntimeTarget(install model.AppInstall) (dto.NginxModuleTarget, string, error) {
	target := dto.NginxModuleTarget{
		OpenRestyVersion: install.Version,
		Architecture:     runtime.GOARCH,
	}
	envContent, _ := os.ReadFile(install.GetEnvPath())
	images, imageErr := dockerUtils.GetImagesFromDockerCompose(envContent, []byte(install.DockerCompose))
	if imageErr != nil || len(images) == 0 {
		images, imageErr = compose.GetComposeImages(install.GetComposePath())
	}
	var warning string
	if imageErr == nil && len(images) > 0 {
		target.Image = images[0]
		inspectMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
		inspectOut, inspectErr := inspectMgr.RunWithStdout("docker", "image", "inspect", "--format={{.Id}}\t{{.Architecture}}", target.Image)
		if inspectErr != nil {
			warning = fmt.Sprintf("inspect OpenResty image %s failed, module rebuilds will fall back to host architecture %s: %v", target.Image, target.Architecture, inspectErr)
		} else if fields := strings.Fields(inspectOut); len(fields) >= 2 {
			target.ImageDigest = fields[0]
			target.Architecture = fields[1]
		}
	}
	setNginxModuleTargetKey(&target)
	return target, warning, nil
}

func setNginxModuleTargetKey(target *dto.NginxModuleTarget) {
	keyInput := strings.Join([]string{target.OpenRestyVersion, target.Architecture, target.ImageDigest, target.BuilderDigest}, "\x00")
	keySum := sha256.Sum256([]byte(keyInput))
	target.Key = fmt.Sprintf("%s-%s-%s", sanitizeModulePathPart(target.OpenRestyVersion), target.Architecture, hex.EncodeToString(keySum[:6]))
}

// nginxModuleDynamicSupported reports whether the installed version ships both
// the dynamic module builder and the module catalog.
func nginxModuleDynamicSupported(install model.AppInstall) bool {
	fileOp := files.NewFileOp()
	buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
	return fileOp.Stat(path.Join(buildPath, nginxModuleBuilderFile)) &&
		fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
}

func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
	sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
	targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
	if _, err := os.Stat(sourcePath); err != nil {
		if os.IsNotExist(err) {
			if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) {
				return removeErr
			}
			return nil
		}
		return err
	}
	return files.NewFileOp().CopyFile(sourcePath, installBuildDir)
}

// resolveNginxModuleBuildMirror picks the apt mirror for module builds: the
// explicit request value wins, otherwise the install's saved
// CONTAINER_PACKAGE_URL.
func resolveNginxModuleBuildMirror(install model.AppInstall, mirror string) string {
	if mirror != "" {
		return mirror
	}
	envs, err := gotenv.Read(install.GetEnvPath())
	if err != nil {
		return ""
	}
	return strings.TrimSpace(envs["CONTAINER_PACKAGE_URL"])
}

func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModule, selected []string, force bool, mirror string, catalogPath string, parentTask *task.Task) ([]dto.NginxModule, error) {
	// Skip target resolution entirely when nothing needs a dynamic build, so
	// installs without dynamic modules do not require Dockerfile.modules.
	if !hasDynamicNginxModuleBuildTask(modules, selected) {
		return modules, nil
	}
	target, targetWarning, err := resolveNginxModuleTarget(install)
	if err != nil {
		return modules, err
	}
	if targetWarning != "" {
		parentTask.Logf("WARNING: %s", targetWarning)
	}
	originalModules := cloneNginxModules(modules)
	selectedNames := make(map[string]struct{}, len(selected))
	for _, name := range selected {
		selectedNames[name] = struct{}{}
	}
	buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
	buildMirror := resolveNginxModuleBuildMirror(install, mirror)
	for i := range modules {
		module := &modules[i]
		normalizeNginxModule(module)
		if !nginxModuleNeedsDynamicBuild(*module, selectedNames) {
			continue
		}
		provider, providerErr := getNginxModuleProvider(module.Provider)
		if providerErr != nil {
			return modules, providerErr
		}
		previousBuild := findCurrentNginxModuleBuild(*module, target)
		build, buildErr := provider.Resolve(nginxModuleBuildSpec{
			Install: install, Module: *module, Target: target, BuildPath: buildPath, Force: force, Mirror: buildMirror, Task: parentTask,
		})
		if buildErr != nil {
			build.Provider = provider.Name()
			build.Status = nginxModuleStatusFailed
			build.Target = target
			build.Error = buildErr.Error()
			build.BuiltAt = time.Now()
			failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
			removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
			_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
			return failedModules, fmt.Errorf("build dynamic module %s: %w%s", module.Name, buildErr, nginxModuleStaticBuildErrorHint(*module))
		}
		err = validateNginxModuleArtifacts(install, build.Artifacts)
		if err != nil {
			for outputDir := range nginxModuleOutputDirectories(install, []dto.NginxModule{{Builds: []dto.NginxModuleBuild{build}}}) {
				_ = os.RemoveAll(outputDir)
			}
			build.Status = nginxModuleStatusFailed
			build.Error = err.Error()
			build.Artifacts = nil
			build.BuiltAt = time.Now()
			failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
			removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
			_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
			return failedModules, fmt.Errorf("validate dynamic module %s: %w%s", module.Name, err, nginxModuleStaticBuildErrorHint(*module))
		}
		module.LastError = ""
		upsertNginxModuleBuild(module, build)
	}
	return modules, nil
}

func (localNginxModuleProvider) Resolve(spec nginxModuleBuildSpec) (dto.NginxModuleBuild, error) {
	params, err := normalizeDynamicModuleParams(spec.Module.Params)
	if err != nil {
		return dto.NginxModuleBuild{}, err
	}
	configureArgs, err := parseDynamicModuleParams(params)
	if err != nil {
		return dto.NginxModuleBuild{}, err
	}
	if err = validateNginxModulePackages(spec.Module.Packages); err != nil {
		return dto.NginxModuleBuild{}, err
	}
	buildHash, err := nginxModuleBuildHash(spec.Module, spec.Target, params)
	if err != nil {
		return dto.NginxModuleBuild{}, err
	}
	result := dto.NginxModuleBuild{
		Provider:  nginxModuleProviderLocal,
		BuildMode: nginxModuleBuildDynamic,
		Status:    nginxModuleStatusPending,
		Hash:      buildHash,
		Target:    spec.Target,
	}
	modulesRoot := path.Join(spec.Install.GetPath(), nginxModuleModulesDir)
	if !spec.Force {
		if current := findCurrentNginxModuleBuild(spec.Module, spec.Target); current != nil && current.Status == nginxModuleStatusReady {
			if validateNginxModuleArtifacts(spec.Install, current.Artifacts) == nil {
				return *current, nil
			}
		}
	}
	outputRevision := buildHash
	if spec.Force {
		outputRevision = fmt.Sprintf("%s-r%d", buildHash, time.Now().UnixNano())
	}
	modulePathName := nginxModulePathName(spec.Module.Name)
	finalPath := path.Join(modulesRoot, spec.Target.Key, modulePathName, outputRevision)
	buildComplete := false
	defer func() {
		if !buildComplete {
			_ = os.RemoveAll(finalPath)
		}
	}()

	preScriptPath := path.Join(spec.BuildPath, nginxModuleTmpDir, nginxModulePreScriptFile)
	if err = os.WriteFile(preScriptPath, []byte("#!/bin/bash\nset -e\n"+spec.Module.Script+"\n"), constant.FilePerm); err != nil {
		return result, err
	}
	defer os.Remove(preScriptPath)
	configureArgsPath := path.Join(spec.BuildPath, nginxModuleTmpDir, nginxModuleConfigArgsFile)
	if err = os.WriteFile(configureArgsPath, []byte(strings.Join(configureArgs, "\n")+"\n"), constant.FilePerm); err != nil {
		return result, err
	}
	defer os.Remove(configureArgsPath)

	shortHash := buildHash
	if len(shortHash) > 12 {
		shortHash = shortHash[:12]
	}
	taskSuffix := sanitizeModulePathPart(spec.Task.TaskID)
	if len(taskSuffix) > 8 {
		taskSuffix = taskSuffix[:8]
	}
	tempImage := fmt.Sprintf("1panel/openresty-module-builder:%s-%s-%s", modulePathName, shortHash, taskSuffix)
	tempContainer := fmt.Sprintf("1panel-module-%s-%s-%s", modulePathName, shortHash, taskSuffix)
	commandMgr := cmd.NewCommandMgr(cmd.WithTask(*spec.Task), cmd.WithTimeout(120*time.Minute))
	buildArgs := []string{
		"build", "--target", "module-output",
		"-f", path.Join(spec.BuildPath, nginxModuleBuilderFile),
		"-t", tempImage,
		"--build-arg", "PANEL_OPENRESTY_VERSION=" + spec.Install.Version,
		"--build-arg", "RESTY_ADD_PACKAGE_BUILDDEPS=" + strings.Join(spec.Module.Packages, " "),
	}
	if spec.Mirror != "" {
		buildArgs = append(buildArgs, "--build-arg", "CONTAINER_PACKAGE_URL="+spec.Mirror)
	}
	buildArgs = append(buildArgs, spec.BuildPath)
	if err = commandMgr.Run("docker", buildArgs...); err != nil {
		return result, err
	}
	cleanupMgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
	defer func() {
		_ = cleanupMgr.Run("docker", "rm", "-f", tempContainer)
		_ = cleanupMgr.Run("docker", "image", "rm", "-f", tempImage)
	}()
	if err = commandMgr.Run("docker", "create", "--name", tempContainer, tempImage, "/bin/true"); err != nil {
		return result, err
	}

	stagingPath := path.Join(spec.Install.GetPath(), nginxModuleModulesDir, nginxModuleStagingDir, modulePathName+"-"+shortHash)
	_ = os.RemoveAll(stagingPath)
	if err = os.MkdirAll(stagingPath, constant.DirPerm); err != nil {
		return result, err
	}
	defer os.RemoveAll(stagingPath)
	if err = commandMgr.Run("docker", "cp", tempContainer+":/out/.", stagingPath); err != nil {
		return result, err
	}
	if artifacts, artifactErr := collectNginxModuleArtifacts(stagingPath, stagingPath); artifactErr != nil || len(artifacts) == 0 {
		if artifactErr != nil {
			return result, artifactErr
		}
		return result, errors.New("dynamic module build produced no loadable .so files")
	}
	if err = os.MkdirAll(path.Dir(finalPath), constant.DirPerm); err != nil {
		return result, err
	}
	_ = os.RemoveAll(finalPath)
	if err = os.Rename(stagingPath, finalPath); err != nil {
		return result, err
	}
	artifacts, err := collectNginxModuleArtifacts(finalPath, modulesRoot)
	if err != nil {
		return result, err
	}
	result.Status = nginxModuleStatusReady
	result.Artifacts = artifacts
	result.BuiltAt = time.Now()
	manifest, _ := json.MarshalIndent(result, "", "  ")
	_ = os.WriteFile(path.Join(finalPath, nginxModuleManifestFile), manifest, constant.FilePerm)
	buildComplete = true
	return result, nil
}

func commitNginxModuleBuilds(install model.AppInstall, previousModules, modules []dto.NginxModule, reload bool, catalogPath string) error {
	if err := saveNginxModulesWithCatalog(install, modules, catalogPath); err != nil {
		removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
		return err
	}
	if err := reconcileDynamicNginxModuleConfig(install, modules, reload); err != nil {
		rollbackModules := recordNginxModuleActivationFailure(previousModules, modules, err)
		_ = saveNginxModulesWithCatalog(install, rollbackModules, catalogPath)
		removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
		return err
	}
	removeNginxModuleOutputsNotReferenced(install, previousModules, modules)
	return nil
}

func validateNginxModuleArtifacts(install model.AppInstall, artifacts []dto.NginxModuleArtifact) error {
	if len(artifacts) == 0 {
		return errors.New("module build produced no loadable artifacts")
	}
	modulesRoot := filepath.Clean(path.Join(install.GetPath(), nginxModuleModulesDir))
	seen := make(map[string]struct{}, len(artifacts))
	for _, artifact := range artifacts {
		if _, ok := seen[artifact.Path]; ok {
			return fmt.Errorf("duplicate module artifact path %q", artifact.Path)
		}
		seen[artifact.Path] = struct{}{}
		if artifact.Name != path.Base(artifact.Path) {
			return fmt.Errorf("module artifact name %q does not match path %q", artifact.Name, artifact.Path)
		}
		if !re.IsValidNginxModuleChecksum(artifact.Checksum) {
			return fmt.Errorf("invalid checksum for module artifact %q", artifact.Path)
		}
		if !re.IsValidNginxModuleArtifact(artifact.Path) {
			return fmt.Errorf("invalid module artifact path %q", artifact.Path)
		}
		artifactFullPath := filepath.Clean(filepath.Join(modulesRoot, filepath.FromSlash(artifact.Path)))
		if !strings.HasPrefix(artifactFullPath, modulesRoot+string(os.PathSeparator)) {
			return fmt.Errorf("module artifact path %q escapes the module directory", artifact.Path)
		}
		info, err := os.Lstat(artifactFullPath)
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return fmt.Errorf("module artifact %q is not a regular file", artifact.Path)
		}
		file, err := os.Open(artifactFullPath)
		if err != nil {
			return err
		}
		hash := sha256.New()
		_, copyErr := io.Copy(hash, file)
		closeErr := file.Close()
		if copyErr != nil {
			return copyErr
		}
		if closeErr != nil {
			return closeErr
		}
		actualChecksum := hex.EncodeToString(hash.Sum(nil))
		if !strings.EqualFold(actualChecksum, artifact.Checksum) {
			return fmt.Errorf("checksum mismatch for module artifact %q", artifact.Path)
		}
	}
	return nil
}

func collectNginxModuleArtifacts(root, relativeRoot string) ([]dto.NginxModuleArtifact, error) {
	var artifacts []dto.NginxModuleArtifact
	err := filepath.Walk(root, func(filePath string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if info.IsDir() || filepath.Ext(info.Name()) != ".so" || filepath.Base(filepath.Dir(filePath)) == nginxModuleLibDir {
			return nil
		}
		file, err := os.Open(filePath)
		if err != nil {
			return err
		}
		hash := sha256.New()
		_, copyErr := io.Copy(hash, file)
		_ = file.Close()
		if copyErr != nil {
			return copyErr
		}
		relativePath, err := filepath.Rel(relativeRoot, filePath)
		if err != nil {
			return err
		}
		artifacts = append(artifacts, dto.NginxModuleArtifact{
			Name: info.Name(), Path: filepath.ToSlash(relativePath), Checksum: hex.EncodeToString(hash.Sum(nil)),
		})
		return nil
	})
	sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
	return artifacts, err
}

func validateNginxModuleLoadConfig(install model.AppInstall, target dto.NginxModuleTarget, validationName, loadDirectives string) error {
	if target.Image == "" {
		return errors.New("target OpenResty image is not resolved")
	}
	var config strings.Builder
	config.WriteString(loadDirectives)
	config.WriteString("events {}\nhttp {}\n")
	configSum := sha256.Sum256([]byte(config.String()))
	testConfig := path.Join(install.GetPath(), nginxModuleModulesDir, ".test-"+nginxModulePathName(validationName)+"-"+hex.EncodeToString(configSum[:6])+".conf")
	if err := os.WriteFile(testConfig, []byte(config.String()), constant.FilePerm); err != nil {
		return err
	}
	defer os.Remove(testConfig)
	modulesRoot := path.Join(install.GetPath(), nginxModuleModulesDir)
	args := []string{
		"run", "--rm", "--network", "none",
		"-v", modulesRoot + ":" + nginxModuleContainerRoot + ":ro",
		"-v", testConfig + ":/tmp/1panel-module-test.conf:ro",
		"--entrypoint", "/usr/local/openresty/nginx/sbin/nginx",
		target.Image, "-t", "-c", "/tmp/1panel-module-test.conf",
	}
	return cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).Run("docker", args...)
}

type nginxModuleConfigSnapshot map[string][]byte

type openrestyUpgradeSnapshot struct {
	installPath string
	backupPath  string
	existing    map[string]bool
}

var openrestyUpgradeSnapshotPaths = []string{
	nginxModuleBuildDir,
	nginxModuleModulesDir,
	"scripts",
	path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
	path.Join(nginxModuleConfDir, "nginx.conf"),
	"docker-compose.yml",
	".env",
}

func createOpenrestyUpgradeSnapshot(installPath string) (*openrestyUpgradeSnapshot, error) {
	backupPath, err := os.MkdirTemp("", "1panel-openresty-upgrade-*")
	if err != nil {
		return nil, err
	}
	snapshot := &openrestyUpgradeSnapshot{
		installPath: installPath,
		backupPath:  backupPath,
		existing:    make(map[string]bool, len(openrestyUpgradeSnapshotPaths)),
	}
	for _, relativePath := range openrestyUpgradeSnapshotPaths {
		sourcePath := path.Join(installPath, relativePath)
		if _, err = os.Stat(sourcePath); err != nil {
			if os.IsNotExist(err) {
				continue
			}
			snapshot.Cleanup()
			return nil, err
		}
		snapshot.existing[relativePath] = true
		if err = copyOpenrestyUpgradeSnapshotEntry(sourcePath, path.Join(backupPath, relativePath)); err != nil {
			snapshot.Cleanup()
			return nil, err
		}
	}
	return snapshot, nil
}

func copyOpenrestyUpgradeSnapshotEntry(sourcePath, targetPath string) error {
	if err := os.MkdirAll(path.Dir(targetPath), constant.DirPerm); err != nil {
		return err
	}
	info, err := os.Stat(sourcePath)
	if err != nil {
		return err
	}
	fileOp := files.NewFileOp()
	if info.IsDir() {
		return fileOp.CopyDir(sourcePath, path.Dir(targetPath))
	}
	return fileOp.CopyFile(sourcePath, path.Dir(targetPath))
}

func (s *openrestyUpgradeSnapshot) Restore() error {
	for _, relativePath := range openrestyUpgradeSnapshotPaths {
		targetPath := path.Join(s.installPath, relativePath)
		if err := os.RemoveAll(targetPath); err != nil {
			return err
		}
		if !s.existing[relativePath] {
			continue
		}
		if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), targetPath); err != nil {
			return err
		}
	}
	return nil
}

func (s *openrestyUpgradeSnapshot) Cleanup() {
	if s != nil && s.backupPath != "" {
		_ = os.RemoveAll(s.backupPath)
	}
}

func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.NginxModule, reload bool) error {
	var target dto.NginxModuleTarget
	if hasDynamicNginxModuleBuildTask(modules, nil) {
		var targetWarning string
		var err error
		target, targetWarning, err = resolveNginxModuleTarget(install)
		if err != nil {
			return err
		}
		if targetWarning != "" {
			global.LOG.Warn(targetWarning)
		}
	}
	configDir := path.Join(install.GetPath(), nginxModuleConfDir, nginxModuleEnabledConfDir)
	if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
		return err
	}
	snapshot, err := snapshotManagedNginxModuleConfigs(configDir)
	if err != nil {
		return err
	}
	desired := make(map[string][]byte)
	var combinedLoadDirectives strings.Builder
	sortedModules := append([]dto.NginxModule(nil), modules...)
	sort.SliceStable(sortedModules, func(i, j int) bool {
		if sortedModules[i].LoadOrder == sortedModules[j].LoadOrder {
			return sortedModules[i].Name < sortedModules[j].Name
		}
		return sortedModules[i].LoadOrder < sortedModules[j].LoadOrder
	})
	for _, module := range sortedModules {
		normalizeNginxModule(&module)
		if !module.Enable || module.BuildMode == nginxModuleBuildStatic {
			continue
		}
		build := findCurrentNginxModuleBuild(module, target)
		if build == nil || build.Status != nginxModuleStatusReady {
			build = findLatestNginxModuleBuild(module, target)
		}
		if build == nil || build.Status != nginxModuleStatusReady {
			continue
		}
		// Artifacts were already validated after the build; re-validating here
		// is a deliberate guard against on-disk tampering before writing
		// load_module directives.
		if err = validateNginxModuleArtifacts(install, build.Artifacts); err != nil {
			return fmt.Errorf("validate artifacts for dynamic module %s: %w", module.Name, err)
		}
		var content strings.Builder
		content.WriteString("# Managed by 1Panel. Manual changes will be overwritten.\n")
		for _, artifact := range build.Artifacts {
			content.WriteString("load_module ")
			content.WriteString(path.Join(nginxModuleContainerRoot, artifact.Path))
			content.WriteString(";\n")
			combinedLoadDirectives.WriteString("load_module ")
			combinedLoadDirectives.WriteString(path.Join(nginxModuleContainerRoot, artifact.Path))
			combinedLoadDirectives.WriteString(";\n")
		}
		fileName := fmt.Sprintf("%s%04d-%s.conf", nginxModuleConfigPrefix, module.LoadOrder, nginxModulePathName(module.Name))
		desired[fileName] = []byte(content.String())
	}
	if combinedLoadDirectives.Len() > 0 {
		if err = validateNginxModuleLoadConfig(install, target, "combined", combinedLoadDirectives.String()); err != nil {
			return fmt.Errorf("validate combined dynamic module configuration: %w", err)
		}
	}
	if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
		_ = applyManagedNginxModuleConfigs(configDir, snapshot)
		return err
	}
	if !reload {
		return nil
	}
	status, statusErr := checkContainerStatus(install.ContainerName)
	if statusErr != nil || status != "running" {
		return nil
	}
	if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
		_ = applyManagedNginxModuleConfigs(configDir, snapshot)
		return err
	}
	if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
		_ = applyManagedNginxModuleConfigs(configDir, snapshot)
		return err
	}
	return nil
}

func snapshotManagedNginxModuleConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
	snapshot := make(nginxModuleConfigSnapshot)
	entries, err := os.ReadDir(configDir)
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxModuleConfigPrefix) {
			continue
		}
		content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
		if readErr != nil {
			return nil, readErr
		}
		snapshot[entry.Name()] = content
	}
	return snapshot, nil
}

func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte) error {
	entries, err := os.ReadDir(configDir)
	if err != nil {
		return err
	}
	for fileName, content := range desired {
		tmpPath := path.Join(configDir, "."+fileName+".tmp")
		if err = os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
			return err
		}
		if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
			return err
		}
	}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxModuleConfigPrefix) {
			continue
		}
		if _, ok := desired[entry.Name()]; !ok {
			if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
				return err
			}
		}
	}
	return nil
}

func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
	for _, module := range modules {
		normalizeNginxModule(&module)
		if module.Enable && module.BuildMode == nginxModuleBuildStatic {
			return true
		}
	}
	return false
}

func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
	if hasEnabledStaticNginxModules(modules) {
		return true
	}
	envs, err := gotenv.Read(install.GetEnvPath())
	if err != nil {
		return false
	}
	return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
}

func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
	buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
	var params, packages []string
	preScriptPath := path.Join(buildPath, nginxModuleTmpDir, nginxModuleStaticPreScript)
	preScript, err := os.OpenFile(preScriptPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
	if err != nil {
		return err
	}
	for _, module := range modules {
		normalizeNginxModule(&module)
		if !module.Enable || module.BuildMode != nginxModuleBuildStatic {
			continue
		}
		if _, err = preScript.WriteString(module.Script + "\n"); err != nil {
			_ = preScript.Close()
			return err
		}
		params = append(params, module.Params)
		packages = append(packages, module.Packages...)
	}
	if err = preScript.Close(); err != nil {
		return err
	}
	envs, err := gotenv.Read(install.GetEnvPath())
	if err != nil {
		return err
	}
	if mirror != "" {
		envs["CONTAINER_PACKAGE_URL"] = mirror
	}
	envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(compactStrings(params), " ")
	envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(compactStrings(packages), " ")
	return gotenv.Write(envs, install.GetEnvPath())
}

func executeStaticNginxModuleBuild(install model.AppInstall, modules []dto.NginxModule, mirror string, force bool, parentTask *task.Task) error {
	if err := configureStaticNginxModules(install, modules, mirror); err != nil {
		return err
	}
	commandMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute))
	if err := commandMgr.Run("docker", "compose", "-f", install.GetComposePath(), "build"); err != nil {
		return err
	}
	previousModules := cloneNginxModules(modules)
	// A rebuilt runtime changes the target ABI, so every enabled dynamic module
	// must be rebuilt even when the user selected only one module.
	modules, err := buildDynamicNginxModules(install, modules, nil, force, mirror, "", parentTask)
	if err != nil {
		return err
	}
	if err = commitNginxModuleBuilds(install, previousModules, modules, false, ""); err != nil {
		return err
	}
	if _, err = compose.DownAndUp(install.GetComposePath()); err != nil {
		return err
	}
	return commitStaticNginxModuleBuilds(install, "", parentTask)
}

func executeNginxModuleBuild(install model.AppInstall, reqModules []string, force bool, mirror string, parentTask *task.Task, reload bool) error {
	modules, err := loadNginxModules(install)
	if err != nil {
		return err
	}
	staticBuild := staticNginxBuildRequired(install, modules)
	if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
		if !nginxModuleDynamicSupported(install) {
			return errors.New("the installed OpenResty version does not support dynamic module builds")
		}
	}
	if staticBuild {
		return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
	}
	previousModules := cloneNginxModules(modules)
	modules, err = buildDynamicNginxModules(install, modules, reqModules, force, mirror, "", parentTask)
	if err != nil {
		return err
	}
	return commitNginxModuleBuilds(install, previousModules, modules, reload, "")
}

func removeNginxModuleArtifacts(install model.AppInstall, module dto.NginxModule) error {
	for _, build := range module.Builds {
		moduleDir := path.Join(install.GetPath(), nginxModuleModulesDir, build.Target.Key, nginxModulePathName(module.Name))
		if err := files.NewFileOp().DeleteDir(moduleDir); err != nil && !os.IsNotExist(err) {
			return err
		}
	}
	return nil
}

func loadNginxModules(install model.AppInstall) ([]dto.NginxModule, error) {
	return loadNginxModulesWithCatalog(install, "")
}

func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) ([]dto.NginxModule, error) {
	modulePath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile)
	states, err := readNginxModuleStateFile(modulePath)
	if err != nil {
		return nil, err
	}
	if catalogPath == "" {
		catalogPath = path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
	}
	modules, err := readNginxModuleFile(catalogPath)
	if err != nil {
		return nil, err
	}
	moduleIndexes := make(map[string]int, len(modules))
	for i := range modules {
		modules[i].Custom = false
		modules[i].Enable = false
		modules[i].Builds = nil
		modules[i].LastError = ""
		if _, exists := moduleIndexes[modules[i].Name]; exists {
			return nil, fmt.Errorf("duplicate OpenResty module catalog name %q", modules[i].Name)
		}
		moduleIndexes[modules[i].Name] = i
	}
	stateNames := make(map[string]struct{}, len(states))
	for _, state := range states {
		if _, exists := stateNames[state.Name]; exists {
			return nil, fmt.Errorf("duplicate OpenResty module state name %q", state.Name)
		}
		stateNames[state.Name] = struct{}{}
		if index, ok := moduleIndexes[state.Name]; ok {
			if state.Custom {
				return nil, fmt.Errorf("custom OpenResty module %s conflicts with the module catalog", state.Name)
			}
			modules[index].Enable = state.Enable
			modules[index].Builds = state.Builds
			modules[index].LastError = state.LastError
			continue
		}
		if !state.Custom {
			return nil, fmt.Errorf("OpenResty module state %s is missing from the module catalog", state.Name)
		}
		modules = append(modules, dto.NginxModule{
			Name: state.Name, Custom: true, Script: state.Script, Packages: state.Packages, Params: state.Params,
			Enable: state.Enable, BuildMode: state.BuildMode, Provider: state.Provider, LoadOrder: state.LoadOrder,
			Builds: state.Builds, LastError: state.LastError,
		})
	}
	for i := range modules {
		if err = validateNginxModuleBuildMode(modules[i]); err != nil {
			return nil, err
		}
		normalizeNginxModule(&modules[i])
	}
	return modules, nil
}

func readNginxModuleStateFile(filePath string) ([]nginxModuleState, error) {
	content, err := os.ReadFile(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			return []nginxModuleState{}, nil
		}
		return nil, err
	}
	if len(strings.TrimSpace(string(content))) == 0 {
		return []nginxModuleState{}, nil
	}
	var states []nginxModuleState
	if err = json.Unmarshal(content, &states); err != nil {
		return nil, fmt.Errorf("parse OpenResty module state %s: %w", filePath, err)
	}
	return states, nil
}

func readNginxModuleFile(filePath string) ([]dto.NginxModule, error) {
	content, err := os.ReadFile(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			return []dto.NginxModule{}, nil
		}
		return nil, err
	}
	if len(strings.TrimSpace(string(content))) == 0 {
		return []dto.NginxModule{}, nil
	}
	var modules []dto.NginxModule
	if err = json.Unmarshal(content, &modules); err != nil {
		return nil, fmt.Errorf("parse OpenResty module configuration %s: %w", filePath, err)
	}
	return modules, nil
}

func saveNginxModules(install model.AppInstall, modules []dto.NginxModule) error {
	return saveNginxModulesWithCatalog(install, modules, "")
}

func saveNginxModulesWithCatalog(install model.AppInstall, modules []dto.NginxModule, catalogPath string) error {
	if catalogPath == "" {
		catalogPath = path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
	}
	catalog, err := readNginxModuleFile(catalogPath)
	if err != nil {
		return err
	}
	catalogNames := make(map[string]struct{}, len(catalog))
	for _, module := range catalog {
		if _, exists := catalogNames[module.Name]; exists {
			return fmt.Errorf("duplicate OpenResty module catalog name %q", module.Name)
		}
		catalogNames[module.Name] = struct{}{}
	}
	states := make([]nginxModuleState, 0, len(modules))
	for i := range modules {
		module := &modules[i]
		if err = validateNginxModuleBuildMode(*module); err != nil {
			return err
		}
		normalizeNginxModule(module)
		if _, builtin := catalogNames[module.Name]; builtin {
			if module.Custom {
				return fmt.Errorf("custom OpenResty module %s conflicts with the module catalog", module.Name)
			}
			if !module.Enable && len(module.Builds) == 0 && module.LastError == "" {
				continue
			}
			states = append(states, nginxModuleState{
				Name: module.Name, Enable: module.Enable, Builds: module.Builds, LastError: module.LastError,
			})
			continue
		}
		if !module.Custom {
			return fmt.Errorf("OpenResty module %s is missing from the module catalog", module.Name)
		}
		states = append(states, nginxModuleState{
			Name: module.Name, Custom: true, Script: module.Script, Packages: module.Packages, Params: module.Params,
			Enable: module.Enable, BuildMode: module.BuildMode, Provider: module.Provider, LoadOrder: module.LoadOrder,
			Builds: module.Builds, LastError: module.LastError,
		})
	}
	content, err := json.MarshalIndent(states, "", "  ")
	if err != nil {
		return err
	}
	modulePath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile)
	if err = os.MkdirAll(path.Dir(modulePath), constant.DirPerm); err != nil {
		return err
	}
	tmpPath := modulePath + ".tmp"
	if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
		return err
	}
	return os.Rename(tmpPath, modulePath)
}

func activateNginxModuleCatalog(pendingPath, activePath string) error {
	return os.Rename(pendingPath, activePath)
}

func activateNginxModuleCatalogAndCommit(pendingPath, activePath string, commit func() error) error {
	previous, err := os.ReadFile(activePath)
	if err != nil {
		return err
	}
	if err = activateNginxModuleCatalog(pendingPath, activePath); err != nil {
		return err
	}
	if err = commit(); err == nil {
		return nil
	}
	if restoreErr := writeNginxModuleCatalog(activePath, previous); restoreErr != nil {
		return fmt.Errorf("%w; restore previous OpenResty module catalog: %v", err, restoreErr)
	}
	return err
}

func stageNginxModuleCatalog(sourcePath, pendingPath string) error {
	content, err := os.ReadFile(sourcePath)
	if err != nil {
		return err
	}
	return writeNginxModuleCatalog(pendingPath, content)
}

func writeNginxModuleCatalog(targetPath string, content []byte) error {
	tmpPath := targetPath + ".tmp"
	defer func() { _ = os.Remove(tmpPath) }()
	if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
		return err
	}
	return os.Rename(tmpPath, targetPath)
}

func normalizeNginxModule(module *dto.NginxModule) {
	if module.Provider == "" {
		module.Provider = nginxModuleProviderLocal
	}
	module.Packages = compactStrings(module.Packages)
}

func validateNginxModuleBuildMode(module dto.NginxModule) error {
	if module.BuildMode != nginxModuleBuildDynamic && module.BuildMode != nginxModuleBuildStatic {
		return fmt.Errorf("OpenResty module %s has invalid build mode %q", module.Name, module.BuildMode)
	}
	return nil
}

func compactStrings(items []string) []string {
	result := make([]string, 0, len(items))
	seen := make(map[string]struct{})
	for _, item := range items {
		item = strings.TrimSpace(item)
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		result = append(result, item)
	}
	return result
}

func nginxModuleStaticBuildErrorHint(module dto.NginxModule) string {
	if module.Custom {
		return nginxModuleStaticBuildHint
	}
	return ""
}

// nginxModuleNeedsDynamicBuild mirrors the dynamic-build filter of
// buildDynamicNginxModules. It normalizes a copy so prescan callers never
// mutate the stored entities.
func nginxModuleNeedsDynamicBuild(module dto.NginxModule, selectedNames map[string]struct{}) bool {
	normalizeNginxModule(&module)
	if module.BuildMode != nginxModuleBuildDynamic {
		return false
	}
	if len(selectedNames) > 0 {
		_, ok := selectedNames[module.Name]
		return ok
	}
	return module.Enable
}

func hasDynamicNginxModuleBuildTask(modules []dto.NginxModule, selected []string) bool {
	selectedNames := make(map[string]struct{}, len(selected))
	for _, name := range selected {
		selectedNames[name] = struct{}{}
	}
	for _, module := range modules {
		if nginxModuleNeedsDynamicBuild(module, selectedNames) {
			return true
		}
	}
	return false
}

func findLatestNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
	var latest *dto.NginxModuleBuild
	for i := range module.Builds {
		if module.Builds[i].BuildMode != module.BuildMode ||
			module.Builds[i].Target.Key != target.Key ||
			module.Builds[i].Status != nginxModuleStatusReady {
			continue
		}
		if latest == nil || module.Builds[i].BuiltAt.After(latest.BuiltAt) {
			latest = &module.Builds[i]
		}
	}
	return latest
}

func findCurrentNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
	params, err := nginxModuleBuildParams(module)
	if err != nil {
		return nil
	}
	buildHash, err := nginxModuleBuildHash(module, target, params)
	if err != nil {
		return nil
	}
	for i := range module.Builds {
		if module.Builds[i].BuildMode == module.BuildMode &&
			module.Builds[i].Target.Key == target.Key &&
			module.Builds[i].Hash == buildHash {
			return &module.Builds[i]
		}
	}
	return nil
}

func nginxModuleBuildParams(module dto.NginxModule) (string, error) {
	if module.BuildMode == nginxModuleBuildStatic {
		params := strings.TrimSpace(module.Params)
		if params == "" {
			return "", errors.New("static module parameters are empty")
		}
		return params, nil
	}
	return normalizeDynamicModuleParams(module.Params)
}

func recordStaticNginxModuleBuilds(modules []dto.NginxModule, target dto.NginxModuleTarget) ([]dto.NginxModule, error) {
	for i := range modules {
		module := &modules[i]
		normalizeNginxModule(module)
		if !module.Enable || module.BuildMode != nginxModuleBuildStatic {
			continue
		}
		params, err := nginxModuleBuildParams(*module)
		if err != nil {
			return modules, err
		}
		buildHash, err := nginxModuleBuildHash(*module, target, params)
		if err != nil {
			return modules, err
		}
		upsertNginxModuleBuild(module, dto.NginxModuleBuild{
			Provider:  nginxModuleProviderLocal,
			BuildMode: nginxModuleBuildStatic,
			Status:    nginxModuleStatusReady,
			Hash:      buildHash,
			Target:    target,
			BuiltAt:   time.Now(),
		})
		module.LastError = ""
	}
	return modules, nil
}

func commitStaticNginxModuleBuilds(install model.AppInstall, catalogPath string, parentTask *task.Task) error {
	modules, err := loadNginxModulesWithCatalog(install, catalogPath)
	if err != nil || !hasEnabledStaticNginxModules(modules) {
		return err
	}
	status, err := checkContainerStatus(install.ContainerName)
	if err != nil {
		return err
	}
	if status != "running" {
		return fmt.Errorf("OpenResty container %s is not running after static module build", install.ContainerName)
	}
	if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
		return err
	}
	target, targetWarning, err := resolveNginxRuntimeTarget(install)
	if err != nil {
		return err
	}
	if targetWarning != "" && parentTask != nil {
		parentTask.Logf("WARNING: %s", targetWarning)
	}
	modules, err = recordStaticNginxModuleBuilds(modules, target)
	if err != nil {
		return err
	}
	return saveNginxModulesWithCatalog(install, modules, catalogPath)
}

func upsertNginxModuleBuild(module *dto.NginxModule, build dto.NginxModuleBuild) {
	for i := range module.Builds {
		if module.Builds[i].Target.Key == build.Target.Key && module.Builds[i].Hash == build.Hash {
			module.Builds[i] = build
			return
		}
	}
	module.Builds = append(module.Builds, build)
}

func cloneNginxModules(modules []dto.NginxModule) []dto.NginxModule {
	cloned := make([]dto.NginxModule, len(modules))
	copy(cloned, modules)
	for i := range cloned {
		cloned[i].Packages = append([]string(nil), modules[i].Packages...)
		cloned[i].Builds = make([]dto.NginxModuleBuild, len(modules[i].Builds))
		copy(cloned[i].Builds, modules[i].Builds)
		for j := range cloned[i].Builds {
			cloned[i].Builds[j].Artifacts = append([]dto.NginxModuleArtifact(nil), modules[i].Builds[j].Artifacts...)
		}
	}
	return cloned
}

func recordNginxModuleBuildFailure(originalModules []dto.NginxModule, moduleName string, build dto.NginxModuleBuild, previousBuild *dto.NginxModuleBuild) []dto.NginxModule {
	failedModules := cloneNginxModules(originalModules)
	for i := range failedModules {
		if failedModules[i].Name != moduleName {
			continue
		}
		failedModules[i].LastError = build.Error
		if previousBuild == nil || previousBuild.Status != nginxModuleStatusReady {
			upsertNginxModuleBuild(&failedModules[i], build)
		}
		break
	}
	return failedModules
}

func recordNginxModuleActivationFailure(previousModules, candidateModules []dto.NginxModule, activationErr error) []dto.NginxModule {
	rollbackModules := cloneNginxModules(previousModules)
	candidates := make(map[string]dto.NginxModule, len(candidateModules))
	for _, module := range candidateModules {
		candidates[module.Name] = module
	}
	for i := range rollbackModules {
		candidate, ok := candidates[rollbackModules[i].Name]
		if !ok || !candidate.Enable || candidate.BuildMode == nginxModuleBuildStatic {
			continue
		}
		rollbackModules[i].LastError = activationErr.Error()
	}
	return rollbackModules
}

func normalizeDynamicModuleParams(params string) (string, error) {
	params = strings.TrimSpace(params)
	params = strings.ReplaceAll(params, "--add-module=", "--add-dynamic-module=")
	if !strings.Contains(params, "--add-dynamic-module=") && !strings.Contains(params, "=dynamic") {
		return "", errors.New("module does not declare a dynamic configure option")
	}
	if _, err := parseDynamicModuleParams(params); err != nil {
		return "", err
	}
	return params, nil
}

func parseDynamicModuleParams(params string) ([]string, error) {
	// shellwords silently stops at unquoted shell metacharacters instead of
	// reporting them, so reject them on the raw input before parsing.
	if strings.ContainsAny(params, "\x00\r\n;&|<>") {
		return nil, errors.New("dynamic module parameters contain unsupported characters")
	}
	parser := shellwords.NewParser()
	parser.ParseBacktick = false
	parser.ParseEnv = false
	args, err := parser.Parse(params)
	if err != nil {
		return nil, fmt.Errorf("parse dynamic module parameters: %w", err)
	}
	if len(args) == 0 {
		return nil, errors.New("dynamic module parameters are empty")
	}
	for _, arg := range args {
		if !strings.HasPrefix(arg, "--") {
			return nil, fmt.Errorf("unsupported configure argument %q", arg)
		}
		if strings.ContainsAny(arg, "\x00\r\n;&|<>") {
			return nil, fmt.Errorf("configure argument %q contains unsupported characters", arg)
		}
	}
	return args, nil
}

func validateNginxModulePackages(packages []string) error {
	for _, item := range packages {
		if !re.IsValidNginxModulePackage(item) {
			return fmt.Errorf("invalid build package %q", item)
		}
	}
	return nil
}

func nginxModuleBuildHash(module dto.NginxModule, target dto.NginxModuleTarget, params string) (string, error) {
	payload := struct {
		Name      string
		Script    string
		Packages  []string
		Params    string
		BuildMode string
		TargetKey string
		Provider  string
	}{module.Name, module.Script, module.Packages, params, module.BuildMode, target.Key, module.Provider}
	content, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256(content)
	return hex.EncodeToString(sum[:]), nil
}

func getNginxModuleProvider(name string) (nginxModuleArtifactProvider, error) {
	switch name {
	case "", nginxModuleProviderLocal:
		return localNginxModuleProvider{}, nil
	case "prebuilt":
		return nil, errors.New("prebuilt module provider is not configured; use the local provider")
	default:
		return nil, fmt.Errorf("unknown module artifact provider %q", name)
	}
}

func nginxModuleOutputDirectories(install model.AppInstall, modules []dto.NginxModule) map[string]struct{} {
	result := make(map[string]struct{})
	modulesRoot := filepath.Clean(path.Join(install.GetPath(), nginxModuleModulesDir))
	for _, module := range modules {
		for _, build := range module.Builds {
			for _, artifact := range build.Artifacts {
				if !re.IsValidNginxModuleArtifact(artifact.Path) {
					continue
				}
				artifactFullPath := filepath.Clean(filepath.Join(modulesRoot, filepath.FromSlash(artifact.Path)))
				if !strings.HasPrefix(artifactFullPath, modulesRoot+string(os.PathSeparator)) {
					continue
				}
				outputDir := filepath.Dir(artifactFullPath)
				if outputDir != modulesRoot {
					result[outputDir] = struct{}{}
				}
			}
		}
	}
	return result
}

func removeNginxModuleOutputsNotReferenced(install model.AppInstall, fromModules, referencedModules []dto.NginxModule) {
	referenced := nginxModuleOutputDirectories(install, referencedModules)
	for outputDir := range nginxModuleOutputDirectories(install, fromModules) {
		if _, ok := referenced[outputDir]; !ok {
			_ = os.RemoveAll(outputDir)
		}
	}
}

func sanitizeModulePathPart(value string) string {
	var result strings.Builder
	for _, char := range value {
		if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '-' || char == '_' {
			result.WriteRune(char)
		} else {
			result.WriteByte('-')
		}
	}
	return strings.Trim(result.String(), "-")
}

func nginxModulePathName(value string) string {
	base := sanitizeModulePathPart(value)
	if base == "" {
		base = "module"
	}
	if len(base) > 48 {
		base = base[:48]
	}
	sum := sha256.Sum256([]byte(value))
	return fmt.Sprintf("%s-%s", base, hex.EncodeToString(sum[:4]))
}
