package main

import (
	"fmt"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"

	"sigs.k8s.io/yaml"
)

type Contribution struct {
	URLs        []string `json:"urls"`
	Tags        []string `json:"tags"`
	SponsoredBy string   `json:"sponsored-by"`
	Desc        string   `json:"desc"`

	SubmittedAt   time.Time `json:"submitted-at"`
	LastUpdatedAt time.Time `json:"last-updated-at"`
	Status        string    `json:"status"`

	StatusClass string `json:"-"`
}

func ReadContribs(filename string) ([]Contribution, error) {
	bs, err := os.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("contribs: %q: %w", filename, err)
	}
	var ret []Contribution
	if err := yaml.UnmarshalStrict(bs, &ret); err != nil {
		return nil, fmt.Errorf("contribs: %q: %w", filename, err)
	}
	for i := range ret {
		contrib := ret[i]
		if err := contrib.Fill(); err != nil {
			return nil, fmt.Errorf("contribs: %q: %w", filename, err)
		}
		ret[i] = contrib
	}
	return ret, nil
}

func (c *Contribution) Fill() error {
	var err error
	if c.SubmittedAt.IsZero() {
		c.SubmittedAt, err = c.fetchSubmittedAt()
		if err != nil {
			return err
		}
	}
	if c.LastUpdatedAt.IsZero() {
		c.LastUpdatedAt, err = c.fetchLastUpdatedAt()
		if err != nil {
			return err
		}
	}
	if c.Status == "" {
		c.Status, err = c.fetchStatus()
		if err != nil {
			return err
		}
	}
	c.StatusClass, err = classifyStatus(c.Status)
	if err != nil {
		return err
	}
	return nil
}

func classifyStatus(status string) (string, error) {
	switch {
	case strings.Contains(status, "released") || strings.Contains(status, "deployed"):
		return "released", nil
	case strings.Contains(status, "merged"):
		return "merged", nil
	case strings.Contains(status, "open"):
		return "open", nil
	case strings.Contains(status, "closed") || strings.Contains(status, "locked"):
		return "closed", nil
	default:
		return "", fmt.Errorf("unrecognized status string: %q", status)
	}
}

