package nix

import (
	"bufio"
	"context"
	"errors"
	"io"
	"iter"
	"os/exec"
	"path"
	"slices"
	"strings"
	"sync"
)

const (
	instantiatedPrefix    = "instantiated '"
	instantiatedSeparator = " -> "
	instantiatedFields    = 2

	// a reasonable starting buffer capacity: pretty close to average of test cases
	instantiatedInitialCap = 1 << 15
)

const (
	InstantiatedBadFields = iota
	InstantiatedUnexpectedQuotes
	InstantiatedNotAbsolute
)

type MalformedInstantiatedError struct {
	Message string
	Type    int
}

func (m *MalformedInstantiatedError) Error() string {
	switch m.Type {
	case InstantiatedBadFields:
		return "incorrect amount of fields in instantiated message"
	case InstantiatedUnexpectedQuotes:
		return "unexpected quotes in final instantiated field"
	case InstantiatedNotAbsolute:
		return "instantiated derivation path is not absolute"
	default:
		panic("unreachable")
	}
}

func (m *MalformedInstantiatedError) Is(err error) bool {
	var e *MalformedInstantiatedError
	if !errors.As(err, &e) {
		return false
	}
	if e == nil || m == nil {
		return e == m
	}
	return e.Type == m.Type
}

// InstantiatedDecoder interprets the verbose output of `nix build` and collects instantiated derivations.
type InstantiatedDecoder struct {
	err     error
	pt      func(string)
	drv     string
	scanner *bufio.Scanner
}

// Err returns the first error encountered by the [InstantiatedDecoder].
func (d *InstantiatedDecoder) Err() error { return errors.Join(d.err, d.scanner.Err()) }

// NewInstantiatedDecoder returns a [InstantiatedDecoder] struct for collecting derivations from r.
// Skipped lines are written to w.
func NewInstantiatedDecoder(r io.Reader, w io.Writer) *InstantiatedDecoder {
	passthrough := func(string) {}
	if w != nil {
		passthrough = func(v string) { _, _ = w.Write([]byte(v + "\n")) }
	}
	return &InstantiatedDecoder{scanner: bufio.NewScanner(r), pt: passthrough}
}

// Scan advances the decoder to the next instantiated path, skipping all unrelated lines in-between.
func (d *InstantiatedDecoder) Scan() bool {
	if d.err != nil {
		return false
	}
rescan:
	if !d.scanner.Scan() {
		return false
	}

	v := d.scanner.Text()
	if !strings.HasPrefix(v, instantiatedPrefix) {
		// write skipped lines to stderr
		d.pt(v)
		goto rescan
	}

	fields := strings.SplitN(v, instantiatedSeparator, instantiatedFields)
	if len(fields) != instantiatedFields {
		// no more than a single -> is expected
		d.err = &MalformedInstantiatedError{v, InstantiatedBadFields}
		return false
	}

	// very basic validation here: the output format is not fully understood
	if len(fields[1]) < 3 || fields[1][0] != '\'' || fields[1][len(fields[1])-1] != '\'' {
		d.err = &MalformedInstantiatedError{v, InstantiatedUnexpectedQuotes}
		return false
	}
	drv := fields[1][1 : len(fields[1])-1]

	if !path.IsAbs(drv) {
		d.err = &MalformedInstantiatedError{v, InstantiatedNotAbsolute}
		return false
	}

	d.drv = drv
	return true
}

// Text returns the most recent instantiated path generated by a call to [InstantiatedDecoder.Scan]
// as a newly allocated string holding its bytes.
func (d *InstantiatedDecoder) Text() string { return d.drv }

// Instantiated returns a non-reusable iterator over instantiated derivations collected from [io.Reader].
func (d *InstantiatedDecoder) Instantiated() iter.Seq[string] {
	return func(yield func(string) bool) {
		for d.Scan() {
			if !yield(d.Text()) {
				return
			}
		}
	}
}

// Decode collects and deduplicates derivations emitted by [InstantiatedDecoder].
func (d *InstantiatedDecoder) Decode() ([]string, error) {
	instantiated := make([]string, 0, instantiatedInitialCap)
	for drv := range d.Instantiated() {
		if len(instantiated) == cap(instantiated) {
			// grow the buffer exponentially to minimise copies
			instantiated = slices.Grow(instantiated, cap(instantiated))
		}
		instantiated = append(instantiated, drv)
	}

	slices.Sort(instantiated)
	return slices.Compact(instantiated), d.Err()
}

// InstantiatedEvaluator evaluates a nix installable and collects its instantiated derivations via [InstantiatedDecoder].
// This interprets verbose output of `nix build` and is certainly not the correct way to do it, but so far its results
// are significantly more complete than `nix-store -qR` and there does not appear to be a better way.
type InstantiatedEvaluator struct {
	// underlying nix program
	cmd *exec.Cmd
	// populated by Close
	waitErr error
	// synchronises access to waitErr
	waitMu sync.RWMutex

	*InstantiatedDecoder
}

// Err blocks until process exit and returns the first error encountered by the [InstantiatedEvaluator].
func (e *InstantiatedEvaluator) Err() error {
	e.waitMu.RLock()
	defer e.waitMu.RUnlock()
	return errors.Join(e.waitErr, e.InstantiatedDecoder.Err())
}

// NewInstantiatedEvaluator initialises an [InstantiatedEvaluator] struct and its underlying nix process.
func NewInstantiatedEvaluator(ctx Context, installable string) (*InstantiatedEvaluator, error) {
	c, cancel := context.WithCancel(ctx.Unwrap())
	e := &InstantiatedEvaluator{
		cmd: ctx.Nix(c, CommandBuild, installable,
			// 'instantiated' messages are only emitted when actually evaluating something
			FlagOption, OptionEvalCache, ValueFalse,
			// do not actually build anything
			FlagDryRun,
			// increase verbosity so nix outputs 'instantiated' messages
			FlagPrintBuildLogs, FlagDebug,
		),
	}

	stdout, stderr := ctx.Streams()
	e.cmd.Stdout = stdout

	// verbose output ends up on stderr in the current nix implementation
	if r, err := e.cmd.StderrPipe(); err != nil {
		cancel()
		return nil, err
	} else {
		e.InstantiatedDecoder = NewInstantiatedDecoder(r, stderr)
	}

	if err := e.cmd.Start(); err != nil {
		cancel()
		// have finalizer take care of the pipe
		return nil, err
	}
	e.waitMu.Lock()
	go func() { e.waitErr = e.cmd.Wait(); cancel(); e.waitMu.Unlock() }()

	return e, nil
}

// EvalInstantiated calls the underlying [InstantiatedDecoder.Decode] of a new [InstantiatedEvaluator].
func EvalInstantiated(ctx Context, installable string) ([]string, error) {
	evaluator, err := NewInstantiatedEvaluator(ctx, installable)
	if err != nil {
		return nil, err
	}
	instantiated, _ := evaluator.Decode() // error joined by Close
	return instantiated, evaluator.Err()
}
