package forgejo

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
)

type RepositoryFile struct {
	Name string

	owner string
	repo  string
	ref   string

	localDir *string
}

func (r *RepositoryFile) Read() ([]byte, error) {
	var (
		fp  io.ReadCloser
		err error
	)
	if r.localDir != nil {
		fp, err = os.Open(filepath.Join(*r.localDir, r.Name))
	} else {
		fp, err = GetRepositoryFile(r.owner, r.repo, r.Name, r.ref)
	}
	if err != nil {
		return nil, err
	}
	defer fp.Close()

	return io.ReadAll(fp)
}

func newRepositoryDir(owner string, repo string, path string, ref string, localDir *string, recursive bool) ([]*RepositoryFile, error) {
	pp := "repos/" + owner + "/" + repo + "/contents"
	if path != "" {
		pp += "/" + strings.TrimRight(path, "/")
	}

	resp, err := Request("GET", pp, nil, nil)
	if err != nil {
		if eerr, ok := err.(*Error); ok && eerr.StatusCode == 404 {
			return []*RepositoryFile{}, nil
		}
		return nil, err
	}
	defer resp.Body.Close()

	v := []struct {
		Path string `json:"path"`
		Type string `json:"type"`
	}{}
	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
		return nil, err
	}

	rv := []*RepositoryFile{}
	for _, d := range v {
		if d.Type == "file" {
			rv = append(rv, &RepositoryFile{
				Name:     d.Path,
				owner:    owner,
				repo:     repo,
				ref:      ref,
				localDir: localDir,
			})
		} else if d.Type == "dir" && recursive {
			fl, err := newRepositoryDir(owner, repo, d.Path, ref, localDir, false) // only recurse 1 level
			if err != nil {
				return nil, err
			}
			rv = append(rv, fl...)
		}
	}
	return rv, nil
}

type RepositoryReleaseAsset struct {
	Name        string
	DownloadUrl string
}

type RepositoryLatestRelease struct {
	Name        string
	Tag         string
	Url         string
	Description string
	Assets      []RepositoryReleaseAsset
}

type RepositoryRollingRelease struct {
	Assets []RepositoryReleaseAsset
}

type Repository struct {
	Description    string
	HomepageUrl    string
	DefaultBranch  string
	Head           string
	LatestRelease  *RepositoryLatestRelease
	RollingRelease *RepositoryRollingRelease
	Releases       []string
	Readme         *RepositoryFile
	Docs           []*RepositoryFile
	Headers        []*RepositoryFile

	owner      string
	repo       string
	headersDir *string
	localDir   *string
}

