// Package hydrant runs hydrant inside a go program through its C api.
//
// hydrant keeps serving its usual http api, so after Start you talk to it over the bind you
// gave it, ideally a unix socket. link against libhydrant_ffi.a by putting its directory in
// CGO_LDFLAGS, eg. CGO_LDFLAGS=-L/path/to/target/release.
package hydrant

/*
#cgo CFLAGS: -I${SRCDIR}/../include
#cgo LDFLAGS: -lhydrant_ffi
#cgo darwin LDFLAGS: -framework SystemConfiguration -framework Security -framework CoreFoundation -liconv -lm
// no -lgcc_s because musl has no shared one, and gcc already pulls in an unwinder
// either way (libgcc_s when dynamic, libgcc_eh when static)
#cgo linux LDFLAGS: -lutil -lrt -lpthread -lm -ldl
#include <stdlib.h>
#include "hydrant.h"
*/
import "C"

import (
	"encoding/json"
	"errors"
	"fmt"
	"os/signal"
	"syscall"
	"time"
	"unsafe"
)

// ErrShutdown is what Err reports after Shutdown stopped hydrant.
var ErrShutdown = errors.New("hydrant was shut down")

// Hydrant is a hydrant running in this process, until Shutdown.
type Hydrant struct {
	handle *C.hydrant_t
	done   chan struct{}
	err    error
}

// Start runs hydrant with the settings its binary reads from the environment, like
// HYDRANT_DATABASE_PATH and RUST_LOG, given as a map instead. the process environment is
// ignored. HYDRANT_API_BIND is required, and "none" runs without an api. hydrant binds it in
// the background, so the api may need a moment before it answers. it also ignores SIGPIPE
// for the whole process, so writing to a closed pipe returns EPIPE instead of exiting.
func Start(settings map[string]string) (*Hydrant, error) {
	raw, err := json.Marshal(settings)
	if err != nil {
		return nil, fmt.Errorf("encode hydrant settings: %w", err)
	}
	cSettings := C.CString(string(raw))
	defer C.free(unsafe.Pointer(cSettings))

	// hydrant writes to sockets whose peer may be gone, and on linux that raises
	// SIGPIPE on a rust thread, which go doesn't handle and so the default kills
	// the whole process. rust binaries ignore it at startup, and this does the same
	signal.Ignore(syscall.SIGPIPE)

	var cErr *C.char
	handle := C.hydrant_start(cSettings, &cErr)
	if handle == nil {
		return nil, takeErr(cErr)
	}

	h := &Hydrant{handle: handle, done: make(chan struct{})}
	go func() {
		var cErr *C.char
		if C.hydrant_wait(handle, &cErr) != 0 {
			h.err = takeErr(cErr)
		} else {
			h.err = ErrShutdown
		}
		close(h.done)
	}()
	return h, nil
}

// Done is closed once hydrant stops, because it failed or Shutdown stopped it. a failed
// hydrant still holds its database until Shutdown.
func (h *Hydrant) Done() <-chan struct{} {
	return h.done
}

// Shutdown stops hydrant and waits up to timeout for its database to close, so its memory
// is given back and Start can open the same database again. whatever hydrant was in the
// middle of is cut off like a crash, which it recovers from on the next start. call it
// once, from one goroutine, also after a failure. it errors if the database is still open
// after timeout, and the Hydrant can't be used again either way.
func (h *Hydrant) Shutdown(timeout time.Duration) error {
	if h.handle == nil {
		return errors.New("hydrant was already shut down")
	}
	var cErr *C.char
	var err error
	if C.hydrant_shutdown(h.handle, C.uint64_t(timeout.Milliseconds()), &cErr) != 0 {
		err = takeErr(cErr)
	}
	// the wait goroutine still uses the handle until it sees the stop
	<-h.done
	C.hydrant_free(h.handle)
	h.handle = nil
	return err
}

// Err says why hydrant stopped. it's nil while hydrant is running.
func (h *Hydrant) Err() error {
	select {
	case <-h.done:
		return h.err
	default:
		return nil
	}
}

func takeErr(cErr *C.char) error {
	if cErr == nil {
		return errors.New("hydrant failed without saying why")
	}
	defer C.hydrant_free_string(cErr)
	return errors.New(C.GoString(cErr))
}
