package container

import (
	"encoding/gob"
	"fmt"
	"slices"

	"hakurei.app/check"
	"hakurei.app/ext"
	"hakurei.app/fhs"
)

const (
	// intermediate root file name pattern for [MountOverlayOp.Upper];
	// remains after apply returns
	intermediatePatternOverlayUpper = "overlay.upper.*"
	// intermediate root file name pattern for [MountOverlayOp.Work];
	// remains after apply returns
	intermediatePatternOverlayWork = "overlay.work.*"
)

func init() { gob.Register(new(MountOverlayOp)) }

const (
	// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
	// and [MountOverlayOp.Upper] holds an unexpected value.
	OverlayEphemeralUnexpectedUpper = iota
	// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
	// two entries when mounting readonly.
	OverlayReadonlyLower
	// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
	OverlayEmptyLower
)

// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
type OverlayArgumentError struct {
	Type  uintptr
	Value string
}

func (e *OverlayArgumentError) Error() string {
	switch e.Type {
	case OverlayEphemeralUnexpectedUpper:
		return fmt.Sprintf("upperdir has unexpected value %q", e.Value)

	case OverlayReadonlyLower:
		return "readonly overlay requires at least two lowerdir"

	case OverlayEmptyLower:
		return "overlay requires at least one lowerdir"

	default:
		return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
	}
}

// Overlay is a helper for appending [MountOverlayOp] to [Ops].
func (f *Ops) Overlay(
	target, state, work *check.Absolute,
	layers ...*check.Absolute,
) *Ops {
	*f = append(*f, &MountOverlayOp{
		Target: target,
		Lower:  layers,
		Upper:  state,
		Work:   work,
	})
	return f
}

// OverlayEphemeral appends a [MountOverlayOp] with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
	return f.Overlay(target, fhs.AbsRoot, nil, layers...)
}

// OverlayReadonly appends a readonly [MountOverlayOp].
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
	return f.Overlay(target, nil, nil, layers...)
}

// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
	Target *check.Absolute

	// Any filesystem, does not need to be on a writable filesystem.
	Lower []*check.Absolute
	// Formatted for [OptionOverlayLowerdir].
	//
	// Resolved, prefixed and escaped during early.
	lower []string

	// The upperdir is normally on a writable filesystem.
	//
	// If Work is nil and Upper holds the special value [fhs.AbsRoot], an
	// ephemeral upperdir and workdir will be set up.
	//
	// If both Work and Upper are nil, upperdir and workdir is omitted and the
	// overlay is mounted readonly.
	Upper *check.Absolute
	// Formatted for [OptionOverlayUpperdir].
	//
	// Resolved, prefixed and escaped during early.
	upper string

	// The workdir needs to be an empty directory on the same filesystem as upperdir.
	Work *check.Absolute
	// Formatted for [OptionOverlayWorkdir].
	//
	// Resolved, prefixed and escaped during early.
	work string

	ephemeral bool

	// Used internally for mounting to the intermediate root.
	noPrefix bool
}

func (o *MountOverlayOp) Valid() bool {
	if o == nil {
		return false
	}
	if o.Work != nil && o.Upper == nil {
		return false
	}
	if slices.Contains(o.Lower, nil) {
		return false
	}
	return o.Target != nil
}

func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
	if o.Work == nil && o.Upper != nil {
		switch o.Upper.String() {
		case fhs.Root: // ephemeral
			o.ephemeral = true // intermediate root not yet available

		default:
			return &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()}
		}
	}
	// readonly handled in apply

	if !o.ephemeral {
		if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
			// unreachable
			return OpStateError("overlay")
		}

		if o.Upper != nil {
			if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
				return err
			} else {
				o.upper = toHost(v)
			}
		}

		if o.Work != nil {
			if v, err := k.evalSymlinks(o.Work.String()); err != nil {
				return err
			} else {
				o.work = toHost(v)
			}
		}
	}

	o.lower = make([]string, len(o.Lower))
	for i, a := range o.Lower { // nil checked in Valid
		if v, err := k.evalSymlinks(a.String()); err != nil {
			return err
		} else {
			o.lower[i] = toHost(v)
		}
	}
	return nil
}

// mountOverlay sets up an overlay mount via [ext.FS].
func mountOverlay(target string, options [][2]string) error {
	fs, err := ext.OpenFS(SourceOverlay, 0)
	if err != nil {
		return err
	}
	if err = fs.SetString("source", SourceOverlay); err != nil {
		_ = fs.Close()
		return err
	}
	for _, option := range options {
		if err = fs.SetString(option[0], option[1]); err != nil {
			_ = fs.Close()
			return err
		}
	}
	if err = fs.SetFlag(OptionOverlayUserxattr); err != nil {
		_ = fs.Close()
		return err
	}
	if err = fs.Mount(target, 0); err != nil {
		_ = fs.Close()
		return err
	}
	return fs.Close()
}

func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
	target := o.Target.String()
	if !o.noPrefix {
		target = toSysroot(target)
	}
	if err := k.mkdirAll(target, state.ParentPerm); err != nil {
		return err
	}

	if o.ephemeral {
		var err error
		// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
		if o.upper, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayUpper); err != nil {
			return err
		}
		if o.work, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayWork); err != nil {
			return err
		}
	}

	options := make([][2]string, 0, 2+len(o.lower))

	if o.upper == zeroString && o.work == zeroString { // readonly
		if len(o.Lower) < 2 {
			return &OverlayArgumentError{OverlayReadonlyLower, zeroString}
		}
		// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
	} else {
		if len(o.Lower) == 0 {
			return &OverlayArgumentError{OverlayEmptyLower, zeroString}
		}
		options = append(options, [][2]string{
			{OptionOverlayUpperdir, o.upper},
			{OptionOverlayWorkdir, o.work},
		}...)
	}
	for _, lower := range o.lower {
		options = append(options, [2]string{OptionOverlayLowerdir + "+", lower})
	}

	return k.mountOverlay(target, options)
}

func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }

func (o *MountOverlayOp) Is(op Op) bool {
	vo, ok := op.(*MountOverlayOp)
	return ok && o.Valid() && vo.Valid() &&
		o.Target.Is(vo.Target) &&
		slices.EqualFunc(o.Lower, vo.Lower, func(a, v *check.Absolute) bool { return a.Is(v) }) &&
		o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
}
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
func (o *MountOverlayOp) String() string {
	return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}
