/*
Branch Release creates a new Boulder hotfix release branch and pushes it to
GitHub. It ensures that the release branch has a standard name, and starts at
a previously-tagged mainline release.

The expectation is that this branch will then be the target of one or more PRs
copying (cherry-picking) commits from main to the release branch, and then a
hotfix release will be tagged on the branch using the related Tag Release tool.

Usage:

	go run github.com/letsencrypt/boulder/tools/release/branch@main [-push] tagname

The provided tagname must be a pre-existing release tag which is reachable from
the "main" branch.

If the -push flag is not provided, it will simply print the details of the new
branch and then exit. If it is provided, it will initiate a push to the remote.

In all cases, it assumes that the upstream remote is named "origin".
*/
package main

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"
)

type cmdError struct {
	error
	output string
}

func (e cmdError) Unwrap() error {
	return e.error
}

func git(args ...string) (string, error) {
	cmd := exec.Command("git", args...)
	fmt.Println("Running:", cmd.String())
	out, err := cmd.CombinedOutput()
	if err != nil {
		return string(out), cmdError{
			error:  fmt.Errorf("running %q: %w", cmd.String(), err),
			output: string(out),
		}
	}
	return string(out), nil
}

func show(output string) {
	for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
		fmt.Println("  ", line)
	}
}

func main() {
	err := branch(os.Args[1:])
	if err != nil {
		var cmdErr cmdError
		if errors.As(err, &cmdErr) {
			show(cmdErr.output)
		}
		fmt.Println(err.Error())
		os.Exit(1)
	}
}

func branch(args []string) error {
	fs := flag.NewFlagSet("branch", flag.ContinueOnError)
	var push bool
	fs.BoolVar(&push, "push", false, "If set, push the resulting hotfix release branch to GitHub.")
	err := fs.Parse(args)
	if err != nil {
		return fmt.Errorf("invalid flags: %w", err)
	}

	if len(fs.Args()) != 1 {
		return fmt.Errorf("must supply exactly one argument, got %d: %#v", len(fs.Args()), fs.Args())
	}

	tag := fs.Arg(0)

	// Confirm the reasonableness of the given tag name by inspecting each of its
	// components.
	parts := strings.SplitN(tag, ".", 3)
	if len(parts) != 3 {
		return fmt.Errorf("failed to parse release tag %q as semver", tag)
	}

	major := parts[0]
	if major != "v0" {
		return fmt.Errorf("expected major portion of release tag to be 'v0', got %q", major)
	}

	minor := parts[1]
	t, err := time.Parse("20060102", minor)
	if err != nil {
		return fmt.Errorf("expected minor portion of release tag to be a date: %w", err)
	}
	if t.Year() < 2015 {
		return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String())
	}

	patch := parts[2]
	if patch != "0" {
		return fmt.Errorf("expected patch portion of release tag to be '0', got %q", patch)
	}

	// Fetch all of the latest refs from origin, so that we can get the most
	// complete view of this tag and its relationship to main.
	_, err = git("fetch", "origin")
	if err != nil {
		return err
	}

	_, err = git("merge-base", "--is-ancestor", tag, "origin/main")
	if err != nil {
		return fmt.Errorf("tag %q is not reachable from origin/main, may not have been created properly: %w", tag, err)
	}

	// Create the branch. We could skip this and instead push the tag directly
	// to the desired ref name on the remote, but that wouldn't give the operator
	// a chance to inspect it locally.
	branch := fmt.Sprintf("release-branch-%s.%s", major, minor)
	_, err = git("branch", branch, tag)
	if err != nil {
		return err
	}

	// Show the HEAD of the new branch, not including its diff.
	out, err := git("show", "-s", branch)
	if err != nil {
		return err
	}
	show(out)

	refspec := fmt.Sprintf("%s:%s", branch, branch)

	if push {
		_, err = git("push", "origin", refspec)
		if err != nil {
			return err
		}
	} else {
		fmt.Println()
		fmt.Println("Please inspect the branch above, then run:")
		fmt.Printf("    git push origin %s\n", refspec)
	}
	return nil
}