var (
	reGitHubPR      = regexp.MustCompile(`^https://github.com/([^/?#]+)/([^/?#]+)/pull/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
	reGitHubCommit  = regexp.MustCompile(`^https://github.com/([^/?#]+)/([^/?#]+)/commit/([0-9a-f]+)(?:\?[^#]*)?(?:#.*)?$`)
	reGitLabMR      = regexp.MustCompile(`^https://([^/]+)/([^?#]+)/-/merge_requests/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
	rePiperMailDate = regexp.MustCompile(`^\s*<I>([^<]+)</I>\s*$`)
)

const (
	statusOpen        = "open"
	statusMerged      = "merged, not yet in a release"
	statusReleasedFmt = "merged, released in %s"
)

func (c Contribution) fetchStatus() (string, error) {
	if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
		user := m[1]
		repo := m[2]
		prnum := m[3]

		urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum

		var obj struct {
			// State values are "open" and "closed".
			State          string `json:"state"`
			Merged         bool   `json:"merged"`
			MergeCommitSha string `json:"merge_commit_sha"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return "", err
		}
		ret := obj.State
		if obj.Merged {
			ret = statusMerged
			tag, err := getGitTagThatContainsAll("https://github.com/"+user+"/"+repo, obj.MergeCommitSha)
			if err != nil {
				return "", err
			}
			if tag != "" {
				ret = fmt.Sprintf(statusReleasedFmt, tag)
			}
		}

		return ret, nil
	}
	if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
		authority := m[1]
		projectID := m[2]
		mrnum := m[3]

		urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum

		var obj struct {
			// State values are "opened", "closed", "locked", and "merged".
			State           string `json:"state"`
			MergeCommitSha  string `json:"merge_commit_sha"`
			SquashCommitSha string `json:"squash_commit_sha"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return "", err
		}

		ret := obj.State
		if ret == "opened" {
			ret = statusOpen
		}

		if ret == "merged" {
			ret = statusMerged
			var mergeCommit string
			if obj.MergeCommitSha != "" {
				mergeCommit = obj.MergeCommitSha
			}
			if obj.SquashCommitSha != "" {
				mergeCommit = obj.SquashCommitSha
			}
			if mergeCommit != "" {
				tag, err := getGitTagThatContainsAll("https://"+authority+"/"+projectID+".git", mergeCommit)
				if err != nil {
					return "", err
				}
				if tag != "" {
					ret = fmt.Sprintf(statusReleasedFmt, tag)
				}
			}
		}

		return ret, nil
	}
	if len(c.URLs) > 1 {
		var gitURL string
		var gitCommits []string
		for _, u := range c.URLs[1:] {
			if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
				user := m[1]
				repo := m[2]
				hash := m[3]

				gitURL = "https://github.com/" + user + "/" + repo
				gitCommits = append(gitCommits, hash)
			}
		}
		if len(gitCommits) > 0 {
			ret := statusMerged
			tag, err := getGitTagThatContainsAll(gitURL, gitCommits...)
			if err != nil {
				return "", err
			}
			if tag != "" {
				ret = fmt.Sprintf(statusReleasedFmt, tag)
			}
			return ret, nil
		}
	}
	return "", fmt.Errorf("idk how to get status for %q", c.URLs[0])
}

func (c Contribution) fetchSubmittedAt() (time.Time, error) {
	if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
		user := m[1]
		repo := m[2]
		prnum := m[3]

		urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum

		var obj struct {
			CreatedAt time.Time `json:"created_at"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return time.Time{}, err
		}
		return obj.CreatedAt, nil
	}
	if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
		authority := m[1]
		projectID := m[2]
		mrnum := m[3]

		urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum

		var obj struct {
			CreatedAt time.Time `json:"created_at"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return time.Time{}, err
		}
		return obj.CreatedAt, nil
	}
	if strings.Contains(c.URLs[0], "/pipermail/") {
		htmlStr, err := httpGet(c.URLs[0])
		if err != nil {
			return time.Time{}, err
		}
		for _, line := range strings.Split(htmlStr, "\n") {
			if m := rePiperMailDate.FindStringSubmatch(line); m != nil {
				return time.Parse(time.UnixDate, m[1])
			}
		}
	}
	return time.Time{}, fmt.Errorf("idk how to get created timestamp for %q", c.URLs[0])
}

func (c Contribution) fetchLastUpdatedAt() (time.Time, error) {
	if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
		user := m[1]
		repo := m[2]
		prnum := m[3]

		urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum

		var obj struct {
			UpdatedAt time.Time `json:"updated_at"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return time.Time{}, err
		}
		return obj.UpdatedAt, nil
	}
	if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
		authority := m[1]
		projectID := m[2]
		mrnum := m[3]

		urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum

		var obj struct {
			UpdatedAt time.Time `json:"updated_at"`
		}
		if err := httpGetJSON(urlStr, &obj); err != nil {
			return time.Time{}, err
		}
		return obj.UpdatedAt, nil
	}

	var ret time.Time
	if len(c.URLs) > 1 {
		for _, u := range c.URLs[1:] {
			if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
				user := m[1]
				repo := m[2]
				hash := m[3]

				urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/commits/" + hash
				var obj struct {
					Commit struct {
						Author struct {
							Date time.Time `json:"date"`
						} `json:"author"`
						Committer struct {
							Date time.Time `json:"date"`
						} `json:"committer"`
					} `json:"commit"`
				}
				if err := httpGetJSON(urlStr, &obj); err != nil {
					return time.Time{}, err
				}
				if obj.Commit.Author.Date.After(ret) {
					ret = obj.Commit.Author.Date
				}
				if obj.Commit.Committer.Date.After(ret) {
					ret = obj.Commit.Committer.Date
				}
			}
		}
	}
	if !ret.IsZero() {
		return ret, nil
	}

	return time.Time{}, nil //fmt.Errorf("idk how to get updated timestamp for %q", c.URLs[0])
}