// Package updater implements the Generic Update system: pluggable
// providers that fetch new versions of game files from SteamCMD,
// GitHub releases, or a direct URL, then lay them down under the
// instance's data path (bind mount) or container volume.
//
// The panel's manifest declares one or more update_providers per module;
// the operator picks one to trigger. Each provider runs on the *Agent*
// (close to the data) and streams progress back as LogLine envelopes
// so the dashboard renders updates in the same events pane as normal
// server output.
package updater

import (
	"context"
	"fmt"
	"os"
	"path"
	"path/filepath"

	"github.com/dbledeez/panel/agent/internal/runtime"
	modulepkg "github.com/dbledeez/panel/pkg/module"
)

// Context is the environment an update runs in.
type Context struct {
	InstanceID     string
	Manifest       *modulepkg.Manifest
	ContainerID    string               // empty if container doesn't exist; present for running + stopped
	BrowseableRoot string               // container-absolute install root, when applicable
	DataPath       string               // host bind-mount root, fallback
	Runtime        runtime.Runtime      // for container file ops + sidecar launches (future)
	Log            func(line string)    // streams a progress line upstream via LogLine

	// Steam login credentials for SteamCMD apps that refuse +login
	// anonymous (DayZ, Arma, etc.). Populated by the controller from its
	// encrypted steam_credentials store, forwarded via the mTLS-protected
	// gRPC stream. The agent redacts SteamPassword from log output before
	// it ever hits the bus; see steamcmd.go.
	SteamUsername string
	SteamPassword string
}

// Provider performs an update according to its kind.
type Provider interface {
	Name() string
	Update(ctx context.Context, uc *Context, spec *modulepkg.UpdateProvider) error
}

// Get returns the Provider implementation for the given kind, or an
// error if the kind is unknown.
func Get(kind string) (Provider, error) {
	switch kind {
	case "direct":
		return &DirectProvider{}, nil
	case "github":
		return &GitHubProvider{}, nil
	case "steamcmd":
		return &SteamCMDProvider{}, nil
	default:
		return nil, fmt.Errorf("unknown update provider kind %q", kind)
	}
}

// writeToInstance puts data at the instance-relative path. Prefers
// container-path ops when the instance has a BrowseableRoot and a live
// container; otherwise falls back to writing under DataPath.
func writeToInstance(ctx context.Context, uc *Context, targetPath string, data []byte) error {
	if targetPath == "" {
		return fmt.Errorf("target_path is required")
	}
	if uc.BrowseableRoot != "" && uc.ContainerID != "" {
		abs := path.Join(uc.BrowseableRoot, targetPath)
		// Ensure parent dir exists inside the container via runtime exec.
		// We don't have a helper in the interface; we just try to write
		// and rely on CopyFileToContainer returning a clear error if the
		// parent is missing. (itzg and most images pre-create /data.)
		return uc.Runtime.CopyFileToContainer(ctx, uc.ContainerID, abs, data)
	}
	if uc.DataPath == "" {
		return fmt.Errorf("no storage available: container has no BrowseableRoot and instance has no DataPath")
	}
	abs := filepath.Join(uc.DataPath, filepath.FromSlash(targetPath))
	if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", filepath.Dir(abs), err)
	}
	if err := os.WriteFile(abs, data, 0o644); err != nil {
		return fmt.Errorf("write %s: %w", abs, err)
	}
	return nil
}
