package app

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/topdata"
	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/validator"
)

type command struct {
	name        string
	description string
	run         func(context) error
}

type context struct {
	stdout io.Writer
	stderr io.Writer
	cwd    string
	args   []string
}

var commands = []command{
	{
		name:        "build",
		description: "Build module and HAK outputs into build/, plus top package output when topdata is configured.",
		run:         runBuild,
	},
	{
		name:        "build-module",
		description: "Build only the module archive from src/ into build/.",
		run:         runBuildModule,
	},
	{
		name:        "build-haks",
		description: "Build only HAK archives and manifest from assets/ into build/.",
		run:         runBuildHAKs,
	},
	{
		name:        "extract",
		description: "Extract a module archive back into the src/ tree.",
		run:         runExtract,
	},
	{
		name:        "validate",
		description: "Validate project layout, config, and source inventory.",
		run:         runValidate,
	},
	{
		name:        "compare",
		description: "Compare current source content against the built archives.",
		run:         runCompare,
	},
	{
		name:        "apply-hak-manifest",
		description: "Apply a generated HAK manifest to src/module/module.ifo.json.",
		run:         runApplyHAKManifest,
	},
	{
		name:        "validate-topdata",
		description: "Validate topdata source layout and canonical JSON compatibility.",
		run:         runValidateTopData,
	},
	{
		name:        "build-topdata",
		description: "Build canonical topdata outputs into .cache/2da and .cache/wiki, then refresh build/sow_top.hak and build/sow_tlk.tlk.",
		run:         runBuildTopData,
	},
	{
		name:        "build-top-package",
		description: "Build build/sow_top.hak and build/sow_tlk.tlk from cached native topdata output and authored topdata/assets.",
		run:         runBuildTopPackage,
	},
	{
		name:        "compare-topdata",
		description: "Rebuild topdata natively and compare the current build output against that fresh native result.",
		run:         runCompareTopData,
	},
	{
		name:        "convert-topdata",
		description: "Convert between 2DA and native topdata JSON/module formats.",
		run:         runConvertTopData,
	},
	{
		name:        "build-wiki",
		description: "Build wiki pages from the current topdata state.",
		run:         runBuildWiki,
	},
	{
		name:        "deploy-wiki",
		description: "Deploy wiki pages to DokuWiki.",
		run:         runDeployWiki,
	},
}

func Run(args []string) (int, error) {
	ctx, err := newContext()
	if err != nil {
		return 1, err
	}

	if len(args) == 0 {
		printUsage(ctx.stdout)
		return 0, nil
	}

	switch args[0] {
	case "-h", "--help", "help":
		printUsage(ctx.stdout)
		return 0, nil
	default:
		for _, cmd := range commands {
			if cmd.name == args[0] {
				if err := cmd.run(ctx); err != nil {
					return 1, err
				}
				return 0, nil
			}
		}
	}

	return 1, fmt.Errorf("unknown command %q", args[0])
}

func newContext() (context, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return context{}, fmt.Errorf("resolve working directory: %w", err)
	}

	return context{
		stdout: os.Stdout,
		stderr: os.Stderr,
		cwd:    cwd,
		args:   os.Args[1:],
	}, nil
}

func runBuild(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	result, err := pipeline.Build(p)
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
	fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources)
	if result.TopPackageHAK != "" {
		fmt.Fprintf(ctx.stdout, "top package hak: %s\n", result.TopPackageHAK)
		fmt.Fprintf(ctx.stdout, "top package tlk: %s\n", result.TopPackageTLK)
	}
	if len(result.HAKPaths) > 0 {
		fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
		fmt.Fprintf(ctx.stdout, "hak assets: %d\n", result.HAKAssets)
		fmt.Fprintf(ctx.stdout, "hak manifest: %s\n", result.Manifest)
	}
	return nil
}

