package main

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"

	"github.com/coreos/go-semver/semver"
	"golang.org/x/net/html"
)

type FileInfo struct {
	FileName string
	Regex    string
	Replace  string
}

func (f FileInfo) Update() error {
	b, err := os.ReadFile(f.FileName)
	if err != nil {
		return err
	}

	re := regexp.MustCompile(f.Regex)
	newContents := re.ReplaceAll(b, []byte(f.Replace))

	err = os.WriteFile(f.FileName, newContents, 0640)
	if err != nil {
		return err
	}

	return nil
}

// removeZeroPatch cleans version in case the user provides the minor version as "1.19.0" but require "1.19"
func removeZeroPatch(version string) string {
	if strings.HasSuffix(version, ".0") {
		return strings.Trim(version, ".0")
	}
	return version
}

// removePatch cleans version from "1.20.1" to "1.20" (think go.mod entry)
func removePatch(version string) string {
	verInfo := semver.New(version)
	return fmt.Sprintf("%d.%d", verInfo.Major, verInfo.Minor)
}

// findHash will search the downloads table for the hashes matching the artifacts list
func findHashes(body io.Reader, version string) (map[string]string, error) {
	version = removeZeroPatch(version)

	htmlTokens := html.NewTokenizer(body)
	artifacts := []string{
		fmt.Sprintf("go%s.linux-amd64.tar.gz", version),
		fmt.Sprintf("go%s.darwin-arm64.tar.gz", version),
		fmt.Sprintf("go%s.darwin-amd64.tar.gz", version),
	}

	var insideDownloadTable bool
	var currentRow string
	hashes := make(map[string]string)

	for {
		tokenType := htmlTokens.Next()

		//if it's an error token, we either reached
		//the end of the file, or the HTML was malformed
		if tokenType == html.ErrorToken {
			err := htmlTokens.Err()
			if errors.Is(err, io.EOF) {
				//end of the file, break out of the loop
				break
			}
			return nil, htmlTokens.Err()
		}

		if tokenType == html.StartTagToken {
			//get the token
			token := htmlTokens.Token()
			if "table" == token.Data && len(token.Attr) == 1 && token.Attr[0].Val == "downloadtable" {
				insideDownloadTable = true
			}

			if insideDownloadTable && token.Data == "a" && len(token.Attr) == 2 {
				for _, f := range artifacts {
					// Check if the current row matches a desired file
					if strings.Contains(token.Attr[1].Val, f) {
						currentRow = f
						break
					}
				}
			}

			if currentRow != "" && token.Data == "tt" {
				//the next token should be the page title
				tokenType = htmlTokens.Next()
				//just make sure it's actually a text token
				if tokenType == html.TextToken {
					hashes[currentRow] = htmlTokens.Token().Data
					currentRow = ""
				}
			}
		}

		// Found a hash for each filename
		if len(hashes) == len(artifacts) {
			break
		}

		// Reached end of table
		if tokenType == html.EndTagToken && htmlTokens.Token().Data == "table" {
			if len(hashes) == 0 {
				return nil, fmt.Errorf("could not find version %q on downloads page", version)
			}

			return nil, fmt.Errorf("only found %d hashes expected %d: %v", len(hashes), len(artifacts), hashes)
		}
	}

	return hashes, nil
}

func getHashes(version string) (map[string]string, error) {
	resp, err := http.Get(`https://go.dev/dl/`)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return findHashes(resp.Body, version)
}

func main() {
	version := os.Args[1]
	// Handle situation user accidentally provides version as "v1.19.2"
	if strings.HasPrefix(version, "v") {
		version = strings.TrimLeft(version, "v")
	}

	hashes, err := getHashes(version)
	if err != nil {
		log.Fatal(err)
	}

	zeroPatchVersion := removeZeroPatch(version)
	noPatchVersion := removePatch(version)

	files := []FileInfo{
		{
			FileName: ".circleci/config.yml",
			Regex:    `(quay\.io\/influxdb\/telegraf-ci):(\d.\d*.\d)`,
			Replace:  fmt.Sprintf("$1:%s", version),
		},
		{
			FileName: ".github/workflows/govulncheck.yml",
			Regex:    `(go-version-input).*`,
			Replace:  fmt.Sprintf("$1: %s", version),
		},
		{
			FileName: "go.mod",
			Regex:    `(go)\s(\d.\d*)`,
			Replace:  fmt.Sprintf("$1 %s", noPatchVersion),
		},
		{
			FileName: "Makefile",
			Regex:    `(quay\.io\/influxdb\/telegraf-ci):(\d.\d*.\d)`,
			Replace:  fmt.Sprintf("$1:%s", version),
		},
		{
			FileName: "README.md",
			Regex:    `(Telegraf requires Go version) (\d.\d*)`,
			Replace:  fmt.Sprintf("$1 %s", noPatchVersion),
		},
		{
			FileName: "scripts/ci.docker",
			Regex:    `(FROM golang):(\d.\d*.\d)`,
			Replace:  fmt.Sprintf("$1:%s", zeroPatchVersion),
		},
		{
			FileName: "scripts/installgo_linux.sh",
			Regex:    `(GO_VERSION)=("\d.\d*.\d")`,
			Replace:  fmt.Sprintf("$1=%q", zeroPatchVersion),
		},
		{
			FileName: "scripts/installgo_mac.sh",
			Regex:    `(GO_VERSION)=("\d.\d*.\d")`,
			Replace:  fmt.Sprintf("$1=%q", zeroPatchVersion),
		},
		{
			FileName: "scripts/installgo_windows.sh",
			Regex:    `(GO_VERSION)=("\d.\d*.\d")`,
			Replace:  fmt.Sprintf("$1=%q", zeroPatchVersion),
		},
		{
			FileName: "scripts/installgo_linux.sh",
			Regex:    `(GO_VERSION_SHA)=".*"`,
			Replace:  fmt.Sprintf("$1=%q", hashes[fmt.Sprintf("go%s.linux-amd64.tar.gz", zeroPatchVersion)]),
		},
		{
			FileName: "scripts/installgo_mac.sh",
			Regex:    `(GO_VERSION_SHA_arm64)=".*"`,
			Replace:  fmt.Sprintf("$1=%q", hashes[fmt.Sprintf("go%s.darwin-arm64.tar.gz", zeroPatchVersion)]),
		},
		{
			FileName: "scripts/installgo_mac.sh",
			Regex:    `(GO_VERSION_SHA_amd64)=".*"`,
			Replace:  fmt.Sprintf("$1=%q", hashes[fmt.Sprintf("go%s.darwin-amd64.tar.gz", zeroPatchVersion)]),
		},
	}

	for _, f := range files {
		fmt.Printf("Updating %s \n", f.FileName)
		err := f.Update()
		if err != nil {
			log.Panic(err)
		}
	}
}
