// Copyright 2023 Google LLC
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This task driver runs various Go linters via Bazel, and fails on any errors or Git diffs.
package main

import (
	"context"
	"flag"
	"fmt"
	"path/filepath"
	"strings"

	sk_exec "go.skia.org/infra/go/exec"
	"go.skia.org/infra/task_driver/go/lib/bazel"
	"go.skia.org/infra/task_driver/go/lib/os_steps"
	"go.skia.org/infra/task_driver/go/td"
	"go.skia.org/skia/infra/bots/task_drivers/common"
)

var (
	// Required properties for this task.
	// We want the cache to be on a bigger disk than default. The root disk, where the home
	// directory (and default Bazel cache) lives, is only 15 GB on our GCE VMs.
	gitPath   = flag.String("git_path", "", "Location of git binary to use for diffs.")
	projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
	taskId    = flag.String("task_id", "", "ID of this task.")
	taskName  = flag.String("task_name", "", "Name of the task.")
	workdir   = flag.String("workdir", ".", "Working directory, the root directory of a full Skia checkout")
	// Optional flags.
	local  = flag.Bool("local", false, "True if running locally (as opposed to on the CI/CQ)")
	output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)

func main() {
	bazelFlags := common.MakeBazelFlags(common.MakeBazelFlagsOpts{
		AdditionalArgs: true,
	})

	// StartRun calls flag.Parse()
	ctx := td.StartRun(projectId, taskId, taskName, output, local)
	defer td.EndRun(ctx)

	bazelFlags.Validate(ctx)

	if *gitPath == "" {
		td.Fatal(ctx, fmt.Errorf("--git_path is required"))
	}

	wd, err := os_steps.Abs(ctx, *workdir)
	if err != nil {
		td.Fatal(ctx, err)
	}
	absGit, err := os_steps.Abs(ctx, *gitPath)
	if err != nil {
		td.Fatal(ctx, err)
	}
	if _, err := os_steps.Stat(ctx, absGit); err != nil {
		fmt.Printf("Cannot stat git binary %s\n", absGit)
		td.Fatal(ctx, err)
	}
	skiaPath := filepath.Join(wd, "skia")

	// When running on the CI, there is not a git checkout here, so we make a temp one.
	if !*local {
		if err := gitInit(ctx, absGit, skiaPath); err != nil {
			td.Fatal(ctx, err)
		}
	}

	opts := bazel.BazelOptions{
		CachePath: *bazelFlags.CacheDir,
	}
	if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil {
		td.Fatal(ctx, err)
	}

	if err := bazelRun(ctx, skiaPath, "//:go", append(*bazelFlags.AdditionalArgs, "--", "fmt", "./...")...); err != nil {
		td.Fatal(ctx, err)
	}

	if err := checkGitDiff(ctx, absGit, skiaPath); err != nil {
		td.Fatal(ctx, err)
	}

	if !*local {
		if err := common.BazelCleanIfLowDiskSpace(ctx, *bazelFlags.CacheDir, skiaPath, "bazelisk"); err != nil {
			td.Fatal(ctx, err)
		}
	}
}

// bazelRun runs the given Bazel label from the Skia path with any given args using Bazelisk.
func bazelRun(ctx context.Context, skiaPath, label string, args ...string) error {
	return td.Do(ctx, td.Props("bazel run "+label), func(ctx context.Context) error {
		runCmd := &sk_exec.Command{
			Name:       "bazelisk",
			Args:       append([]string{"run", label}, args...),
			InheritEnv: true, // Need to make sure bazelisk is on the path
			Dir:        skiaPath,
			LogStdout:  true,
			LogStderr:  true,
		}
		_, err := sk_exec.RunCommand(ctx, runCmd)
		if err != nil {
			return err
		}
		return nil
	})
}

// gitInit creates a temporary git repository with all files in the Skia path. This allows the later
// git diff call to work properly. This is necessary because our current Swarming setup does not
// include the .git folder when copying down files.
func gitInit(ctx context.Context, gitPath, skiaPath string) error {
	step := fmt.Sprintf("Setting git baseline in %s", skiaPath)
	err := td.Do(ctx, td.Props(step), func(ctx context.Context) error {
		initCmd := &sk_exec.Command{
			Name:       gitPath,
			Args:       []string{"init"},
			InheritEnv: false,
			Dir:        skiaPath,
			LogStdout:  true,
			LogStderr:  true,
		}
		if _, err := sk_exec.RunCommand(ctx, initCmd); err != nil {
			return err
		}
		addCmd := &sk_exec.Command{
			Name:       gitPath,
			Args:       []string{"add", "."},
			InheritEnv: false,
			Dir:        skiaPath,
			LogStdout:  true,
			LogStderr:  true,
		}
		if _, err := sk_exec.RunCommand(ctx, addCmd); err != nil {
			return err
		}
		commitCmd := &sk_exec.Command{
			Name:       gitPath,
			Args:       []string{"commit", "-m", "baseline commit"},
			InheritEnv: false,
			Dir:        skiaPath,
			LogStdout:  true,
			LogStderr:  true,
		}
		if _, err := sk_exec.RunCommand(ctx, commitCmd); err != nil {
			return err
		}
		return nil
	})
	return err
}

// checkGitDiff runs git diff and returns error if the diff is non-empty.
func checkGitDiff(ctx context.Context, gitPath, skiaPath string) error {
	step := fmt.Sprintf("git diff %s", skiaPath)
	return td.Do(ctx, td.Props(step), func(ctx context.Context) error {
		runCmd := &sk_exec.Command{
			Name:       gitPath,
			Args:       []string{"diff", "--no-ext-diff"},
			InheritEnv: false,
			Dir:        skiaPath,
			LogStdout:  true,
			LogStderr:  true,
		}
		rv, err := sk_exec.RunCommand(ctx, runCmd)
		if err != nil {
			return err
		}
		if strings.TrimSpace(rv) != "" {
			return fmt.Errorf("Non-empty diff:\n" + rv)
		}
		return nil
	})
}