func runBuildModule(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	result, err := pipeline.BuildModuleWithProgress(p, func(message string) {
		fmt.Fprintf(ctx.stdout, "[build-module] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
	fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources)
	return nil
}

type buildHAKOptions struct {
	filteredHAKs     []string
	filteredArchives []string
	sourceManifest   string
	planOnly         bool
}

func runBuildHAKs(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	opts, err := parseBuildHAKArgs(ctx.args[1:])
	if err != nil {
		return err
	}
	p, err = p.CloneWithHAKNames(opts.filteredHAKs)
	if err != nil {
		return err
	}

	progress := func(message string) {
		fmt.Fprintf(ctx.stdout, "[build-haks] %s\n", message)
	}
	var result pipeline.BuildResult
	if opts.planOnly {
		result, err = pipeline.PlanHAKsWithProgress(p, progress)
	} else {
		result, err = pipeline.BuildHAKsWithOptions(p, pipeline.BuildHAKOptions{
			Progress:           progress,
			ArchiveNames:       opts.filteredArchives,
			SourceManifestPath: opts.sourceManifest,
		})
	}
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
	fmt.Fprintf(ctx.stdout, "hak assets: %d\n", result.HAKAssets)
	if result.Manifest != "" {
		fmt.Fprintf(ctx.stdout, "hak manifest: %s\n", result.Manifest)
	}
	return nil
}

func parseBuildHAKArgs(args []string) (buildHAKOptions, error) {
	opts := buildHAKOptions{}
	if len(args) == 0 {
		return opts, nil
	}

	for index := 0; index < len(args); index++ {
		arg := args[index]
		switch arg {
		case "-h", "--help":
			return opts, errors.New("usage: build-haks [--hak <hak-name> ...] [--archive <archive-name> ...] [--source-manifest <path>] [--plan-only]")
		case "--hak":
			index++
			if index >= len(args) {
				return opts, errors.New("--hak requires a value")
			}
			opts.filteredHAKs = append(opts.filteredHAKs, args[index])
		case "--archive":
			index++
			if index >= len(args) {
				return opts, errors.New("--archive requires a value")
			}
			opts.filteredArchives = append(opts.filteredArchives, args[index])
		case "--plan-only":
			opts.planOnly = true
		case "--source-manifest":
			index++
			if index >= len(args) {
				return opts, errors.New("--source-manifest requires a value")
			}
			opts.sourceManifest = args[index]
		default:
			if value, ok := parseInlineFlagValue(arg, "--hak"); ok {
				opts.filteredHAKs = append(opts.filteredHAKs, value)
				continue
			}
			if value, ok := parseInlineFlagValue(arg, "--archive"); ok {
				opts.filteredArchives = append(opts.filteredArchives, value)
				continue
			}
			if value, ok := parseInlineFlagValue(arg, "--source-manifest"); ok {
				opts.sourceManifest = value
				continue
			}
			return opts, fmt.Errorf("unknown build-haks argument %q", arg)
		}
	}
	return opts, nil
}

func parseInlineFlagValue(arg, name string) (string, bool) {
	prefix := name + "="
	if !strings.HasPrefix(arg, prefix) {
		return "", false
	}
	return strings.TrimSpace(strings.TrimPrefix(arg, prefix)), true
}

func runExtract(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	result, err := pipeline.Extract(p)
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
	if len(result.HAKPaths) > 0 {
		fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
	}
	fmt.Fprintf(ctx.stdout, "written: %d\n", result.Written)
	fmt.Fprintf(ctx.stdout, "overwritten: %d\n", result.Overwritten)
	fmt.Fprintf(ctx.stdout, "removed: %d\n", result.Removed)
	fmt.Fprintf(ctx.stdout, "skipped: %d\n", result.Skipped)
	return nil
}

func runValidate(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	validationReport := validator.ValidateProject(p)
	emitValidatorReport(ctx.stderr, validationReport)
	if validationReport.HasErrors() {
		return fmt.Errorf("validation failed with %d error(s)", validationReport.ErrorCount())
	}

	inventoryReport := p.Inventory.Report()
	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "root: %s\n", p.Root)
	fmt.Fprintf(ctx.stdout, "source files: %d\n", inventoryReport.SourceFiles)
	fmt.Fprintf(ctx.stdout, "script files: %d\n", inventoryReport.ScriptFiles)
	fmt.Fprintf(ctx.stdout, "asset files: %d\n", inventoryReport.AssetFiles)
	fmt.Fprintf(ctx.stdout, "module file types: %s\n", strings.Join(inventoryReport.Extensions, ", "))
	if warnings := validationReport.WarningCount(); warnings > 0 {
		fmt.Fprintf(ctx.stdout, "warnings: %d\n", warnings)
	}
	fmt.Fprintf(ctx.stdout, "validation: ok\n")
	return nil
}

func emitValidatorReport(w io.Writer, report validator.Report) {
	lines := report.SummaryLines(5)
	emitSummaryLines(w, lines, 20, "validation diagnostics")
	if line := report.DecompiledSummaryLine(12); line != "" {
		fmt.Fprintln(w, line)
	}
}

func emitTopdataValidationReport(w io.Writer, report topdata.ValidationReport) {
	lines := report.SummaryLines(5)
	emitSummaryLines(w, lines, 20, "topdata validation diagnostics")
}

func emitSummaryLines(w io.Writer, lines []string, maxLines int, label string) {
	if maxLines <= 0 || len(lines) <= maxLines {
		for _, line := range lines {
			fmt.Fprintln(w, line)
		}
		return
	}
	for _, line := range lines[:maxLines] {
		fmt.Fprintln(w, line)
	}
	fmt.Fprintf(w, "info: %d additional %s omitted\n", len(lines)-maxLines, label)
}

func runCompare(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	result, err := pipeline.Compare(p)
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
	if len(result.HAKPaths) > 0 {
		fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
	}
	fmt.Fprintf(ctx.stdout, "checked resources: %d\n", result.Checked)
	fmt.Fprintf(ctx.stdout, "compare: ok\n")
	return nil
}

func runApplyHAKManifest(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	manifestPath := filepath.Join(p.BuildDir(), "haks.json")
	if len(ctx.args) > 1 {
		manifestPath = ctx.args[1]
		if !filepath.IsAbs(manifestPath) {
			manifestPath = filepath.Join(ctx.cwd, manifestPath)
		}
	}

	result, err := pipeline.ApplyHAKManifest(p, manifestPath)
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "manifest: %s\n", result.ManifestPath)
	fmt.Fprintf(ctx.stdout, "module source: %s\n", result.ModuleSource)
	fmt.Fprintf(ctx.stdout, "hak entries: %d\n", result.HAKCount)
	return nil
}

