package update

import (
	"context"
	"errors"
	"fmt"
	"os"
	"syscall"
	"time"
	"unicode/utf8"
)

// Verification is "does this tree build and does it pass its own tests", run
// before a single byte is written to the install dir.
//
// It is `make build` and `make test`, not `go build`: the CGO daemons need the
// vendored toolchain and the whisper/piper include and library paths wired
// through the Makefile, and a bare `go build` on them fails in a way that has
// nothing to do with the change being deployed. `make test` is the -race suite
// with the CGO env set, and it is the only evidence available on a single box
// that the new code does what the old code did.
//
// This is not a substitute for a second environment. A test suite that passes
// says the code is self-consistent; it does not say the new build will start
// against this machine's actual models, sockets and encrypted store. That is
// what the post-restart health check is for, and it is why the install is
// reversible rather than merely careful.

// Step — one verification or orchestration step and how it went. Kept so the CLI
// can print a truthful account of what was done, including on the failure path.
type Step struct {
	Name   string
	Argv   []string
	Took   time.Duration
	Err    error
	Output string // combined output, only retained for failures
}

// Verify runs the build and the test suite in SourceDir.
func (u *Updater) Verify(ctx context.Context) ([]Step, error) {
	if err := u.refuseRootBuild(); err != nil {
		return nil, err
	}
	ctx, cancel := context.WithTimeout(ctx, u.cfg.verifyTimeout())
	defer cancel()
	var steps []Step
	for _, argv := range [][]string{{"make", "build"}, {"make", "test"}} {
		u.log("verify: %v (this takes a while)", argv)
		start := u.now()
		out, err := u.run(ctx, u.cfg.SourceDir, argv)
		st := Step{Name: argv[len(argv)-1], Argv: argv, Took: u.now().Sub(start), Err: err}
		if err != nil {
			st.Output = tail(out, 4000)
		}
		steps = append(steps, st)
		if err != nil {
			u.log("verify: %v FAILED after %s", argv, st.Took.Round(time.Second))
			return steps, fmt.Errorf("%w: %v: %v", ErrVerifyFailed, argv, err)
		}
		u.log("verify: %v ok in %s", argv, st.Took.Round(time.Second))
	}
	return steps, nil
}

// refuseRootBuild stops a sudo'd apply from building in a tree it does not own.
//
// The seam is injected so the tests can drive both sides without a second uid.
func (u *Updater) refuseRootBuild() error {
	uid, owner, err := u.ids(u.cfg.SourceDir)
	if err != nil || uid != 0 || owner == 0 {
		return nil // not root, or root's own tree, or we cannot tell
	}
	return fmt.Errorf("%w: %s is owned by uid %d", ErrRootOnHisTree, u.cfg.SourceDir, owner)
}

// realIDs — the running uid and the owner of dir. Split out for the tests.
func realIDs(dir string) (uid int, owner uint32, err error) {
	fi, err := os.Stat(dir)
	if err != nil {
		return 0, 0, err
	}
	st, ok := fi.Sys().(*syscall.Stat_t)
	if !ok {
		return 0, 0, errors.New("update: cannot read directory ownership")
	}
	return os.Geteuid(), st.Uid, nil
}

// tail keeps the last n bytes — a failing `make test` prints far more than is
// useful, and the failure is always at the end.
//
// The cut is nudged forward to a rune boundary. Russian test names and fixture
// strings are the common case in this tree, and a slice landing mid-rune starts
// the log with a replacement character.
func tail(s string, n int) string {
	if len(s) <= n {
		return s
	}
	cut := len(s) - n
	for cut < len(s) && !utf8.RuneStart(s[cut]) {
		cut++
	}
	return "…" + s[cut:]
}
