package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"runtime"
	"time"

	"golang.org/x/sys/cpu"

	"github.com/stashapp/stash/internal/build"
	"github.com/stashapp/stash/pkg/logger"
)

// we use the github REST V3 API as no login is required
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
const apiAcceptHeader string = "application/vnd.github.v3+json"
const developmentTag string = "latest_develop"
const defaultSHLength int = 8 // default length of SHA short hash returned by <git rev-parse --short HEAD>

var stashReleases = func() map[string]string {
	return map[string]string{
		"darwin/amd64":  "stash-macos",
		"darwin/arm64":  "stash-macos",
		"linux/amd64":   "stash-linux",
		"windows/amd64": "stash-win.exe",
		"linux/arm":     "stash-linux-arm32v6",
		"linux/arm64":   "stash-linux-arm64v8",
		"linux/armv7":   "stash-linux-arm32v7",
	}
}

type githubReleasesResponse struct {
	Url              string
	Assets_url       string
	Upload_url       string
	Html_url         string
	Id               int64
	Node_id          string
	Tag_name         string
	Target_commitish string
	Name             string
	Draft            bool
	Author           githubAuthor
	Prerelease       bool
	Created_at       string
	Published_at     string
	Assets           []githubAsset
	Tarball_url      string
	Zipball_url      string
	Body             string
}

type githubAuthor struct {
	Login               string
	Id                  int64
	Node_id             string
	Avatar_url          string
	Gravatar_id         string
	Url                 string
	Html_url            string
	Followers_url       string
	Following_url       string
	Gists_url           string
	Starred_url         string
	Subscriptions_url   string
	Organizations_url   string
	Repos_url           string
	Events_url          string
	Received_events_url string
	Type                string
	Site_admin          bool
}

type githubAsset struct {
	Url                  string
	Id                   int64
	Node_id              string
	Name                 string
	Label                string
	Uploader             githubAuthor
	Content_type         string
	State                string
	Size                 int64
	Download_count       int64
	Created_at           string
	Updated_at           string
	Browser_download_url string
}

type githubTagResponse struct {
	Name        string
	Zipball_url string
	Tarball_url string
	Commit      struct {
		Sha string
		Url string
	}
	Node_id string
}

type LatestRelease struct {
	Version   string
	Hash      string
	ShortHash string
	Date      string
	Url       string
}

func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
	transport := &http.Transport{Proxy: http.ProxyFromEnvironment}

	client := &http.Client{
		Timeout:   3 * time.Second,
		Transport: transport,
	}

	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

	req.Header.Add("Accept", apiAcceptHeader) // gh api recommendation , send header with api version
	logger.Debugf("Github API request: %s", url)
	response, err := client.Do(req)

	if err != nil {
		//lint:ignore ST1005 Github is a proper capitalized noun
		return fmt.Errorf("Github API request failed: %w", err)
	}

	if response.StatusCode != http.StatusOK {
		//lint:ignore ST1005 Github is a proper capitalized noun
		return fmt.Errorf("Github API request failed: %s", response.Status)
	}

	defer response.Body.Close()

	data, err := io.ReadAll(response.Body)
	if err != nil {
		//lint:ignore ST1005 Github is a proper capitalized noun
		return fmt.Errorf("Github API read response failed: %w", err)
	}

	err = json.Unmarshal(data, output)
	if err != nil {
		return fmt.Errorf("unmarshalling Github API response failed: %w", err)
	}

	return nil
}

// GetLatestRelease gets latest release information from github API
// If running a build from the "master" branch, then the latest full release
// is used, otherwise it uses the release that is tagged with "latest_develop"
// which is the latest pre-release build.
func GetLatestRelease(ctx context.Context) (*LatestRelease, error) {
	arch := runtime.GOARCH

	// https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
	// armv6 doesn't support any of these features
	isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4
	if arch == "arm" && isARMv7 {
		arch = "armv7"
	}

	platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
	wantedRelease := stashReleases()[platform]

	url := apiReleases
	if build.IsDevelop() {
		// get the release tagged with the development tag
		url += "/tags/" + developmentTag
	} else {
		// just get the latest full release
		url += "/latest"
	}

	var release githubReleasesResponse
	err := makeGithubRequest(ctx, url, &release)
	if err != nil {
		return nil, err
	}

	version := release.Name
	if release.Prerelease {
		// find version in prerelease name
		re := regexp.MustCompile(`v[\w-\.]+-\d+-g[0-9a-f]+`)
		if match := re.FindString(version); match != "" {
			version = match
		}
	}

	latestHash, err := getReleaseHash(ctx, release.Tag_name)
	if err != nil {
		return nil, err
	}

	var releaseDate string
	if publishedAt, err := time.Parse(time.RFC3339, release.Published_at); err == nil {
		releaseDate = publishedAt.Format("2006-01-02")
	}

	var releaseUrl string
	if wantedRelease != "" {
		for _, asset := range release.Assets {
			if asset.Name == wantedRelease {
				releaseUrl = asset.Browser_download_url
				break
			}
		}
	}

	_, githash, _ := build.Version()
	shLength := len(githash)
	if shLength == 0 {
		shLength = defaultSHLength
	}

	return &LatestRelease{
		Version:   version,
		Hash:      latestHash,
		ShortHash: latestHash[:shLength],
		Date:      releaseDate,
		Url:       releaseUrl,
	}, nil
}

func getReleaseHash(ctx context.Context, tagName string) (string, error) {
	// Start with a small page size if not searching for latest_develop
	perPage := 10
	if tagName == developmentTag {
		perPage = 100
	}

	// Limit to 5 pages, ie 500 tags - should be plenty
	for page := 1; page <= 5; {
		url := fmt.Sprintf("%s?per_page=%d&page=%d", apiTags, perPage, page)
		tags := []githubTagResponse{}
		err := makeGithubRequest(ctx, url, &tags)
		if err != nil {
			return "", err
		}

		for _, tag := range tags {
			if tag.Name == tagName {
				if len(tag.Commit.Sha) != 40 {
					return "", errors.New("invalid Github API response")
				}
				return tag.Commit.Sha, nil
			}
		}

		if len(tags) == 0 {
			break
		}

		// if not found in the first 10, search again on page 1 with the first 100
		if perPage == 10 {
			perPage = 100
		} else {
			page++
		}
	}

	return "", errors.New("invalid Github API response")
}

func printLatestVersion(ctx context.Context) {
	latestRelease, err := GetLatestRelease(ctx)
	if err != nil {
		logger.Errorf("Couldn't retrieve latest version: %v", err)
	} else {
		_, githash, _ := build.Version()
		switch {
		case githash == "":
			logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
		case githash == latestRelease.ShortHash:
			logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
		default:
			logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
		}
	}
}
