// check-quality validates PR body links and repository quality against
// CONTRIBUTING.md minimum standards. It outputs a markdown report and
// labels via GITHUB_OUTPUT for the PR Quality Checks workflow.
//
// Checks performed:
//   - Repo accessible, not archived, has go.mod and SemVer release
//   - Open source license present
//   - Repository age >= 5 months
//   - CI/CD (GitHub Actions) configured
//   - README exists
//   - Go Report Card grade A- or better
//   - pkg.go.dev page reachable
//   - Coverage link reachable
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"
)

var (
	reForgeLink  = regexp.MustCompile(`(?i)forge\s+link[^:]*:\s*(https?://(?:github\.com|gitlab\.com|bitbucket\.org)/\S+)`)
	rePkgGoDev   = regexp.MustCompile(`(?i)pkg\.go\.dev:\s*(https?://pkg\.go\.dev/\S+)`)
	reGoReport   = regexp.MustCompile(`(?i)goreportcard\.com:\s*(https?://goreportcard\.com/\S+)`)
	reCoverage   = regexp.MustCompile(`(?i)coverage[^:]*:\s*(https?://(?:coveralls\.io|(?:app\.)?codecov\.io)/\S+)`)
	reGrade      = regexp.MustCompile(`(?i)Grade:\s*([A-F][+-]?)`)
	reGithubRepo = regexp.MustCompile(`^https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$`)
	reSemver     = regexp.MustCompile(`^v\d+\.\d+\.\d+`)
)

type prEvent struct {
	PullRequest struct {
		Body string `json:"body"`
	} `json:"pull_request"`
}

type repoData struct {
	Archived bool `json:"archived"`
	License  *struct {
		SPDXID string `json:"spdx_id"`
		Name   string `json:"name"`
	} `json:"license"`
	Message string `json:"message"` // present on API errors
}

type contentEntry struct {
	Name string `json:"name"`
}

type tagEntry struct {
	Name string `json:"name"`
}

type workflowsResponse struct {
	TotalCount int `json:"total_count"`
}

