package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	pb "code.tvl.fyi/tools/depot-scanner/proto"
)

var nixInstantiatePath = flag.String("nix-bin", "/run/current-system/sw/bin/nix-instantiate", "path to nix-instantiate")
var depotRoot = flag.String("depot", envOr("DEPOT_ROOT", "/depot/"), "path to tvl.fyi depot at current canon")
var nixStoreRoot = flag.String("store-path", "/nix/store/", "prefix for all valid nix store paths")

var modeFlag = flag.String("mode", modeArchive, "operation mode. valid values: tar, print")
var onlyFlag = flag.String("only", "", "only enable the listed output types, comma separated. valid values: DEPOT, STORE, CORE, UNKNOWN")
var relativeFlag = flag.Bool("relpath", false, "when printing paths, print them relative to the root of their path type")

const (
	modeArchive = "tar"
	modePrint   = "print"
)

const (
	// String that identifies a path as belonging to nix corepkgs.
	corePkgsString = "/share/nix/corepkgs/"

	depotTraceString = "trace: depot-scan: "
)

type fileScanType int

const (
	unknownPath fileScanType = iota
	depotPath
	nixStorePath
	corePkgsPath
)

func launchNix(attr string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
	cmd := exec.Command(*nixInstantiatePath, "--trace-file-access", "-A", attr)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, nil, nil, err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		stdout.Close()
		return nil, nil, nil, err
	}

	err = cmd.Start()
	if err != nil {
		stdout.Close()
		stderr.Close()
		return nil, nil, nil, err
	}

	return cmd, stdout, stderr, nil
}

func categorizePath(path string) fileScanType {
	if strings.HasPrefix(path, *nixStoreRoot) {
		if strings.Contains(path, corePkgsString) {
			return corePkgsPath
		}
		return nixStorePath
	} else if strings.HasPrefix(path, *depotRoot) {
		return depotPath
	} else if strings.Contains(path, corePkgsString) {
		return corePkgsPath
	}
	return unknownPath
}

func addPath(path string, out map[fileScanType]map[string]struct{}) {
	cat := categorizePath(path)
	if out[cat] == nil {
		out[cat] = make(map[string]struct{})
	}

	out[cat][path] = struct{}{}
}

func consumeOutput(stdout, stderr io.ReadCloser) (map[fileScanType]map[string]struct{}, string, error) {
	result := make(map[fileScanType]map[string]struct{})

	scanner := bufio.NewScanner(stderr)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, depotTraceString) {
			addPath(strings.TrimPrefix(line, depotTraceString), result)
		} else {
			// print remaining stderr output of nix-instantiate
			// to prevent silent swallowing of possible important
			// error messages (e.g. about command line interface changes)
			fmt.Fprintf(os.Stderr, "nix-inst> %s\n", line)
		}
	}
	if scanner.Err() != nil {
		return nil, "", scanner.Err()
	}

	// Get derivation path
	derivPath := ""
	scanner = bufio.NewScanner(stdout)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, *nixStoreRoot) {
			derivPath = line
			// consume the rest of the output
		}
	}
	if scanner.Err() != nil {
		return nil, "", scanner.Err()
	}

	return result, derivPath, nil
}

func main() {
	flag.Parse()

	checkDepotRoot()

	enabledPathTypes := make(map[pb.PathType]bool, 4)
	if len(*onlyFlag) > 0 {
		enabledOutputs := strings.Split(*onlyFlag, ",")
		for _, v := range enabledOutputs {
			i, ok := pb.PathType_value[strings.ToUpper(v)]
			if !ok {
				fmt.Fprintln(os.Stderr, "warning: unrecognized PathType name: ", v)
				continue
			}
			enabledPathTypes[pb.PathType(i)] = true
		}
	} else {
		// Default
		enabledPathTypes = map[pb.PathType]bool{
			pb.PathType_UNKNOWN: true,
			pb.PathType_DEPOT:   true,
			pb.PathType_STORE:   true,
			pb.PathType_CORE:    true,
		}
	}

	cmd, stdout, stderr, err := launchNix(flag.Arg(0))
	if err != nil {
		panic(fmt.Errorf("could not launch nix: %w", err))
	}
	results, derivPath, err := consumeOutput(stdout, stderr)
	if err != nil {
		err2 := cmd.Wait()
		if err2 != nil {
			panic(fmt.Errorf("nix-instantiate failed: %w\nadditionally, while reading output: %w", err2, err))
		}
		panic(fmt.Errorf("problem reading nix output: %w", err))
	}
	err = cmd.Wait()
	if err != nil {
		panic(fmt.Errorf("nix-instantiate failed: %w", err))
	}

	_ = derivPath

	if *modeFlag == "print" {
		if enabledPathTypes[pb.PathType_STORE] {
			for k, _ := range results[nixStorePath] {
				if *relativeFlag {
					k = strings.TrimPrefix(k, *nixStoreRoot)
					k = strings.TrimPrefix(k, "/")
				}
				fmt.Println(k)
			}
		}
		if enabledPathTypes[pb.PathType_DEPOT] {
			for k, _ := range results[depotPath] {
				if *relativeFlag {
					k = strings.TrimPrefix(k, *depotRoot)
					k = strings.TrimPrefix(k, "/")
				}
				fmt.Println(k)
			}
		}
		if enabledPathTypes[pb.PathType_CORE] {
			for k, _ := range results[corePkgsPath] {
				// TODO relativeFlag
				fmt.Println(k)
			}
		}
		if enabledPathTypes[pb.PathType_UNKNOWN] {
			for k, _ := range results[unknownPath] {
				fmt.Println(k)
			}
		}
	} else {
		panic("unimplemented")
	}
}

func envOr(envVar, def string) string {
	v := os.Getenv(envVar)
	if v == "" {
		return def
	}
	return v
}

func checkDepotRoot() {
	if *depotRoot == "" {
		fmt.Fprintln(os.Stderr, "error: DEPOT_ROOT / -depot not set")
		os.Exit(2)
	}
	_, err := os.Stat(*depotRoot)
	if os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "error: %q does not exist\ndid you forget to set DEPOT_ROOT / --depot ?\n", *depotRoot)
		os.Exit(1)
	} else if err != nil {
		fmt.Fprintf(os.Stderr, "error: could not stat %q: %v\n", *depotRoot, err)
		os.Exit(1)
	}

}
