// Copyright 2024 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package versions

import (
	"context"
	"errors"
	"fmt"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"github.com/cloudwego/kitex"
	"github.com/cloudwego/kitex/tool/internal_pkg/log"
)

const (
	CompatibilityCheckExitCode = 3
)

var dm *dependencyManager

func init() {
	dm = &dependencyManager{}
}

// RegisterMinDepVersion registers Minimal Dependency Version to be checked
func RegisterMinDepVersion(depVer *MinDepVersion) error {
	return dm.Register(depVer)
}

// CheckDependency is responsible for checking the compatibility of dependency
// and return the checking results.
// After calling CheckDependencies(), you should check if the result is Nil:
//
//	res := CheckDependency()
//	if res != nil {
//	}
//
// If res is not nil, pls check the related error with `Err()`:
//
//		if err := res.Err(); err != nil {
//		    switch {
//	        case errors.Is(err, ErrGoCmdNotFound):
//	        case errors.Is(err, ErrGoModNotFound):
//		    case errors.Is(err, ErrDependencyNotFound):
//		    case errors.Is(err, ErrDependencyVersionNotSemantic):
//		    case errors.Is(err, ErrDependencyReplacedWithLocalRepo):
//		    case errors.Is(err, ErrDependencyVersionNotCompatible):
//		    }
//		}
//
// Then you can get the version in go.mod with `GoModVersion()` and retrieve the MinDepVersion
// information with `MinDepVersion()`.
func CheckDependency() *CheckResult {
	return dm.CheckDependency()
}

// DefaultCheckDependencyAndProcess provided default processing procedure to parse
// CheckResult and prompt users
func DefaultCheckDependencyAndProcess() error {
	cr := CheckDependency()
	if cr == nil {
		return nil
	}
	res, shouldExit := defaultParseCheckResult(cr)
	if res != "" {
		log.Info(res)
	}
	if shouldExit {
		return errors.New("kitex cmd tool dependency compatibility check failed")
	}
	return nil
}

func defaultParseCheckResult(cr *CheckResult) (prompt string, shouldExit bool) {
	if cr == nil {
		return
	}
	if err := cr.Err(); err != nil && (errors.Is(err, ErrDependencyVersionNotCompatible)) {
		prompt = defaultPromptWithCheckResult(cr)
		shouldExit = true
	}

	return
}

var defaultPrompt = `# Kitex Cmd Tool %s is not compatible with %s %s in your go.mod
# You can upgrade %s to latest version
go get %s@latest
# Or upgrade %s to %s version
go get %s@%s
# Or downgrade Kitex Cmd Tool to %s version
go install github.com/cloudwego/kitex/tool/cmd/kitex@%s`

func defaultPromptWithCheckResult(cr *CheckResult) string {
	depVer := cr.MinDepVersion()
	goModVer := cr.GoModVersion()
	kitexName := "Kitex"
	return fmt.Sprintf(defaultPrompt, kitex.Version, depVer.RefPath, goModVer,
		kitexName,
		depVer.RefPath,
		kitexName, kitex.Version,
		depVer.RefPath, kitex.Version,
		goModVer,
		goModVer,
	)
}

type MinDepVersion struct {
	// RefPath is the reference path to the dependency
	// e.g. github.com/cloudwego/kitex
	RefPath string
	// Version is the minimal required version
	Version string

	ver      *version
	goModVer *version
}

func (m *MinDepVersion) init() error {
	if m == nil {
		return errors.New("nil MinDepVersion")
	}
	if m.RefPath == "" {
		return errors.New("empty RefPath")
	}
	if m.Version == "" {
		return errors.New("empty Version")
	}
	ver, err := newVersion(m.Version)
	if err != nil {
		return err
	}
	m.ver = ver

	return nil
}

func (m *MinDepVersion) parseGoModVersion() error {
	res, err := runGoListCmd(m.RefPath)
	if err != nil {
		return err
	}

	verStr := parseGoListVersion(res)
	// replace with local repository
	if verStr == "" {
		return ErrDependencyReplacedWithLocalRepo
	}

	ver, err := newVersion(verStr)
	if err != nil {
		return ErrDependencyVersionNotSemantic
	}
	m.goModVer = ver

	return nil
}

func (m *MinDepVersion) getGoModVersion() string {
	return m.goModVer.String()
}