func main() {
	event := readEvent()
	body := event.PullRequest.Body

	forgeLink := captureMatch(body, reForgeLink)
	pkgLink := captureMatch(body, rePkgGoDev)
	gorepLink := captureMatch(body, reGoReport)
	covLink := captureMatch(body, reCoverage)

	var (
		critical     []string
		warnings     []string
		criticalFail bool
		repoOk       bool
		pkgOk        bool
		gorepOk      bool
		coverageOk   bool
		labels       []string
	)

	// --- Repo checks ---
	if forgeLink == "" {
		critical = append(critical, icon(false)+" **Repo link**: missing from PR body")
		critical = append(critical, fix("Add the following to your PR description:", "```\nForge link: https://github.com/your-org/your-project\n```"))
		criticalFail = true
	} else {
		r := checkGithubRepo(forgeLink)
		if !r.ok {
			critical = append(critical, fmt.Sprintf("%s **Repo**: %s", icon(false), r.reason))
			critical = append(critical, repoFixMessage(r.reason, forgeLink))
			criticalFail = true
		} else {
			critical = append(critical, icon(true)+" **Repo**: accessible, has go.mod and SemVer release")
			repoOk = true
		}

		if r.licenseChecked {
			if r.hasLicense {
				warnings = append(warnings, fmt.Sprintf("%s **License**: %s", warnIcon(true), r.licenseName))
			} else {
				warnings = append(warnings, warnIcon(false)+" **License**: no open source license detected")
				warnings = append(warnings, fix("Add a LICENSE file to your repository root.", "Choose one at https://choosealicense.com — common choices for Go projects: MIT, Apache-2.0, BSD-3-Clause."))
				labels = append(labels, "needs-license")
			}
		}
		if r.maturityChecked {
			if r.hasFiveMonths {
				warnings = append(warnings, warnIcon(true)+" **Maturity**: repo has 5+ months of history")
			} else {
				warnings = append(warnings, warnIcon(false)+" **Maturity**: repo appears to have less than 5 months of history")
				warnings = append(warnings, fix("Your repository needs at least 5 months of history since the first commit.", "Please resubmit after the repository meets this requirement."))
				labels = append(labels, "needs-maturity")
			}
		}
		if r.ciChecked {
			if r.hasCI {
				warnings = append(warnings, warnIcon(true)+" **CI/CD**: GitHub Actions workflows detected")
			} else {
				warnings = append(warnings, warnIcon(false)+" **CI/CD**: no GitHub Actions workflows found")
				warnings = append(warnings, fix("Add a CI workflow to run tests automatically.", "Create `.github/workflows/test.yml` — see https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-go"))
			}
		}
		if r.readmeChecked {
			if r.hasReadme {
				warnings = append(warnings, warnIcon(true)+" **README**: present")
			} else {
				warnings = append(warnings, warnIcon(false)+" **README**: not found in repository root")
				warnings = append(warnings, fix("Add a `README.md` to your repository root.", "It should explain what the project does, how to install it, and how to use it, in English."))
			}
		}
	}

	// --- pkg.go.dev ---
	if pkgLink == "" {
		critical = append(critical, icon(false)+" **pkg.go.dev**: missing from PR body")
		critical = append(critical, fix("Add the following to your PR description:", "```\npkg.go.dev: https://pkg.go.dev/github.com/your-org/your-project\n```"))
		criticalFail = true
	} else if !isReachable(pkgLink) {
		critical = append(critical, icon(false)+" **pkg.go.dev**: unreachable")
		critical = append(critical, fix("The pkg.go.dev page could not be reached.", "Ensure your module path in `go.mod` matches the URL. After pushing a tagged release, pkg.go.dev indexes the module automatically — this can take a few minutes. You can trigger it manually by visiting `https://pkg.go.dev/your-module-path`."))
		criticalFail = true
	} else {
		critical = append(critical, icon(true)+" **pkg.go.dev**: OK")
		pkgOk = true
	}

	// --- Go Report Card ---
	if gorepLink == "" {
		critical = append(critical, icon(false)+" **Go Report Card**: missing from PR body")
		if forgeLink != "" {
			critical = append(critical, fix("Add the following to your PR description:", fmt.Sprintf("```\ngoreportcard.com: https://goreportcard.com/report/%s\n```", strings.TrimPrefix(strings.TrimPrefix(forgeLink, "https://"), "http://"))))
		} else {
			critical = append(critical, fix("Add the following to your PR description:", "```\ngoreportcard.com: https://goreportcard.com/report/github.com/your-org/your-project\n```"))
		}
		criticalFail = true
	} else {
		grade, ok := checkGoReportCard(gorepLink)
		if !ok {
			critical = append(critical, fmt.Sprintf("%s **Go Report Card**: %s", icon(false), grade))
			if grade == "unreachable" || grade == "fetch error" {
				critical = append(critical, fix("The Go Report Card page could not be reached.", "Visit https://goreportcard.com and generate a report for your project. Then add the correct link to your PR body."))
			} else {
				critical = append(critical, fix(fmt.Sprintf("Your project received grade **%s** — minimum required is **A-**.", grade), "Run `gofmt -s -w .` to fix formatting, `go vet ./...` to fix vet issues, and review the report at "+gorepLink+" for specific problems to address."))
			}
			criticalFail = true
		} else {
			msg := icon(true) + " **Go Report Card**: OK"
			if grade != "" {
				msg += fmt.Sprintf(" (grade %s)", grade)
			}
			critical = append(critical, msg)
			gorepOk = true
		}
	}

	// --- Coverage ---
	if covLink == "" {
		warnings = append(warnings, warnIcon(false)+" **Coverage**: missing from PR body")
		warnings = append(warnings, fix("Add a coverage service link to your PR description:", "```\nCoverage: https://app.codecov.io/gh/your-org/your-project\n```\nPopular options: [Codecov](https://codecov.io), [Coveralls](https://coveralls.io). Integrate one with your CI to track coverage automatically."))
	} else if !isReachable(covLink) {
		warnings = append(warnings, warnIcon(false)+" **Coverage**: unreachable")
		warnings = append(warnings, fix("The coverage link could not be reached.", "Ensure the coverage service is configured for your repository and the link is correct. If you just set it up, it may need a CI run to generate the first report."))
	} else {
		warnings = append(warnings, warnIcon(true)+" **Coverage**: link accessible")
		coverageOk = true
	}

	// --- Build comment ---
	var lines []string
	lines = append(lines, "## Automated Quality Checks", "")
	lines = append(lines, "### Required checks", "")
	lines = append(lines, critical...)
	lines = append(lines, "", "### Additional checks", "")
	lines = append(lines, warnings...)
	lines = append(lines, "")

	if criticalFail {
		lines = append(lines, "---")
		lines = append(lines, "> **Action needed:** one or more required checks failed. Please update your PR body with the missing links and ensure the repository meets the [quality standards](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#quality-standards).")
	}

	lines = append(lines, "")
	lines = append(lines, "_These checks are automated and do not replace maintainer review. See [CONTRIBUTING.md](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md) for full guidelines._")

	comment := strings.Join(lines, "\n")

	// --- Labels ---
	if forgeLink == "" || pkgLink == "" || gorepLink == "" {
		labels = append(labels, "needs-info")
	}
	if !coverageOk {
		labels = append(labels, "needs-coverage")
	}
	if criticalFail {
		labels = append(labels, "quality:fail")
	}
	if !criticalFail && repoOk && pkgOk && gorepOk {
		labels = append(labels, "quality:ok")
	}

	labelsJSON, _ := json.Marshal(labels)

	setOutput("comment", comment)
	setOutput("fail", boolStr(criticalFail))
	setOutput("labels", string(labelsJSON))
}

