package main

import (
	"flag"
	"fmt"
	"log"
	"os/exec"
	"strings"

	"golang.org/x/xerrors"

	"golang.org/x/mod/semver"
)

// main will print out the number of migrations added between each release.
// All upgrades are categorized as either major, minor, or patch based on semver.
//
// This isn't an exact science and is opinionated. Upgrade paths are not
// always strictly linear from release to release. Users can skip patches for
// example.
func main() {
	var includePatches bool
	var includeMinors bool
	var includeMajors bool
	var afterV2 bool
	var listMigs bool
	var migrationDirectory string
	var versionList string

	// If you only run with --patches, the upgrades that are minors are excluded.
	// Example being 1.0.0 -> 1.1.0 is a minor upgrade, so it's not included.
	flag.BoolVar(&includePatches, "patches", false, "Include patches releases")
	flag.BoolVar(&includeMinors, "minors", false, "Include minor releases")
	flag.BoolVar(&includeMajors, "majors", false, "Include major releases")
	flag.StringVar(&versionList, "versions", "", "Comma separated list of versions to use. This skips uses git tag to find tags.")
	flag.BoolVar(&afterV2, "after-v2", false, "Only include releases after v2.0.0")
	flag.BoolVar(&listMigs, "list", false, "List migrations")
	flag.StringVar(&migrationDirectory, "dir", "coderd/database/migrations", "Migration directory")
	flag.Parse()

	if !includePatches && !includeMinors && !includeMajors && versionList == "" {
		usage()
		return
	}

	var vList []string
	if versionList != "" {
		// Include all for printing purposes.
		includeMajors = true
		includeMinors = true
		includePatches = true
		vList = strings.Split(versionList, ",")
	}

	err := run(Options{
		VersionList:        vList,
		IncludePatches:     includePatches,
		IncludeMinors:      includeMinors,
		IncludeMajors:      includeMajors,
		AfterV2:            afterV2,
		ListMigrations:     listMigs,
		MigrationDirectory: migrationDirectory,
	})
	if err != nil {
		log.Fatal(err)
	}
}

func usage() {
	_, _ = fmt.Println("Usage: releasemigrations [--patches] [--minors] [--majors] [--list]")
	_, _ = fmt.Println("Choose at lease one of --patches, --minors, or --majors. You can choose all!")
	_, _ = fmt.Println("Must be run from the coder repo at the root.")
}

type Options struct {
	VersionList        []string
	IncludePatches     bool
	IncludeMinors      bool
	IncludeMajors      bool
	AfterV2            bool
	ListMigrations     bool
	MigrationDirectory string
}

func (o Options) Filter(tags []string) []string {
	if o.AfterV2 {
		for i, tag := range tags {
			if tag == "v2.0.0" {
				tags = tags[i:]
				break
			}
		}
	}

	if o.IncludeMajors && o.IncludeMinors && o.IncludePatches {
		return tags
	}

	filtered := make([]string, 0, len(tags))
	current := tags[0]
	filtered = append(filtered, current)
	for i := 1; i < len(tags); i++ {
		a := current
		current = tags[i]

		vDiffType := versionDiff(a, tags[i])
		if !o.IncludeMajors && vDiffType == "major" {
			continue
		}
		if !o.IncludeMinors && vDiffType == "minor" {
			// This isn't perfect, but we need to include
			// the first minor release for the first patch to work.
			// Eg: 1.0.0 -> 1.1.0 -> 1.1.1
			//	If we didn't include 1.1.0, then the 1.1.1 patch would
			// 	apply to 1.0.0
			if !o.IncludePatches {
				continue
			}
		}
		if !o.IncludePatches && vDiffType == "patch" {
			continue
		}
		filtered = append(filtered, tags[i])
	}

	return filtered
}

func run(opts Options) error {
	var tags []string
	if len(opts.VersionList) > 0 {
		tags = opts.VersionList
	} else {
		var err error
		tags, err = gitTags()
		if err != nil {
			return xerrors.Errorf("gitTags: %w", err)
		}
		tags = opts.Filter(tags)
	}

	patches := make([]string, 0)
	minors := make([]string, 0)
	majors := make([]string, 0)
	patchesHasMig := 0
	minorsHasMig := 0
	majorsHasMig := 0

	for i := 0; i < len(tags)-1; i++ {
		a := tags[i]
		b := tags[i+1]

		migrations, err := hasMigrationDiff(opts.MigrationDirectory, a, b)
		if err != nil {
			return xerrors.Errorf("hasMigrationDiff %q->%q: %w", a, b, err)
		}

		vDiff := fmt.Sprintf("%s->%s", a, b)
		vDiffType := versionDiff(a, b)
		skipPrint := true
		switch vDiffType {
		case "major":
			majors = append(majors, vDiff)
			if len(migrations) > 0 {
				majorsHasMig++
			}
			skipPrint = !opts.IncludeMajors
		case "minor":
			minors = append(minors, vDiff)
			if len(migrations) > 0 {
				minorsHasMig++
			}
			skipPrint = !opts.IncludeMinors
		case "patch":
			patches = append(patches, vDiff)
			if len(migrations) > 0 {
				patchesHasMig++
			}
			skipPrint = !opts.IncludePatches
		}

		if skipPrint {
			continue
		}

		if migrations != nil {
			log.Printf("[%s] %d migrations added between %s and %s\n", vDiffType, len(migrations), a, b)
			if opts.ListMigrations {
				for _, migration := range migrations {
					log.Printf("\t%s", migration)
				}
			}
		} else {
			log.Printf("[%s] No migrations added between %s and %s\n", vDiffType, a, b)
		}
	}

	log.Printf("Patches: %d (%d with migrations)\n", len(patches), patchesHasMig)
	log.Printf("Minors: %d (%d with migrations)\n", len(minors), minorsHasMig)
	log.Printf("Majors: %d (%d with migrations)\n", len(majors), majorsHasMig)

	return nil
}

func versionDiff(a, b string) string {
	ac, bc := semver.Canonical(a), semver.Canonical(b)
	if semver.Major(ac) != semver.Major(bc) {
		return "major"
	}
	if semver.MajorMinor(ac) != semver.MajorMinor(bc) {
		return "minor"
	}
	return "patch"
}

func hasMigrationDiff(dir string, a, b string) ([]string, error) {
	cmd := exec.Command("git", "diff",
		// Only added files
		"--diff-filter=A",
		"--name-only",
		a, b, dir)
	output, err := cmd.Output()
	if err != nil {
		return nil, xerrors.Errorf("%s\n%s", strings.Join(cmd.Args, " "), err)
	}
	if len(output) == 0 {
		return nil, nil
	}

	migrations := strings.Split(strings.TrimSpace(string(output)), "\n")
	filtered := make([]string, 0, len(migrations))
	for _, migration := range migrations {
		migration := migration
		if strings.Contains(migration, "fixtures") {
			continue
		}
		// Only show the ups
		if strings.HasSuffix(migration, ".down.sql") {
			continue
		}
		filtered = append(filtered, migration)
	}
	return filtered, nil
}

func gitTags() ([]string, error) {
	cmd := exec.Command("git", "tag")
	output, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	tags := strings.Split(string(output), "\n")

	// Sort by semver
	semver.Sort(tags)

	filtered := make([]string, 0, len(tags))
	for _, tag := range tags {
		if tag != "" && semver.IsValid(tag) {
			filtered = append(filtered, tag)
		}
	}

	return filtered, nil
}
