package hst

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"reflect"
	"strings"

	"hakurei.app/check"
)

// FilesystemConfig is an abstract representation of a mount point.
type FilesystemConfig interface {
	// Valid returns whether the configuration is valid.
	Valid() bool
	// Path returns the target path in the container.
	Path() *check.Absolute
	// Host returns a slice of all host paths used by this operation.
	Host() []*check.Absolute
	// Apply appends the [container.Op] implementing this operation.
	Apply(z *ApplyState)

	fmt.Stringer
}

// The Ops interface enables [FilesystemConfig] to queue container ops without
// depending on the container package.
type Ops interface {
	// Tmpfs appends an op that mounts tmpfs on a container path.
	Tmpfs(target *check.Absolute, size int, perm os.FileMode) Ops
	// Readonly appends an op that mounts read-only tmpfs on a container path.
	Readonly(target *check.Absolute, perm os.FileMode) Ops

	// Bind appends an op that bind mounts a host path on a container path.
	Bind(source, target *check.Absolute, flags int) Ops
	// Overlay appends an op that mounts the overlay pseudo filesystem.
	Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
	// OverlayEphemeral appends a MountOverlayOp with an ephemeral upperdir and workdir.
	OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) Ops
	// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
	OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops

	// Link appends an op that creates a symlink in the container filesystem.
	Link(target *check.Absolute, linkName string, dereference bool) Ops

	// Root appends an op that expands a directory into a toplevel bind mount
	// mirror on container root.
	Root(host *check.Absolute, flags int) Ops
	// Etc appends an op that expands host /etc into a toplevel symlink mirror
	// with /etc semantics.
	Etc(host *check.Absolute, prefix string) Ops

	// Daemon appends an op that starts a daemon in the container and blocks
	// until target appears.
	Daemon(target, path *check.Absolute, args ...string) Ops
}

// ApplyState holds the address of [Ops] and any relevant application state.
type ApplyState struct {
	// Prefix for [FSBind] in autoetc [FSBind.Special] condition.
	AutoEtcPrefix string
	// Whether to skip remounting root.
	NoRemountRoot bool

	Ops
}

// ErrFSNull is returned by [json] on encountering a null [FilesystemConfig] value.
var ErrFSNull = errors.New("unexpected null in mount point")

// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry
// with invalid type.
type FSTypeError string

func (f FSTypeError) Error() string { return fmt.Sprintf("invalid filesystem type %q", string(f)) }

// FSImplError is returned for unsupported implementations of [FilesystemConfig].
type FSImplError struct{ Value FilesystemConfig }

func (f FSImplError) Error() string {
	implType := reflect.TypeOf(f.Value)
	var buf strings.Builder
	for implType != nil && implType.Kind() == reflect.Pointer {
		buf.WriteByte('*')
		implType = implType.Elem()
	}
	if implType != nil {
		buf.WriteString(implType.Name())
	} else {
		buf.WriteString("nil")
	}
	return "implementation " + buf.String() + " not supported"
}

// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
type FilesystemConfigJSON struct{ FilesystemConfig }

// Valid returns whether the [FilesystemConfigJSON] is valid.
func (f *FilesystemConfigJSON) Valid() bool {
	return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
}

// fsType holds the string representation of the concrete type of [FilesystemConfig].
type fsType struct {
	Type string `json:"type"`
}

func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
	if f == nil || f.FilesystemConfig == nil {
		return nil, ErrFSNull
	}
	var v any
	switch cv := f.FilesystemConfig.(type) {
	case *FSBind:
		v = &struct {
			fsType
			*FSBind
		}{fsType{FilesystemBind}, cv}

	case *FSEphemeral:
		v = &struct {
			fsType
			*FSEphemeral
		}{fsType{FilesystemEphemeral}, cv}

	case *FSOverlay:
		v = &struct {
			fsType
			*FSOverlay
		}{fsType{FilesystemOverlay}, cv}

	case *FSLink:
		v = &struct {
			fsType
			*FSLink
		}{fsType{FilesystemLink}, cv}

	case *FSDaemon:
		v = &struct {
			fsType
			*FSDaemon
		}{fsType{FilesystemDaemon}, cv}

	default:
		return nil, FSImplError{f.FilesystemConfig}
	}

	return json.Marshal(v)
}

func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
	t := new(fsType)
	if err := json.Unmarshal(data, &t); err != nil {
		return err
	}
	if t == nil {
		return ErrFSNull
	}
	switch t.Type {
	case FilesystemBind:
		*f = FilesystemConfigJSON{new(FSBind)}

	case FilesystemEphemeral:
		*f = FilesystemConfigJSON{new(FSEphemeral)}

	case FilesystemOverlay:
		*f = FilesystemConfigJSON{new(FSOverlay)}

	case FilesystemLink:
		*f = FilesystemConfigJSON{new(FSLink)}

	case FilesystemDaemon:
		*f = FilesystemConfigJSON{new(FSDaemon)}

	default:
		return FSTypeError(t.Type)
	}

	return json.Unmarshal(data, f.FilesystemConfig)
}