// --- Event reading ---

func readEvent() prEvent {
	var ev prEvent
	path := os.Getenv("GITHUB_EVENT_PATH")
	if path == "" {
		return ev
	}
	data, err := os.ReadFile(path)
	if err != nil {
		return ev
	}
	_ = json.Unmarshal(data, &ev)
	return ev
}

// --- Regex helpers ---

func captureMatch(s string, re *regexp.Regexp) string {
	m := re.FindStringSubmatch(s)
	if len(m) < 2 {
		return ""
	}
	return strings.TrimSpace(m[1])
}

// --- HTTP helpers ---

var httpClient = &http.Client{Timeout: 30 * time.Second}

func isReachable(url string) bool {
	req, err := http.NewRequest(http.MethodHead, url, nil)
	if err != nil {
		return false
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		return false
	}
	resp.Body.Close()
	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
		return true
	}
	// Fallback to GET
	req2, _ := http.NewRequest(http.MethodGet, url, nil)
	resp2, err := httpClient.Do(req2)
	if err != nil {
		return false
	}
	resp2.Body.Close()
	return resp2.StatusCode >= 200 && resp2.StatusCode < 400
}

func githubGet(url string) ([]byte, int, error) {
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, 0, err
	}
	req.Header.Set("User-Agent", "awesome-go-quality-check")
	req.Header.Set("Accept", "application/vnd.github+json")
	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, 0, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	return body, resp.StatusCode, err
}

// --- GitHub repo checks ---

type repoCheckResult struct {
	ok              bool
	reason          string
	hasLicense      bool
	licenseName     string
	licenseChecked  bool
	hasFiveMonths   bool
	maturityChecked bool
	hasCI           bool
	ciChecked       bool
	hasReadme       bool
	readmeChecked   bool
}

