package engine

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

// DeliverVersion makes the build's dictionary version resident on the persistent
// dictionary volume without disturbing the versions in-flight games already pin.
//
// The volume is seeded from the image once and never re-seeded, and the image's
// /opt/dawg is shadowed by that volume mount, so a redeploy that bumped
// BACKEND_DICT_VERSION cannot otherwise make the new DAWGs reachable at runtime.
// The image therefore keeps an unshadowed read-only copy at seedDir, and this
// copies it into dictDir/<version>/ when the version is not already present —
// neither the flat seed's own recorded label nor an existing version
// subdirectory. It is add-only and idempotent: the flat seed and any prior admin
// uploads are never touched, so old games keep the version they pin while the new
// one becomes resident and activatable (see game.Service.InitActiveVersion). A
// blank seedDir disables delivery (the pre-existing seed-only behaviour).
func DeliverVersion(dictDir, seedDir, version string) error {
	if seedDir == "" || version == "" {
		return nil
	}
	target := filepath.Join(dictDir, version)
	if _, err := os.Stat(target); err == nil {
		return nil // already delivered on a prior boot
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("engine: stat delivered dictionary %s: %w", target, err)
	}
	// A fresh volume is populated from the image and recorded as this version by the
	// marker: it is the flat dir, so there is nothing to deliver. resolveSeedVersion
	// records the label on first use, matching OpenWithVersions.
	seed, err := resolveSeedVersion(dictDir, version)
	if err != nil {
		return err
	}
	if seed == version {
		return nil
	}
	// Stage into a dot-prefixed directory (skipped by OpenWithVersions' version scan)
	// then rename, so a crash mid-copy never leaves a half-populated version subdir.
	staging := filepath.Join(dictDir, ".staging-deliver-"+version)
	if err := os.RemoveAll(staging); err != nil {
		return fmt.Errorf("engine: clear deliver staging %s: %w", staging, err)
	}
	if err := os.MkdirAll(staging, 0o755); err != nil {
		return fmt.Errorf("engine: create deliver staging %s: %w", staging, err)
	}
	for _, file := range dictFiles {
		src := filepath.Join(seedDir, file)
		if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
			continue // a variant absent from the seed is simply absent under this version
		} else if err != nil {
			_ = os.RemoveAll(staging)
			return fmt.Errorf("engine: stat seed dawg %s: %w", src, err)
		}
		if err := copyFile(src, filepath.Join(staging, file)); err != nil {
			_ = os.RemoveAll(staging)
			return err
		}
	}
	if err := os.Rename(staging, target); err != nil {
		_ = os.RemoveAll(staging)
		return fmt.Errorf("engine: publish delivered dictionary %s: %w", target, err)
	}
	return nil
}

// copyFile copies a single file, truncating the destination.
func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("engine: open %s: %w", src, err)
	}
	defer func() { _ = in.Close() }()
	out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
	if err != nil {
		return fmt.Errorf("engine: create %s: %w", dst, err)
	}
	if _, err := io.Copy(out, in); err != nil {
		_ = out.Close()
		return fmt.Errorf("engine: copy %s -> %s: %w", src, dst, err)
	}
	if err := out.Close(); err != nil {
		return fmt.Errorf("engine: finalise %s: %w", dst, err)
	}
	return nil
}