func (m *MinDepVersion) isCompatible() bool {
	return m.goModVer.greatOrEqual(m.ver)
}

var (
	ErrGoCmdNotFound                   = errors.New("go cmd not found")
	ErrGoModNotFound                   = errors.New("go.mod file not found in current directory or any parent directory")
	ErrDependencyNotFound              = errors.New("dependency not found")
	ErrDependencyVersionNotCompatible  = errors.New("dependency not compatible")
	ErrDependencyVersionNotSemantic    = errors.New("dependency version is not semantic version")
	ErrDependencyReplacedWithLocalRepo = errors.New("dependency replaced with local repo")
)

type CheckResult struct {
	ver      *MinDepVersion
	goModVer string
	err      error
}

func (cr *CheckResult) MinDepVersion() *MinDepVersion {
	return cr.ver
}

// Err returns the error in checking process
func (cr *CheckResult) Err() error {
	return cr.err
}

// GoModVersion returns the version of the dependency in the go.mod
func (cr *CheckResult) GoModVersion() string {
	return cr.goModVer
}

type dependencyManager struct {
	depVer *MinDepVersion
}

func (dm *dependencyManager) Register(depVer *MinDepVersion) error {
	if err := depVer.init(); err != nil {
		return err
	}
	dm.depVer = depVer

	return nil
}

func (dm *dependencyManager) CheckDependency() *CheckResult {
	if dm.depVer == nil {
		return nil
	}
	err := dm.depVer.parseGoModVersion()
	return generateCheckResult(dm.depVer, err)
}

func runGoListCmd(refPath string) (string, error) {
	res, err := runCommand("go list -m " + refPath)
	if err != nil {
		if errors.Is(err, exec.ErrNotFound) {
			return "", ErrGoCmdNotFound
		}
		if strings.Contains(res, "go.mod file not found") {
			return "", ErrGoModNotFound
		}
		return "", ErrDependencyNotFound
	}
	return res, nil
}

func runCommand(input string) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	arr := strings.Split(input, " ")
	cmd := exec.CommandContext(ctx, arr[0], arr[1:]...)
	output, err := cmd.CombinedOutput()
	// return output even if err is not nil
	// it can help the invoker to determine the specific scenario
	return strings.TrimSpace(string(output)), err
}

var goListVersionRegexp = regexp.MustCompile(`^(\S+)\s+(\S+)(\s+=>\s+)?(\S+)?(\s+)?(\S+)?$`)

// parseGoToolVersion parses the version string returned by `go list -m {dependency}`
// and returns the exact version of the dependency.
// e.g.
//
//	verStr: "github.com/cloudwego/kitex v0.9.0"
//	result: "v0.9.0"
//	verStr: github.com/cloudwego/kitex v0.9.0 => github.com/cloudwego/kitex v0.9.1
//	result: "v0.9.1"
//	verStr: "github.com/cloudwego/kitex v0.9.0 => ./kitex"
//	result: ""
func parseGoListVersion(str string) string {
	// e.g. github.com/cloudwego/kitex v0.9.0 => github.com/cloudwego/kitex v0.9.1
	// parts[0]: "github.com/cloudwego/kitex v0.9.0 => github.com/cloudwego/kitex v0.9.1"
	// parts[1]: "github.com/cloudwego/kitex"
	// parts[2]: "v0.9.0"
	// parts[3]: " => "
	// parts[4]: "github.com/cloudwego/kitex"
	// parts[5]: " "
	// parts[6]: "v0.9.1"
	parts := goListVersionRegexp.FindStringSubmatch(str)
	if parts == nil {
		return ""
	}
	// verify whether verStr is with replacement format
	if parts[3] == "" {
		return parts[2]
	}
	// verify whether replacing with local repository
	// e.g. github.com/cloudwego/kitex v0.9.0 => ./kitex
	// parts[6]: ""
	if parts[6] != "" {
		return parts[6]
	}

	return ""
}

func generateCheckResult(depVer *MinDepVersion, err error) *CheckResult {
	cr := &CheckResult{
		ver: depVer,
	}
	if err != nil {
		cr.err = err
		return cr
	}
	cr.goModVer = depVer.getGoModVersion()

	if !depVer.isCompatible() {
		cr.err = ErrDependencyVersionNotCompatible
	}

	return cr
}
