package report

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"hakurei.app/check"
)

const (
	// Trivial denotes an error that may happen frequently under normal operation.
	Trivial = iota
	// Inconsistent denotes an error diagnosed due to inconsistent state.
	Inconsistent
	// Degraded denotes an error condition causing a degraded state.
	Degraded
	// Fatal denotes an unrecoverable error. This is generally followed by
	// notifying the user and suggesting immediate restart, or an automatic
	// restart of a nonessential daemon process.
	Fatal
)

// A Reporter represents an error reporting object backed by user-facing display
// and optional persistent storage. A Reporter can be used simultaneously from
// multiple goroutines.
type Reporter struct {
	// Backing logger, the zero value implies the return value of [log.Default].
	log atomic.Pointer[log.Logger]
	// Backing persistent storage.
	pathname atomic.Pointer[check.Absolute]
	// Reporting behaviour.
	flag atomic.Uint64

	// Caller-supplied reporting functions.
	notify []func(e Error)
	// Synchronises access to notify.
	notifyMu sync.RWMutex

	// Synchronises access to persistent storage.
	mu sync.Mutex
}

// SetOutput sets the backing [log.Logger].
func (r *Reporter) SetOutput(l *log.Logger) { r.log.Store(l) }

// SetPathname sets the pathname to persistent storage.
func (r *Reporter) SetPathname(a *check.Absolute) { r.pathname.Store(a) }

const (
	// DStrict causes all dispatches to end in a call to panic.
	DStrict = 1 << iota
	// DNoRecover disallows recovering from error write to persistent storage.
	DNoRecover
	// DBypassEarly allows persistent storage to be skipped before it is ready.
	DBypassEarly
)

// SetFlags sets flags for r.
func (r *Reporter) SetFlags(flag uint64) { r.flag.Store(flag) }

// An Error represents an error reported via [Reporter.Dispatch].
type Error struct {
	// Nature of error.
	Severity int
	// User-facing reporting message.
	Message string
	// Underlying error.
	Err error
}

// RepresentableError is implemented by errors friendly to JSON serialisation.
type RepresentableError interface {
	error
	Representable()
}

// MarshalJSON returns an incomplete JSON representation of e.
func (e Error) MarshalJSON() (data []byte, err error) {
	v := struct {
		Severity any             `json:"kind"`
		Message  string          `json:"message"`
		Err      json.RawMessage `json:"error"`
	}{Message: e.Message}

	switch e.Severity {
	case Trivial:
		v.Severity = "trivial"
	case Inconsistent:
		v.Severity = "inconsistent"
	case Degraded:
		v.Severity = "degradation"
	case Fatal:
		v.Severity = "fatal"
	default:
		v.Severity = e.Severity
	}

	if re, ok := errors.AsType[RepresentableError](e.Err); ok {
		v.Err, err = json.Marshal(re)
	} else {
		v.Err, err = json.Marshal(e.Err.Error())
	}
	if err != nil {
		return
	}
	return json.Marshal(&v)
}

// Error is generally only called during a [Reporter.Dispatch] call ending in
// a panic or an otherwise unrecoverable condition.
func (e Error) Error() string {
	return fmt.Sprintf("%s: %v", e.Message, e.Err)
}

// Notify arranges for f to be called for all future dispatched errors.
func (r *Reporter) Notify(f func(Error)) {
	r.notifyMu.Lock()
	r.notify = append(r.notify, f)
	r.notifyMu.Unlock()
}

// getLog returns the address of the underlying [log.Logger], or the return
// value of [log.Default].
func (r *Reporter) getLog() *log.Logger {
	l := r.log.Load()
	if l == nil {
		l = log.Default()
	}
	return l
}

// dispatch implements Dispatch but returns the reporting error.
func (r *Reporter) dispatch(severity int, message string, e error) (err error) {
	p := Error{severity, message, e}
	r.getLog().Println(message+":", e)

	flag := r.flag.Load()
	defer func() {
		if flag&DNoRecover != 0 && err != nil {
			panic(err)
		}
	}()

	if a := r.pathname.Load(); a != nil {
		var w *os.File
		r.mu.Lock()
		w, err = os.OpenFile(
			a.Append(strconv.FormatUint(uint64(time.Now().UnixNano()), 10)).String(),
			os.O_CREATE|os.O_EXCL|os.O_WRONLY,
			0400,
		)
		if err != nil {
			r.mu.Unlock()
			return
		}
		err = json.NewEncoder(w).Encode(p)
		if _err := w.Close(); err == nil {
			err = _err
		}
		r.mu.Unlock()
	} else if flag&DBypassEarly == 0 && severity != Trivial {
		panic(p)
	}

	if flag&DStrict != 0 && severity != Trivial {
		panic(p)
	}

	r.notifyMu.RLock()
	defer r.notifyMu.RUnlock()
	for _, f := range r.notify {
		f(p)
	}
	return
}

// Dispatch reports an error and saves it to backing storage if available.
func (r *Reporter) Dispatch(severity int, message string, e error) {
	if err := r.dispatch(severity, message, e); err != nil {
		r.getLog().Println(err)
	}
}
