// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 deps

import (
	"bytes"
	"encoding/json"
	"fmt"
	"go/build"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"

	"github.com/apache/skywalking-eyes/internal/logger"
	"github.com/apache/skywalking-eyes/pkg/license"

	"golang.org/x/tools/go/packages"
)

type GoModResolver struct {
	Resolver
}

func (resolver *GoModResolver) CanResolve(file string) bool {
	base := filepath.Base(file)
	logger.Log.Debugln("Base name:", base)
	return base == "go.mod"
}

// Resolve resolves licenses of all dependencies declared in the go.mod file.
func (resolver *GoModResolver) Resolve(goModFile string, config *ConfigDeps, report *Report) error {
	if err := os.Chdir(filepath.Dir(goModFile)); err != nil {
		return err
	}

	goModDownload := exec.Command("go", "mod", "download")
	logger.Log.Debugf("Run command: %v, please wait", goModDownload.String())
	goModDownload.Stdout = os.Stdout
	goModDownload.Stderr = os.Stderr
	if err := goModDownload.Run(); err != nil {
		return err
	}

	output, err := exec.Command("go", "mod", "download", "-json").Output()
	if err != nil {
		return err
	}

	modules := make([]*packages.Module, 0)
	decoder := json.NewDecoder(bytes.NewReader(output))
	for {
		var m packages.Module
		if err := decoder.Decode(&m); err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		modules = append(modules, &m)
	}

	logger.Log.Debugln("Module size:", len(modules))

	return resolver.ResolvePackages(modules, config, report)
}

// ResolvePackages resolves the licenses of the given packages.
func (resolver *GoModResolver) ResolvePackages(modules []*packages.Module, config *ConfigDeps, report *Report) error {
	for _, module := range modules {
		func() {
			if excluded, _ := config.IsExcluded(module.Path, module.Version); excluded {
				return
			}
			if l, ok := config.GetUserConfiguredLicense(module.Path, module.Version); ok {
				report.Resolve(&Result{
					Dependency:    module.Path,
					LicenseSpdxID: l,
					Version:       module.Version,
				})
				return
			}
			err := resolver.ResolvePackageLicense(config, module, report)
			if err != nil {
				logger.Log.Warnf("Failed to resolve the license of <%s@%s>: %v\n", module.Path, module.Version, err)
				report.Skip(&Result{
					Dependency:    module.Path,
					LicenseSpdxID: Unknown,
					Version:       module.Version,
				})
			}
		}()
	}
	return nil
}

var possibleLicenseFileName = regexp.MustCompile(`(?i)^LICENSE|LICENCE(\.txt)?|COPYING(\.txt)?$`)

func (resolver *GoModResolver) ResolvePackageLicense(config *ConfigDeps, module *packages.Module, report *Report) error {
	dir := module.Dir

	for {
		logger.Log.Debugf("Directory of %+v is %+v", module.Path, dir)
		files, err := os.ReadDir(dir)
		if err != nil {
			return err
		}
		for _, info := range files {
			if !possibleLicenseFileName.MatchString(info.Name()) {
				continue
			}
			licenseFilePath := filepath.Join(dir, info.Name())
			content, err := os.ReadFile(licenseFilePath)
			if err != nil {
				return err
			}
			identifier, err := license.Identify(string(content), config.Threshold)
			if err != nil {
				return err
			}

			report.Resolve(&Result{
				Dependency:      module.Path,
				LicenseFilePath: licenseFilePath,
				LicenseContent:  string(content),
				LicenseSpdxID:   identifier,
				Version:         module.Version,
			})
			return nil
		}
		if resolver.shouldStopAt(dir, module.Dir) {
			break
		}
		dir = filepath.Dir(dir)
	}
	return fmt.Errorf("cannot find license file")
}

func (resolver *GoModResolver) shouldStopAt(dir, moduleDir string) bool {
	return dir == moduleDir || dir == build.Default.GOPATH
}