func GetRepository(owner string, repo string, rollingtag string, headersDir *string, localDir *string) (*Repository, error) {
	r, err := Request("GET", "repos/"+owner+"/"+repo, nil, nil)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()

	v := struct {
		Description   string `json:"description"`
		Website       string `json:"website"`
		DefaultBranch string `json:"default_branch"`
	}{}
	if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
		return nil, err
	}

	rv := &Repository{
		Description:   v.Description,
		HomepageUrl:   v.Website,
		DefaultBranch: v.DefaultBranch,
		Readme: &RepositoryFile{
			Name: "README.md",

			owner:    owner,
			repo:     repo,
			ref:      v.DefaultBranch,
			localDir: localDir,
		},
		owner:      owner,
		repo:       repo,
		headersDir: headersDir,
		localDir:   localDir,
	}

	head, err := Request("GET", "repos/"+owner+"/"+repo+"/commits?stat=false&verification=false&files=false&limit=1", nil, nil)
	if err != nil {
		return nil, err
	}
	defer head.Body.Close()

	vhead := []struct {
		Sha string `json:"sha"`
	}{}
	if err := json.NewDecoder(head.Body).Decode(&vhead); err != nil {
		return nil, err
	}
	if l := len(vhead); l != 1 || vhead[0].Sha == "" {
		return nil, fmt.Errorf("forgejo: %s:%s: failed to fetch head: %d", owner, repo, l)
	}
	rv.Head = vhead[0].Sha

	docs, err := newRepositoryDir(owner, repo, "docs", v.DefaultBranch, localDir, false)
	if err != nil {
		return nil, err
	}
	rv.Docs = docs

	if headersDir != nil {
		headers, err := newRepositoryDir(owner, repo, *headersDir, v.DefaultBranch, localDir, true)
		if err != nil {
			return nil, err
		}
		rv.Headers = headers
	}

	latest, err := Request("GET", "repos/"+owner+"/"+repo+"/releases/latest", nil, nil)
	if err != nil {
		eerr, ok := err.(*Error)
		if !ok || eerr.StatusCode != 404 {
			return nil, err
		}
	}

	if latest != nil {
		defer latest.Body.Close()

		vlatest := struct {
			Name    string `json:"name"`
			TagName string `json:"tag_name"`
			HtmlUrl string `json:"html_url"`
			Body    string `json:"body"`
			Assets  []struct {
				Name               string `json:"name"`
				BrowserDownloadUrl string `json:"browser_download_url"`
			} `json:"assets"`
		}{}

		if err := json.NewDecoder(latest.Body).Decode(&vlatest); err != nil {
			return nil, err
		}

		rv.LatestRelease = &RepositoryLatestRelease{
			Name:        vlatest.Name,
			Tag:         vlatest.TagName,
			Url:         vlatest.HtmlUrl,
			Description: vlatest.Body,
		}
		for _, a := range vlatest.Assets {
			rv.LatestRelease.Assets = append(rv.LatestRelease.Assets, RepositoryReleaseAsset{
				Name:        a.Name,
				DownloadUrl: a.BrowserDownloadUrl,
			})
		}
	}

	rolling, err := Request("GET", "repos/"+owner+"/"+repo+"/releases/tags/rolling", nil, nil)
	if err != nil {
		eerr, ok := err.(*Error)
		if !ok || eerr.StatusCode != 404 {
			return nil, err
		}
	}

	if rolling != nil {
		defer rolling.Body.Close()

		vrolling := struct {
			Assets []struct {
				Name               string `json:"name"`
				BrowserDownloadUrl string `json:"browser_download_url"`
			} `json:"assets"`
		}{}

		if err := json.NewDecoder(rolling.Body).Decode(&vrolling); err != nil {
			return nil, err
		}

		rv.RollingRelease = &RepositoryRollingRelease{}
		for _, a := range vrolling.Assets {
			rv.RollingRelease.Assets = append(rv.RollingRelease.Assets, RepositoryReleaseAsset{
				Name:        a.Name,
				DownloadUrl: a.BrowserDownloadUrl,
			})
		}
	}

	releases, err := Request("GET", "repos/"+owner+"/"+repo+"/releases?draft=false&pre-release=false&limit=100", nil, nil)
	if err != nil {
		eerr, ok := err.(*Error)
		if !ok || eerr.StatusCode != 404 {
			return nil, err
		}
	}

	if releases != nil {
		defer releases.Body.Close()

		if s := releases.Header.Get("x-total-count"); s != "" {
			if c, err := strconv.ParseInt(s, 10, 64); err == nil && c > 100 {
				return nil, fmt.Errorf("forgejo: %s/%s has more than 100 releases: %d", owner, repo, c)
			}
		}

		vreleases := []struct {
			Tag string `json:"tag_name"`
		}{}
		if err := json.NewDecoder(releases.Body).Decode(&vreleases); err != nil {
			return nil, err
		}

		for _, rel := range vreleases {
			rv.Releases = append(rv.Releases, rel.Tag)
		}
	}

	return rv, nil
}

func (r *Repository) ReloadLocalDir() error {
	if r.localDir == nil {
		return nil
	}

	r.Readme = nil
	if _, err := os.Stat(filepath.Join(*r.localDir, "README.md")); err == nil {
		r.Readme = &RepositoryFile{
			Name: "README.md",

			owner:    r.owner,
			repo:     r.repo,
			ref:      "",
			localDir: r.localDir,
		}
	}

	r.Docs = nil
	l, err := os.ReadDir(filepath.Join(*r.localDir, "docs"))
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	for _, e := range l {
		if e.Type().IsRegular() {
			r.Docs = append(r.Docs,
				&RepositoryFile{
					Name:     path.Join("docs", e.Name()),
					owner:    r.owner,
					repo:     r.repo,
					ref:      "",
					localDir: r.localDir,
				},
			)
		}
	}

	r.Headers = nil
	prefix := ""
	if r.headersDir != nil {
		prefix = *r.headersDir
	}
	l, err = os.ReadDir(filepath.Join(*r.localDir, prefix))
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	for _, e := range l {
		if e.IsDir() {
			l2, err := os.ReadDir(filepath.Join(*r.localDir, prefix, e.Name()))
			if err != nil {
				return err
			}

			for _, e2 := range l2 {
				if e2.Type().IsRegular() {
					r.Headers = append(r.Headers,
						&RepositoryFile{
							Name: path.Join(prefix, e.Name(), e2.Name()),

							owner:    r.owner,
							repo:     r.owner,
							ref:      "",
							localDir: r.localDir,
						},
					)
				}
			}
		}
		if e.Type().IsRegular() {
			r.Headers = append(r.Headers,
				&RepositoryFile{
					Name: path.Join(prefix, e.Name()),

					owner:    r.owner,
					repo:     r.repo,
					ref:      "",
					localDir: r.localDir,
				},
			)
		}
	}
	return nil
}

func GetRepositoryFile(owner string, repo string, ppath string, ref string) (io.ReadCloser, error) {
	p := "repos/" + owner + "/" + repo + "/raw/" + ppath
	if ref != "" {
		p += "?ref" + ref
	}

	resp, err := Request("GET", p, map[string]string{"accept": "application/octet-stream"}, nil)
	if err != nil {
		return nil, err
	}
	return resp.Body, nil
}