func runValidateTopData(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	report := topdata.ValidateProject(p)
	emitTopdataValidationReport(ctx.stderr, report)
	if report.HasErrors() {
		return fmt.Errorf("topdata validation failed with %d error(s)", report.ErrorCount())
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "topdata root: %s\n", p.TopDataSourceDir())
	fmt.Fprintf(ctx.stdout, "topdata files: %d\n", report.Files)
	fmt.Fprintf(ctx.stdout, "data files: %d\n", report.DataFiles)
	fmt.Fprintf(ctx.stdout, "tlk files: %d\n", report.TLKFiles)
	if warnings := report.WarningCount(); warnings > 0 {
		fmt.Fprintf(ctx.stdout, "warnings: %d\n", warnings)
	}
	fmt.Fprintf(ctx.stdout, "topdata validation: ok\n")
	return nil
}

func runBuildTopData(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	opts, err := parseBuildTopDataArgs("build-topdata", ctx.args[1:])
	if err != nil {
		return err
	}

	result, err := topdata.BuildAndPackageWithOptions(p, opts, func(message string) {
		fmt.Fprintf(ctx.stdout, "[build-topdata] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "mode: %s\n", result.Mode)
	fmt.Fprintf(ctx.stdout, "topdata 2da output: %s\n", result.Output2DADir)
	fmt.Fprintf(ctx.stdout, "topdata tlk output: %s\n", result.OutputTLKDir)
	fmt.Fprintf(ctx.stdout, "top package hak: %s\n", result.OutputHAKPath)
	fmt.Fprintf(ctx.stdout, "top package tlk: %s\n", result.OutputTLKPath)
	fmt.Fprintf(ctx.stdout, "2da files: %d\n", result.Files2DA)
	fmt.Fprintf(ctx.stdout, "tlk files: %d\n", result.FilesTLK)
	fmt.Fprintf(ctx.stdout, "top package resources: %d\n", result.HAKResources)
	fmt.Fprintf(ctx.stdout, "top package assets: %d\n", result.AssetFiles)
	if result.WikiOutputDir != "" {
		fmt.Fprintf(ctx.stdout, "wiki output: %s\n", result.WikiOutputDir)
		fmt.Fprintf(ctx.stdout, "wiki pages: %d\n", result.WikiPages)
		fmt.Fprintf(ctx.stdout, "wiki status: %s\n", result.WikiStatus)
	}
	return nil
}

func runBuildTopPackage(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	opts, err := parseBuildTopDataArgs("build-top-package", ctx.args[1:])
	if err != nil {
		return err
	}

	result, err := topdata.BuildAndPackageWithOptions(p, opts, func(message string) {
		fmt.Fprintf(ctx.stdout, "[build-top-package] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "mode: %s\n", result.Mode)
	fmt.Fprintf(ctx.stdout, "topdata 2da output: %s\n", result.Output2DADir)
	fmt.Fprintf(ctx.stdout, "topdata tlk output: %s\n", result.OutputTLKDir)
	fmt.Fprintf(ctx.stdout, "top package hak: %s\n", result.OutputHAKPath)
	fmt.Fprintf(ctx.stdout, "top package tlk: %s\n", result.OutputTLKPath)
	fmt.Fprintf(ctx.stdout, "2da files: %d\n", result.Files2DA)
	fmt.Fprintf(ctx.stdout, "tlk files: %d\n", result.FilesTLK)
	fmt.Fprintf(ctx.stdout, "top package resources: %d\n", result.HAKResources)
	fmt.Fprintf(ctx.stdout, "top package assets: %d\n", result.AssetFiles)
	if result.WikiOutputDir != "" {
		fmt.Fprintf(ctx.stdout, "wiki output: %s\n", result.WikiOutputDir)
		fmt.Fprintf(ctx.stdout, "wiki pages: %d\n", result.WikiPages)
		fmt.Fprintf(ctx.stdout, "wiki status: %s\n", result.WikiStatus)
	}
	return nil
}

func parseBuildTopDataArgs(commandName string, args []string) (topdata.BuildAndPackageOptions, error) {
	opts := topdata.BuildAndPackageOptions{}
	for _, arg := range args {
		switch arg {
		case "--force":
			opts.Force = true
		case "--wiki":
			opts.BuildWiki = true
		case "-h", "--help":
			return opts, fmt.Errorf("usage: %s [--force] [--wiki]", commandName)
		default:
			return opts, fmt.Errorf("unknown %s argument %q", commandName, arg)
		}
	}
	return opts, nil
}

func runCompareTopData(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	result, err := topdata.Compare(p, func(message string) {
		fmt.Fprintf(ctx.stdout, "[compare-topdata] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "mode: %s\n", result.Mode)
	fmt.Fprintf(ctx.stdout, "checked 2da files: %d\n", result.Compared2DA)
	fmt.Fprintf(ctx.stdout, "checked tlk files: %d\n", result.ComparedTLK)
	fmt.Fprintf(ctx.stdout, "native-pass: %d\n", result.NativePass)
	fmt.Fprintf(ctx.stdout, "not-yet-canonical: %d\n", result.NotYetCanonical)
	fmt.Fprintf(ctx.stdout, "native-mismatch: %d\n", result.NativeMismatch)
	fmt.Fprintf(ctx.stdout, "topdata compare: ok\n")
	return nil
}

func runConvertTopData(ctx context) error {
	if len(ctx.args) < 2 {
		return errors.New("usage: convert-topdata <2da-to-json|2da-to-module|json-to-2da> ...")
	}
	return topdata.RunConvertCommand(ctx.args[1:], ctx.stdout)
}

func runBuildWiki(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	opts, err := parseBuildWikiArgs("build-wiki", ctx.args[1:])
	if err != nil {
		return err
	}

	result, err := topdata.BuildWikiWithOptions(p, opts, func(message string) {
		fmt.Fprintf(ctx.stdout, "[build-wiki] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "wiki output: %s\n", result.OutputDir)
	fmt.Fprintf(ctx.stdout, "wiki pages: %d\n", result.PageCount)
	fmt.Fprintf(ctx.stdout, "wiki status: %s\n", result.Status)
	return nil
}

func parseBuildWikiArgs(commandName string, args []string) (topdata.BuildWikiOptions, error) {
	opts := topdata.BuildWikiOptions{}
	for _, arg := range args {
		switch arg {
		case "--force":
			opts.Force = true
		case "-h", "--help":
			return opts, fmt.Errorf("usage: %s [--force]", commandName)
		default:
			return opts, fmt.Errorf("unknown %s argument %q", commandName, arg)
		}
	}
	return opts, nil
}

func runDeployWiki(ctx context) error {
	p, err := loadProject(ctx.cwd)
	if err != nil {
		return err
	}

	opts, err := parseDeployWikiArgs("deploy-wiki", ctx.args[1:])
	if err != nil {
		return err
	}

	result, err := topdata.DeployWikiWithOptions(p, opts, func(message string) {
		fmt.Fprintf(ctx.stdout, "[deploy-wiki] %s\n", message)
	})
	if err != nil {
		return err
	}

	fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name)
	fmt.Fprintf(ctx.stdout, "local pages: %d\n", result.LocalPages)
	fmt.Fprintf(ctx.stdout, "updated: %d\n", result.Updated)
	fmt.Fprintf(ctx.stdout, "skipped: %d\n", result.Skipped)
	fmt.Fprintf(ctx.stdout, "deleted: %d\n", result.Deleted)
	fmt.Fprintf(ctx.stdout, "manifest: %s\n", result.Manifest)
	return nil
}

func parseDeployWikiArgs(commandName string, args []string) (topdata.DeployWikiOptions, error) {
	opts := topdata.DeployWikiOptions{}
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "--source-dir":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--source-dir requires a value")
			}
			opts.SourceDir = args[i]
		case "--endpoint":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--endpoint requires a value")
			}
			opts.Endpoint = args[i]
		case "--username":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--username requires a value")
			}
			opts.Username = args[i]
		case "--password":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--password requires a value")
			}
			opts.Password = args[i]
		case "--version":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--version requires a value")
			}
			opts.Version = args[i]
		case "--namespace":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--namespace requires a value")
			}
			opts.Namespaces = append(opts.Namespaces, args[i])
		case "--manifest":
			i++
			if i >= len(args) {
				return opts, fmt.Errorf("--manifest requires a value")
			}
			opts.ManifestPath = args[i]
		case "--dry-run":
			opts.DryRun = true
		case "-h", "--help":
			return opts, fmt.Errorf("usage: %s [--source-dir <path>] [--endpoint <url>] [--username <user>] [--password <pass>] [--version <ver>] [--namespace <ns> ...] [--manifest <path>] [--dry-run]", commandName)
		default:
			if strings.HasPrefix(arg, "--source-dir=") {
				opts.SourceDir = strings.TrimPrefix(arg, "--source-dir=")
			} else if strings.HasPrefix(arg, "--endpoint=") {
				opts.Endpoint = strings.TrimPrefix(arg, "--endpoint=")
			} else if strings.HasPrefix(arg, "--username=") {
				opts.Username = strings.TrimPrefix(arg, "--username=")
			} else if strings.HasPrefix(arg, "--password=") {
				opts.Password = strings.TrimPrefix(arg, "--password=")
			} else if strings.HasPrefix(arg, "--version=") {
				opts.Version = strings.TrimPrefix(arg, "--version=")
			} else if strings.HasPrefix(arg, "--namespace=") {
				opts.Namespaces = append(opts.Namespaces, strings.TrimPrefix(arg, "--namespace="))
			} else if strings.HasPrefix(arg, "--manifest=") {
				opts.ManifestPath = strings.TrimPrefix(arg, "--manifest=")
			} else if arg == "--dry-run" {
				opts.DryRun = true
			} else {
				return opts, fmt.Errorf("unknown %s argument %q", commandName, arg)
			}
		}
	}

	if opts.Endpoint == "" {
		opts.Endpoint = os.Getenv("DOKUWIKI_RPC_ENDPOINT")
	}
	if opts.Username == "" {
		opts.Username = os.Getenv("DOKUWIKI_RPC_USERNAME")
	}
	if opts.Password == "" {
		opts.Password = os.Getenv("DOKUWIKI_RPC_PASSWORD")
	}
	if opts.Version == "" {
		opts.Version = os.Getenv("GITHUB_REF_NAME")
	}

	return opts, nil
}

func loadProject(cwd string) (*project.Project, error) {
	root, err := project.FindRoot(cwd)
	if err != nil {
		return nil, err
	}

	p, err := project.Load(root)
	if err != nil {
		return nil, err
	}

	var failures []error
	if err := p.ValidateLayout(); err != nil {
		failures = append(failures, err)
	}
	if err := p.Scan(); err != nil {
		failures = append(failures, err)
	}

	if len(failures) > 0 {
		return nil, errors.Join(failures...)
	}

	return p, nil
}

func printUsage(w io.Writer) {
	exe := filepath.Base(os.Args[0])
	fmt.Fprintf(w, "%s <command>\n\n", exe)
	fmt.Fprintln(w, "Commands:")
	for _, cmd := range commands {
		fmt.Fprintf(w, "  %-8s %s\n", cmd.name, cmd.description)
	}
}