func checkGithubRepo(repoURL string) repoCheckResult {
	m := reGithubRepo.FindStringSubmatch(repoURL)
	if m == nil {
		return repoCheckResult{reason: "invalid repo url"}
	}
	owner, repo := m[1], m[2]
	base := "https://api.github.com"

	// Fetch repo metadata
	data, status, err := githubGet(fmt.Sprintf("%s/repos/%s/%s", base, owner, repo))
	if err != nil || status >= 400 {
		return repoCheckResult{reason: "repo api not reachable"}
	}

	var rd repoData
	if json.Unmarshal(data, &rd) != nil {
		return repoCheckResult{reason: "repo api not reachable"}
	}
	if rd.Message != "" {
		return repoCheckResult{reason: "repo api not reachable"}
	}
	if rd.Archived {
		return repoCheckResult{reason: "repo is archived"}
	}

	result := repoCheckResult{}

	// License
	result.licenseChecked = true
	if rd.License != nil && rd.License.SPDXID != "" && rd.License.SPDXID != "NOASSERTION" {
		result.hasLicense = true
		result.licenseName = rd.License.SPDXID
	}

	// go.mod
	hasGoMod := false
	goModData, goModStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/contents/go.mod", base, owner, repo))
	if goModStatus == 200 {
		var entry contentEntry
		if json.Unmarshal(goModData, &entry) == nil && entry.Name == "go.mod" {
			hasGoMod = true
		}
	}

	// SemVer tags
	hasSemver := false
	tagsData, tagsStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/tags?per_page=100", base, owner, repo))
	if tagsStatus == 200 {
		var tags []tagEntry
		if json.Unmarshal(tagsData, &tags) == nil {
			for _, t := range tags {
				if reSemver.MatchString(t.Name) {
					hasSemver = true
					break
				}
			}
		}
	}

	// Maturity (5+ months)
	result.maturityChecked = true
	fiveMonthsAgo := time.Now().AddDate(0, -5, 0).Format(time.RFC3339)
	commitsData, commitsStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/commits?per_page=1&until=%s", base, owner, repo, fiveMonthsAgo))
	if commitsStatus == 200 {
		var commits []json.RawMessage
		if json.Unmarshal(commitsData, &commits) == nil && len(commits) > 0 {
			result.hasFiveMonths = true
		}
	}

	// CI/CD (GitHub Actions)
	result.ciChecked = true
	wfData, wfStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/actions/workflows", base, owner, repo))
	if wfStatus == 200 {
		var wf workflowsResponse
		if json.Unmarshal(wfData, &wf) == nil && wf.TotalCount > 0 {
			result.hasCI = true
		}
	}

	// README
	result.readmeChecked = true
	readmeData, readmeStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/readme", base, owner, repo))
	if readmeStatus == 200 {
		var entry contentEntry
		if json.Unmarshal(readmeData, &entry) == nil && entry.Name != "" {
			result.hasReadme = true
		}
	}

	result.ok = hasGoMod && hasSemver
	if !hasGoMod {
		result.reason = "missing go.mod"
	} else if !hasSemver {
		result.reason = "missing semver release"
	}

	return result
}

// --- Go Report Card ---

func checkGoReportCard(url string) (grade string, ok bool) {
	if !isReachable(url) {
		return "unreachable", false
	}
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return "fetch error", false
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		return "fetch error", false
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "fetch error", false
	}
	m := reGrade.FindSubmatch(body)
	if m == nil {
		return "unknown", true // reachable but no grade found
	}
	g := strings.ToUpper(string(m[1]))
	pass := g == "A" || g == "A+" || g == "A-"
	return g, pass
}

// --- Output helpers ---

func setOutput(name, value string) {
	path := os.Getenv("GITHUB_OUTPUT")
	if path == "" {
		fmt.Printf("%s=%s\n", name, value)
		return
	}
	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	fmt.Fprintf(f, "%s<<EOF\n%s\nEOF\n", name, value)
}

func icon(ok bool) string {
	if ok {
		return "\u2705"
	}
	return "\u274C"
}

func warnIcon(ok bool) string {
	if ok {
		return "\u2705"
	}
	return "\u26A0\uFE0F"
}

func boolStr(b bool) string {
	if b {
		return "true"
	}
	return "false"
}

func fix(problem, howToFix string) string {
	return fmt.Sprintf("  > **How to fix:** %s\n  > %s", problem, howToFix)
}

func repoFixMessage(reason, repoURL string) string {
	switch reason {
	case "invalid repo url":
		return fix("The forge link is not a valid repository URL.", "Use the full URL, e.g. `https://github.com/org/project`.")
	case "repo api not reachable":
		return fix("Could not reach the repository via GitHub API.", "Ensure the repository is **public** and the URL is correct.")
	case "repo is archived":
		return fix("This repository is archived on GitHub.", "Archived repositories are not accepted. The project must be actively maintained or at least open to contributions.")
	case "missing go.mod":
		return fix("No `go.mod` file found at the repository root.", "Initialize Go modules in your project:\n  > ```\n  > go mod init github.com/your-org/your-project\n  > go mod tidy\n  > git add go.mod go.sum && git commit -m \"add go module\" && git push\n  > ```")
	case "missing semver release":
		return fix("No SemVer release tag (e.g. `v1.0.0`) found.", "Create a tagged release:\n  > ```\n  > git tag v1.0.0\n  > git push origin v1.0.0\n  > ```\n  > Or create a release via GitHub's UI at `"+repoURL+"/releases/new`.")
	default:
		return fix(reason, "Review the [quality standards](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#quality-standards).")
	}
}
