package imagepull

import (
	"archive/tar"
	"bytes"
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

	"banger/internal/system"
)

// ApplyOwnership rewrites the ext4 image's per-file uid/gid/mode to match
// the tar-header values Flatten captured. `mkfs.ext4 -d` preserves the
// on-disk ownership of the source tree — which is the runner's uid/gid,
// since we extracted as a regular user — so without this pass setuid
// binaries become setuid-nonroot and root-owned config files are
// readable by the runner's group.
//
// Implementation: stream a "set_inode_field" script to `debugfs -w`.
// One invocation handles tens of thousands of files; the bottleneck is
// debugfs's one-inode-at-a-time disk I/O, not process startup.
func ApplyOwnership(ctx context.Context, runner system.CommandRunner, ext4File string, meta Metadata) error {
	if len(meta.Entries) == 0 {
		return nil
	}
	script, err := buildOwnershipScript(meta)
	if err != nil {
		return err
	}
	if script.Len() == 0 {
		return nil
	}
	stdinRunner, ok := runner.(system.StdinRunner)
	if !ok {
		return fmt.Errorf("ownership fixup requires a runner that supports stdin (got %T)", runner)
	}
	out, err := stdinRunner.RunStdin(ctx, script, "debugfs", "-w", "-f", "-", ext4File)
	if err != nil {
		return fmt.Errorf("debugfs ownership fixup: %w: %s", err, string(out))
	}
	return nil
}

// buildOwnershipScript emits one `set_inode_field` block per entry.
// Paths are prefixed with "/" so debugfs resolves them from the ext4
// root. Entries are sorted for deterministic output (helps testing and
// makes debugfs's internal caching slightly more cache-friendly).
func buildOwnershipScript(meta Metadata) (*bytes.Buffer, error) {
	var buf bytes.Buffer
	paths := make([]string, 0, len(meta.Entries))
	for p := range meta.Entries {
		paths = append(paths, p)
	}
	sort.Strings(paths)
	for _, p := range paths {
		m := meta.Entries[p]
		mode := debugfsMode(m.Type, m.Mode)
		if mode == 0 {
			continue // hardlinks or unsupported types (skip)
		}
		if err := validateDebugFSPath(p); err != nil {
			return nil, err
		}
		escaped := escapeDebugfsPath(p)
		fmt.Fprintf(&buf, "set_inode_field %s uid %d\n", escaped, m.Uid)
		fmt.Fprintf(&buf, "set_inode_field %s gid %d\n", escaped, m.Gid)
		fmt.Fprintf(&buf, "set_inode_field %s mode 0%o\n", escaped, mode)
	}
	return &buf, nil
}

// debugfsMode composes the full i_mode word (file-type bits +
// permission bits) that debugfs' `set_inode_field ... mode` expects.
// Returns 0 for types we don't set (hardlinks, unknown).
func debugfsMode(typ byte, hdrMode int64) uint32 {
	perm := uint32(hdrMode) & 0o7777
	switch typ {
	case tar.TypeReg:
		return 0o100000 | perm
	case tar.TypeDir:
		return 0o040000 | perm
	case tar.TypeSymlink:
		return 0o120000 | perm
	case tar.TypeChar:
		return 0o020000 | perm
	case tar.TypeBlock:
		return 0o060000 | perm
	case tar.TypeFifo:
		return 0o010000 | perm
	default:
		return 0
	}
}

var errUnsafeDebugFSPath = errors.New("unsafe path for debugfs ownership script")

func validateDebugFSPath(rel string) error {
	for i := 0; i < len(rel); i++ {
		switch c := rel[i]; {
		case c == '"':
			return fmt.Errorf("%w: %q contains '\"'", errUnsafeDebugFSPath, rel)
		case c == '\\':
			return fmt.Errorf("%w: %q contains '\\\\'", errUnsafeDebugFSPath, rel)
		case c < 0x20 || c == 0x7f:
			return fmt.Errorf("%w: %q contains control byte 0x%02x", errUnsafeDebugFSPath, rel, c)
		}
	}
	return nil
}

// escapeDebugfsPath prepends "/" and wraps in double quotes if the path
// contains spaces. validateDebugFSPath rejects debugfs-hostile bytes
// before this runs, so the only quoting we need is the simple
// whitespace case debugfs already handles.
func escapeDebugfsPath(rel string) string {
	abs := "/" + rel
	if strings.ContainsRune(abs, ' ') {
		return `"` + abs + `"`
	}
	return abs
}
