package shared

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"reflect"
	"strings"
	"time"

	"github.com/cli/cli/v2/api"
	"github.com/cli/cli/v2/internal/ghinstance"
	"github.com/cli/cli/v2/internal/ghrepo"
)

var ReleaseFields = []string{
	"url",
	"apiUrl",
	"uploadUrl",
	"tarballUrl",
	"zipballUrl",
	"id",
	"tagName",
	"name",
	"body",
	"isDraft",
	"isPrerelease",
	"createdAt",
	"publishedAt",
	"targetCommitish",
	"author",
	"assets",
}

type Release struct {
	ID           string     `json:"node_id"`
	TagName      string     `json:"tag_name"`
	Name         string     `json:"name"`
	Body         string     `json:"body"`
	IsDraft      bool       `json:"draft"`
	IsPrerelease bool       `json:"prerelease"`
	CreatedAt    time.Time  `json:"created_at"`
	PublishedAt  *time.Time `json:"published_at"`

	TargetCommitish string `json:"target_commitish"`

	APIURL     string `json:"url"`
	UploadURL  string `json:"upload_url"`
	TarballURL string `json:"tarball_url"`
	ZipballURL string `json:"zipball_url"`
	URL        string `json:"html_url"`
	Assets     []ReleaseAsset

	Author struct {
		ID    string `json:"node_id"`
		Login string `json:"login"`
	}
}

type ReleaseAsset struct {
	ID     string `json:"node_id"`
	Name   string
	Label  string
	Size   int64
	State  string
	APIURL string `json:"url"`

	CreatedAt          time.Time `json:"created_at"`
	UpdatedAt          time.Time `json:"updated_at"`
	DownloadCount      int       `json:"download_count"`
	ContentType        string    `json:"content_type"`
	BrowserDownloadURL string    `json:"browser_download_url"`
}

func (rel *Release) ExportData(fields []string) map[string]interface{} {
	v := reflect.ValueOf(rel).Elem()
	fieldByName := func(v reflect.Value, field string) reflect.Value {
		return v.FieldByNameFunc(func(s string) bool {
			return strings.EqualFold(field, s)
		})
	}
	data := map[string]interface{}{}

	for _, f := range fields {
		switch f {
		case "author":
			data[f] = map[string]interface{}{
				"id":    rel.Author.ID,
				"login": rel.Author.Login,
			}
		case "assets":
			assets := make([]interface{}, 0, len(rel.Assets))
			for _, a := range rel.Assets {
				assets = append(assets, map[string]interface{}{
					"url":           a.BrowserDownloadURL,
					"apiUrl":        a.APIURL,
					"id":            a.ID,
					"name":          a.Name,
					"label":         a.Label,
					"size":          a.Size,
					"state":         a.State,
					"createdAt":     a.CreatedAt,
					"updatedAt":     a.UpdatedAt,
					"downloadCount": a.DownloadCount,
					"contentType":   a.ContentType,
				})
			}
			data[f] = assets
		default:
			sf := fieldByName(v, f)
			data[f] = sf.Interface()
		}
	}

	return data
}

// FetchRelease finds a repository release by its tagName.
func FetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
	path := fmt.Sprintf("repos/%s/%s/releases/tags/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), tagName)
	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return FindDraftRelease(httpClient, baseRepo, tagName)
	}

	if resp.StatusCode > 299 {
		return nil, api.HandleHTTPError(resp)
	}

	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var release Release
	err = json.Unmarshal(b, &release)
	if err != nil {
		return nil, err
	}

	return &release, nil
}

// FetchLatestRelease finds the latest published release for a repository.
func FetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*Release, error) {
	path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode > 299 {
		return nil, api.HandleHTTPError(resp)
	}

	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var release Release
	err = json.Unmarshal(b, &release)
	if err != nil {
		return nil, err
	}

	return &release, nil
}

// FindDraftRelease returns the latest draft release that matches tagName.
func FindDraftRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
	path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path

	perPage := 100
	page := 1
	for {
		req, err := http.NewRequest("GET", fmt.Sprintf("%s?per_page=%d&page=%d", url, perPage, page), nil)
		if err != nil {
			return nil, err
		}

		resp, err := httpClient.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		if resp.StatusCode > 299 {
			return nil, api.HandleHTTPError(resp)
		}

		b, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}

		var releases []Release
		err = json.Unmarshal(b, &releases)
		if err != nil {
			return nil, err
		}

		for _, r := range releases {
			if r.IsDraft && r.TagName == tagName {
				return &r, nil
			}
		}
		//nolint:staticcheck
		break
	}

	return nil, errors.New("release not found")
}
