/*
Copyright 2021 The KodeRover 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 archive

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"

	"github.com/koderover/zadig/pkg/microservice/reaper/core/service/meta"
	"github.com/koderover/zadig/pkg/microservice/reaper/internal/s3"
	"github.com/koderover/zadig/pkg/setting"
	"github.com/koderover/zadig/pkg/tool/log"
	s3tool "github.com/koderover/zadig/pkg/tool/s3"
)

type WorkspaceAchiever struct {
	paths        []string
	gitFolders   []string
	wd           string
	files        map[string]os.FileInfo
	StorageURI   string
	PipelineName string
	ServiceName  string
	Envs         []string
	aesKey       string
}

func NewWorkspaceAchiever(storageURI, pipelineName, serviceName, wd, aesKey string, caches, gitFolders, envs []string) *WorkspaceAchiever {
	return &WorkspaceAchiever{
		paths:        caches,
		wd:           wd,
		aesKey:       aesKey,
		gitFolders:   gitFolders,
		StorageURI:   storageURI,
		PipelineName: pipelineName,
		ServiceName:  serviceName,
		Envs:         envs,
	}
}

type FileInfo struct {
	path string
}

type ByPathName []*FileInfo

func (p ByPathName) Len() int           { return len(p) }
func (p ByPathName) Less(i, j int) bool { return p[i].path < p[j].path }
func (p ByPathName) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func (c *WorkspaceAchiever) sortedFiles() []string {
	files := make([]string, len(c.files))

	i := 0
	for file := range c.files {
		files[i] = file
		i++
	}

	sort.Strings(files)
	return files
}

func (c *WorkspaceAchiever) add(path string) (err error) {
	// Always use slashes
	path = filepath.ToSlash(path)

	// Check if file exist
	info, err := os.Lstat(path)
	if err == nil {
		c.files[path] = info
	}
	return
}

func (c *WorkspaceAchiever) process(match string) bool {
	var absolute, relative string
	var err error

	absolute, err = filepath.Abs(match)
	if err == nil {
		// Let's try to find a real relative path to an absolute from working directory
		relative, err = filepath.Rel(c.wd, absolute)
	}
	if err == nil {
		// Process path only if it lives in our build directory
		if !strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
			err = c.add(relative)
		} else {
			err = errors.New("not supported: outside build directory")
		}
	}
	if err == nil {
		return true
	} else if os.IsNotExist(err) {
		// We hide the error that file doesn't exist
		return false
	}

	log.Warningf("%s: %v", match, err)
	return false
}

func (c *WorkspaceAchiever) processPaths(paths []string, verbose bool) error {
	newPath := make([]string, 0, len(paths))
	for _, path := range paths {
		path, err := c.renderPathVariable(path)
		if err != nil {
			log.Errorf("The variable of the custom cache directory is invalid, failed to render variables, error :%s", err)
			return err
		}
		newPath = append(newPath, path)
		matches, err := filepath.Glob(filepath.Join(c.wd, path))
		if err != nil {
			log.Warningf("%s: %v", path, err)
			continue
		}

		found := 0

		for _, match := range matches {
			err := filepath.Walk(match, func(path string, info os.FileInfo, err error) error {
				if c.process(path) {
					found++
				}
				return nil
			})
			if err != nil {
				log.Warningf("Walking", match, err)
			}
		}

		if verbose {
			if found == 0 {
				log.Warningf("%s: no matching files", path)
			} else {
				log.Infof("%s: found %d matching files", path, found)
			}
		}
	}
	c.paths = newPath
	return nil
}

func (c *WorkspaceAchiever) enumerate() error {
	c.files = make(map[string]os.FileInfo)

	if err := c.processPaths(c.paths, true); err != nil {
		return err
	}

	for _, folder := range c.gitFolders {
		c.processPaths([]string{filepath.Join(folder, ".git")}, false)
	}

	return nil
}

func (c *WorkspaceAchiever) Achieve(target string) ([]string, error) {
	if err := c.enumerate(); err != nil {
		return nil, err
	}

	f, err := ioutil.TempFile("", "*cached_files.txt")
	if err != nil {
		return nil, err
	}

	defer func() {
		_ = f.Close()
		_ = os.Remove(f.Name())
	}()

	for _, path := range c.sortedFiles() {
		_, err = f.WriteString(path + "\n")
		if err != nil {
			return nil, fmt.Errorf("failed to create cache file list: %s", err)
		}
	}

	if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
		return nil, err
	}

	temp, err := ioutil.TempFile("", "*reaper.tar.gz")
	if err != nil {
		log.Errorf("failed to create temporary file: %v", err)
		return nil, err
	}

	_ = temp.Close()

	log.Info("achieving caches ...")
	cmd := exec.Command("tar", "czf", temp.Name(), "--no-recursion", "-C", c.wd, "-T", f.Name())
	cmd.Dir = c.wd
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

	if err := cmd.Run(); err != nil {
		log.Errorf("failed to compress %v", err)
		return nil, err
	}

	//if err := helper.Move(temp.Name(), target); err != nil {
	//	log.Errorf("failed to move cache to shared fs %v", err)
	//	return err
	//}

	if store, err := s3.NewS3StorageFromEncryptedURI(c.StorageURI, c.aesKey); err == nil {
		forcedPathStyle := true
		if store.Provider == setting.ProviderSourceAli {
			forcedPathStyle = false
		}
		s3client, err := s3tool.NewClient(store.Endpoint, store.Ak, store.Sk, store.Region, store.Insecure, forcedPathStyle)
		if err != nil {
			log.Errorf("Archive s3 create s3 client error: %+v", err)
			return nil, err
		}
		objectKey := store.GetObjectPath(fmt.Sprintf("%s/%s/%s/%s", c.PipelineName, c.ServiceName, "cache", meta.FileName))
		if err = s3client.Upload(store.Bucket, temp.Name(), objectKey); err != nil {
			log.Errorf("Archive s3 upload err:%v", err)
			return nil, err
		}
	}

	return c.paths, nil
}

// The path contains shell variables, use linux's own ability to render
func (c *WorkspaceAchiever) renderPathVariable(path string) (string, error) {
	envs := c.Envs
	cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("%s %s", "echo", path))
	cmd.Stderr = os.Stderr
	cmd.Env = envs
	data, err := cmd.Output()

	return strings.TrimSuffix(string(data), "\n"), err
}
