// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

//go:generate go run ./copier.go

// Copier is a tool to automate copy of govulncheck's internal files.
//
//   - copy golang.org/x/vuln/internal/osv/ to osv
//   - copy golang.org/x/vuln/internal/govulncheck/ to govulncheck
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"go/parser"
	"go/token"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"golang.org/x/tools/internal/edit"
)

func main() {
	log.SetPrefix("copier: ")
	log.SetFlags(log.Lshortfile)

	srcMod := "golang.org/x/vuln"
	srcModVers := "@latest"
	srcDir, srcVer := downloadModule(srcMod + srcModVers)

	cfg := rewrite{
		banner:        fmt.Sprintf("// Code generated by copying from %v@%v (go run copier.go); DO NOT EDIT.", srcMod, srcVer),
		srcImportPath: "golang.org/x/vuln/internal",
		dstImportPath: currentPackagePath(),
	}

	copyFiles("osv", filepath.Join(srcDir, "internal", "osv"), cfg)
	copyFiles("govulncheck", filepath.Join(srcDir, "internal", "govulncheck"), cfg)
}

type rewrite struct {
	// DO NOT EDIT marker to add at the beginning
	banner string
	// rewrite srcImportPath with dstImportPath
	srcImportPath string
	dstImportPath string
}

func copyFiles(dst, src string, cfg rewrite) {
	entries, err := os.ReadDir(src)
	if err != nil {
		log.Fatalf("failed to read dir: %v", err)
	}
	if err := os.MkdirAll(dst, 0777); err != nil {
		log.Fatalf("failed to create dir: %v", err)
	}

	for _, e := range entries {
		fname := e.Name()
		// we need only non-test go files.
		if e.IsDir() || !strings.HasSuffix(fname, ".go") || strings.HasSuffix(fname, "_test.go") {
			continue
		}
		data, err := os.ReadFile(filepath.Join(src, fname))
		if err != nil {
			log.Fatal(err)
		}
		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, fname, data, parser.ParseComments|parser.ImportsOnly)
		if err != nil {
			log.Fatalf("parsing source module:\n%s", err)
		}

		buf := edit.NewBuffer(data)
		at := func(p token.Pos) int {
			return fset.File(p).Offset(p)
		}

		// Add banner right after the copyright statement (the first comment)
		bannerInsert, banner := f.FileStart, cfg.banner
		if len(f.Comments) > 0 && strings.HasPrefix(f.Comments[0].Text(), "Copyright ") {
			bannerInsert = f.Comments[0].End()
			banner = "\n\n" + banner
		}
		buf.Replace(at(bannerInsert), at(bannerInsert), banner)

		// Adjust imports
		for _, spec := range f.Imports {
			path, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				log.Fatal(err)
			}
			if strings.HasPrefix(path, cfg.srcImportPath) {
				newPath := strings.Replace(path, cfg.srcImportPath, cfg.dstImportPath, 1)
				buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(newPath))
			}
		}
		data = buf.Bytes()

		if err := os.WriteFile(filepath.Join(dst, fname), data, 0666); err != nil {
			log.Fatal(err)
		}
	}
}

func downloadModule(srcModVers string) (dir, ver string) {
	var stdout, stderr bytes.Buffer
	cmd := exec.Command("go", "mod", "download", "-json", srcModVers)
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		log.Fatalf("go mod download -json %s: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes())
	}
	var info struct {
		Dir     string
		Version string
	}
	if err := json.Unmarshal(stdout.Bytes(), &info); err != nil {
		log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes())
	}
	return info.Dir, info.Version
}

func currentPackagePath() string {
	var stdout, stderr bytes.Buffer
	cmd := exec.Command("go", "list", ".")
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		log.Fatalf("go list: %v\n%s%s", err, stderr.Bytes(), stdout.Bytes())
	}
	return strings.TrimSpace(stdout.String())
}
