package store

import (
	"errors"
	"fmt"
	"iter"
	"os"
	"strconv"
	"sync"

	"hakurei.app/check"
	"hakurei.app/hst"
	"hakurei.app/internal/lockedfile"
)

// EntryHandle is a handle on a state entry retrieved from a [Handle].
//
// Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct {
	// Error returned while decoding pathname.
	// A non-nil value disables EntryHandle.
	DecodeErr error

	// Checked pathname to entry file.
	Pathname *check.Absolute

	hst.ID
}

// open opens the underlying state entry file.
//
// A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
	if eh.DecodeErr != nil {
		return nil, eh.DecodeErr
	}

	if f, err := os.OpenFile(eh.Pathname.String(), flag, perm); err != nil {
		return nil, &hst.AppError{Step: "open state entry", Err: err}
	} else {
		return f, nil
	}
}

// Destroy removes the underlying state entry.
//
// A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error {
	// destroy does not go through open
	if eh.DecodeErr != nil {
		return eh.DecodeErr
	}

	if err := os.Remove(eh.Pathname.String()); err != nil {
		return &hst.AppError{Step: "destroy state entry", Err: err}
	}
	return nil
}

// save encodes [hst.State] and writes it to the underlying file.
//
// An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config].
//
// A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error {
	f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		return err
	}

	err = entryEncode(f, state)
	if closeErr := f.Close(); closeErr != nil && err == nil {
		err = &hst.AppError{Step: "close state file", Err: closeErr}
	}
	return err
}

// Load loads and validates the state entry header, and returns the
// [hst.Enablements] byte. For a non-nil v, the full state payload is decoded
// and stored in the value pointed to by v.
//
// Load validates the embedded [hst.Config] value. A non-nil error returned by
// Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
	f, err := eh.open(os.O_RDONLY, 0)
	if err != nil {
		return 0, err
	}

	var et hst.Enablements
	if v != nil {
		et, err = entryDecode(f, v)
		if err == nil && v.ID != eh.ID {
			err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
				Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
		}
	} else {
		et, err = entryDecodeHeader(f)
	}

	if closeErr := f.Close(); closeErr != nil && err == nil {
		err = &hst.AppError{Step: "close state file", Err: closeErr}
	}
	return et, err
}

// Handle is a handle on a [Store] segment.
//
// Initialised by [Store.Handle].
type Handle struct {
	// Identity of instances tracked by this segment.
	Identity int
	// Pathname of directory that the segment referred to by Handle is rooted in.
	Path *check.Absolute

	// Inter-process mutex to synchronise operations against resources in this segment.
	// Must not be held directly, callers should use [Handle.Lock] instead.
	fileMu *lockedfile.Mutex
	// Must be held alongside fileMu.
	mu sync.Mutex
}

// Lock attempts to acquire a lock on [Handle]. If successful, Lock returns a
// non-nil unlock function.
//
// A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) {
	if unlock, err = h.fileMu.Lock(); err != nil {
		return nil, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.Identity), Err: err}
	}
	return
}

// Save attempts to save [hst.State] as a segment entry, and returns its
// [EntryHandle]. Must be called while holding [Handle.Lock].
//
// An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config].
//
// A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
	eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
	return &eh, eh.save(state)
}

// Entries returns an iterator over all [EntryHandle] held in this segment. Must
// be called while holding [Handle.Lock].
//
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier
// and is of type [hst.AppError]. A non-nil error returned by Entries is of type
// [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
	// for error reporting
	const step = "read store segment entries"

	// read directory contents, should only contain storeMutexName and identifier
	var entries []os.DirEntry
	if pl, err := os.ReadDir(h.Path.String()); err != nil {
		return nil, -1, &hst.AppError{Step: step, Err: err}
	} else {
		entries = pl
	}

	// expects lock file
	l := len(entries)
	if l > 0 {
		l--
	}

	return func(yield func(*EntryHandle) bool) {
		for _, ent := range entries {
			var eh = EntryHandle{Pathname: h.Path.Append(ent.Name())}

			// this should never happen
			if ent.IsDir() {
				eh.DecodeErr = &hst.AppError{Step: step,
					Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
				goto out
			}

			// silently skip lock file
			if ent.Name() == MutexName {
				continue
			}

			// this either indicates a serious bug or external interference
			if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
				eh.DecodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
				goto out
			}

		out:
			if !yield(&eh) {
				break
			}
		}
	}, l, nil
}

// newHandle returns the address of a new segment [Handle] rooted in base.
func newHandle(base *check.Absolute, identity int) *Handle {
	h := Handle{Identity: identity, Path: base.Append(strconv.Itoa(identity))}
	h.fileMu = lockedfile.MutexAt(h.Path.Append(MutexName).String())
	return &h
}
