package nixbuild

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

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

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

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
	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.
func NewInstantiatedDecoder(r io.Reader) *InstantiatedDecoder {
	return &InstantiatedDecoder{scanner: bufio.NewScanner(r)}
}

// Drv returns a non-reusable iterator over instantiated derivations collected from [io.Reader].
func (d *InstantiatedDecoder) Drv() iter.Seq[string] {
	return func(yield func(string) bool) {
		for d.scanner.Scan() {
			v := d.scanner.Text()
			if !strings.HasPrefix(v, instantiatedPrefix) {
				// write skipped lines to stderr
				if Stderr != nil {
					_, _ = Stderr.Write([]byte(v + "\n"))
				}
				continue
			}

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

			// 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}
				break
			}
			drv := fields[1][1 : len(fields[1])-1]

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

			if !yield(drv) {
				break
			}
		}
	}
}

// Decode collects and deduplicates derivations emitted by [InstantiatedDecoder].
func (d *InstantiatedDecoder) Decode() ([]string, error) {
	instantiated := make([]string, 0, instantiatedInitialCap)
	for drv := range d.Drv() {
		if len(instantiated) == cap(instantiated) {
			// grow the buffer exponentially to minimise copies
			instantiated = slices.Grow(instantiated, cap(instantiated)<<1)
		}
		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
	// kills the nix command
	cancel context.CancelFunc
	// populated by Close
	waitErr error

	*InstantiatedDecoder
}

// Err returns the first error encountered by the [InstantiatedEvaluator].
func (e *InstantiatedEvaluator) Err() error {
	return errors.Join(e.waitErr, e.InstantiatedDecoder.Err())
}

// NewInstantiatedEvaluator initialises an [InstantiatedEvaluator] struct and its underlying nix process.
func NewInstantiatedEvaluator(ctx context.Context, installable string) (*InstantiatedEvaluator, error) {
	c, cancel := context.WithCancel(ctx)
	e := &InstantiatedEvaluator{
		cmd: Nix(c, "build", installable,
			// 'instantiated' messages are only emitted when actually evaluating something
			"--option", "eval-cache", "false",
			// do not actually build anything
			"--dry-run",
			// increase verbosity so nix outputs 'instantiated' messages
			"-Lvvv",
		),
		cancel: cancel,
	}

	e.cmd.Stdout = Stdout

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

	if err := e.cmd.Start(); err != nil {
		cancel()
		// have finalizer take care of the pipe
		return nil, err
	}

	runtime.SetFinalizer(e, (*InstantiatedEvaluator).Close)
	return e, nil
}

// Close releases system resources held by [InstantiatedEvaluator].
func (e *InstantiatedEvaluator) Close() error {
	if e.cmd.ProcessState != nil {
		return syscall.EBADE
	}

	e.cancel()
	e.waitErr = e.cmd.Wait()
	runtime.SetFinalizer(e, nil)
	return e.Err()
}

// EvalInstantiated calls the underlying [InstantiatedDecoder.Decode] of a new [InstantiatedEvaluator].
func EvalInstantiated(ctx context.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.Close()
}
