// Copyright 2021 storyicon@foxmail.com
//
// 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 pluginmanager

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/mholt/archiver"
	"github.com/pkg/errors"

	"github.com/storyicon/powerproto/pkg/util"
)

// ProtocRelease defines the release of protoc
type ProtocRelease struct {
	workspace string
}

// GetIncludePath is used to get the include path
func (p *ProtocRelease) GetIncludePath() string {
	return filepath.Join(p.workspace, "include")
}

// GetProtocPath is used to get the protoc path
func (p *ProtocRelease) GetProtocPath() string {
	return filepath.Join(p.workspace, "bin", util.GetBinaryFileName("protoc"))
}

// Clear is used to clear the workspace
func (p *ProtocRelease) Clear() error {
	return os.RemoveAll(p.workspace)
}

// GetProtocRelease is used to download protoc release
func GetProtocRelease(ctx context.Context, version string) (*ProtocRelease, error) {
	workspace, err := os.MkdirTemp("", "")
	if err != nil {
		return nil, err
	}
	suffix, err := inferProtocReleaseSuffix()
	if err != nil {
		return nil, err
	}
	filename := fmt.Sprintf("protoc-%s-%s.zip", version, suffix)
	url := fmt.Sprintf("https://github.com/protocolbuffers/protobuf/"+
		"releases/download/v%s/%s", version, filename)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, &ErrHTTPDownload{
			Url: url,
			Err: err,
		}
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, &ErrHTTPDownload{
			Url: url,
			Err: err,
		}
	}
	zipFilePath := filepath.Join(workspace, filename)
	if err := downloadFile(resp, zipFilePath); err != nil {
		return nil, &ErrHTTPDownload{
			Url:  url,
			Err:  err,
			Code: resp.StatusCode,
		}
	}
	zip := archiver.NewZip()
	if err := zip.Unarchive(zipFilePath, workspace); err != nil {
		return nil, err
	}
	return &ProtocRelease{
		workspace: workspace,
	}, nil
}

// IsProtocInstalled is used to check whether the protoc version is installed
func IsProtocInstalled(ctx context.Context, storageDir string, version string) (bool, string, error) {
	local := PathForProtoc(storageDir, version)
	exists, err := util.IsFileExists(local)
	if err != nil {
		return false, "", err
	}
	return exists, local, nil
}

func inferProtocReleaseSuffix() (string, error) {
	goos := strings.ToLower(runtime.GOOS)
	arch := strings.ToLower(runtime.GOARCH)
	switch goos {
	case "linux":
		switch arch {
		case "arm64":
			return "linux-aarch_64", nil
		case "ppc64le":
			return "linux-ppcle_64", nil
		case "s390x":
			return "linux-s390_64", nil
		case "386":
			return "linux-x86_32", nil
		case "amd64":
			return "linux-x86_64", nil
		}
	case "darwin":
		return "osx-x86_64", nil
	case "windows":
		switch arch {
		case "386":
			return "win32", nil
		case "amd64":
			return "win64", nil
		}
	}
	return "", errors.New("protoc did not release on this platform")
}

func downloadFile(resp *http.Response, destination string) error {
	if err := os.MkdirAll(filepath.Dir(destination), fs.ModePerm); err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return errors.Errorf("unexpected code %d for url: %s", resp.StatusCode, resp.Request.URL.String())
	}
	file, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fs.ModePerm)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.Copy(file, resp.Body)
	if err != nil {
		return err
	}
	return nil
}
