package entropy

import (
	crand "crypto/rand"
	"crypto/sha512"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"sync"
)

const blockSize = sha512.Size

var ErrExternalEntropyRequired = errors.New("external entropy source required")

type HybridOptions struct {
	RequireExternal bool
}

type HybridReader struct {
	base            io.Reader
	external        io.Reader
	requireExternal bool

	mu         sync.Mutex
	counter    uint64
	degraded   bool
	degradeErr error
	buffer     []byte
}

func NewHybridReader(base io.Reader, external io.Reader, opts HybridOptions) (*HybridReader, error) {
	if base == nil {
		base = crand.Reader
	}
	if external == nil {
		return nil, ErrExternalEntropyRequired
	}

	return &HybridReader{
		base:            base,
		external:        external,
		requireExternal: opts.RequireExternal,
	}, nil
}

func (r *HybridReader) Read(p []byte) (int, error) {
	if len(p) == 0 {
		return 0, nil
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	written := 0
	for written < len(p) {
		if len(r.buffer) > 0 {
			n := copy(p[written:], r.buffer)
			written += n
			r.buffer = r.buffer[n:]
			continue
		}

		block, err := r.nextBlockLocked()
		if err != nil {
			return written, err
		}

		n := copy(p[written:], block[:])
		written += n
		if n < len(block) {
			r.buffer = append(r.buffer[:0], block[n:]...)
		}
	}

	return written, nil
}

func (r *HybridReader) Degraded() bool {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.degraded
}

func (r *HybridReader) DegradeError() error {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.degradeErr
}

func (r *HybridReader) nextBlockLocked() ([blockSize]byte, error) {
	var base [blockSize]byte
	if _, err := io.ReadFull(r.base, base[:]); err != nil {
		return [blockSize]byte{}, fmt.Errorf("read OS CSPRNG: %w", err)
	}

	if r.degraded {
		return base, nil
	}

	var external [blockSize]byte
	if _, err := io.ReadFull(r.external, external[:]); err != nil {
		if r.requireExternal {
			return [blockSize]byte{}, fmt.Errorf("read external entropy: %w", err)
		}
		r.degraded = true
		r.degradeErr = err
		return base, nil
	}

	var counter [8]byte
	binary.BigEndian.PutUint64(counter[:], r.counter)
	r.counter++

	h := sha512.New()
	h.Write([]byte("qpass hybrid entropy v1\x00"))
	h.Write(counter[:])
	h.Write(base[:])
	h.Write(external[:])

	var out [blockSize]byte
	copy(out[:], h.Sum(nil))
	return out, nil
}

func InstallGlobal(reader io.Reader) func() {
	if reader == nil {
		reader = crand.Reader
	}
	previous := crand.Reader
	crand.Reader = reader
	return func() {
		crand.Reader = previous
	}
}
