package main

import (
	"bufio"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"lukechampine.com/blake3"
)

const releaseFilesDir = "release-files"

// GitGud.io API constants
const (
	gitGudProjectID  = "45219"
	gitGudBaseURL    = "https://gitgud.io/api/v4"
	gitGudProjectURL = "https://gitgud.io/mike/mpv-manager"
)

// Command-line flags
var (
	autoMode       = flag.Bool("auto", false, "Run in non-interactive mode (use fetched defaults, exit on errors)")
	managerVersion = flag.String("manager-version", "", "MPV Manager version to use (required in auto mode)")
	outputFile     = flag.String("output", "releases.json", "Output file path")
	quiet          = flag.Bool("quiet", false, "Suppress progress output (useful for cron)")
	repoStats      = flag.Bool("repo-stats", false, "Generate repo-stats.json instead of releases.json")
	statsOutput    = flag.String("stats-output", "", "Output path for repo-stats.json (default: same directory as releases.json)")
	apiToken       = flag.String("token", "", "Project Access Token for authenticated API requests (optional)")
)

// GitHubRelease represents the response from GitHub's releases API
type GitHubRelease struct {
	TagName string `json:"tag_name"`
	Name    string `json:"name"`
	HTMLURL string `json:"html_url"`
	Assets  []struct {
		Name string `json:"name"`
		URL  string `json:"browser_download_url"`
	} `json:"assets"`
}

// GitLabRelease represents a GitLab release API response
type GitLabRelease struct {
	TagName     string `json:"tag_name"`
	Name        string `json:"name"`
	Description string `json:"description"`
	ReleasedAt  string `json:"released_at"`
}

type usedFile struct {
	url      string
	filename string
}

var allUsedFiles []usedFile

type Release struct {
	Version    string `json:"version"`
	MpvVersion string `json:mpv-version`
	Date       string `json:"date"`
	Windows    struct {
		I686    struct{ URL, BLAKE3 string } `json:"i686"`
		X8664   struct{ URL, BLAKE3 string } `json:"x86-64"`
		X8664v3 struct{ URL, BLAKE3 string } `json:"x86-64-v3"`
		Aarch64 struct{ URL, BLAKE3 string } `json:"aarch64"`
	} `json:"windows"`
	MacOS struct {
		ARMLatest struct{ URL, BLAKE3 string } `json:"arm-latest"`
		ARM15     struct{ URL, BLAKE3 string } `json:"arm-15"`
		Intel15   struct{ URL, BLAKE3 string } `json:"intel-15"`
	} `json:"macos"`
	FFmpeg struct {
		I686       struct{ URL, BLAKE3 string } `json:"i686"`
		X8664      struct{ URL, BLAKE3 string } `json:"x86-64"`
		X8664v3    struct{ URL, BLAKE3 string } `json:"x86-64-v3"`
		Aarch64    struct{ URL, BLAKE3 string } `json:"aarch64"`
		AppVersion string                       `json:"app_version"`
	} `json:"ffmpeg"`
	UOSC struct {
		URL        string `json:"url"`
		BLAKE3     string `json:"blake3"`
		ConfURL    string `json:"conf_url"`
		ConfBLAKE3 string `json:"conf_blake3"`
		AppVersion string `json:"app_version"`
	} `json:"uosc"`
	ModernZ struct {
		ScriptURL        string `json:"script_url"` // modernz.lua
		ScriptBLAKE3     string `json:"script_blake3"`
		FontURL          string `json:"font_url"` // material-design-icons.ttf
		FontBLAKE3       string `json:"font_blake3"`
		FluentFontURL    string `json:"fluent_font_url"` // fluent-system-icons.ttf
		FluentFontBLAKE3 string `json:"fluent_font_blake3"`
		ConfURL          string `json:"conf_url"` // modernz.conf
		ConfBLAKE3       string `json:"conf_blake3"`
		AppVersion       string `json:"app_version"`
	} `json:"modernz"`
	MPCQT struct {
		X8664      struct{ URL, BLAKE3 string } `json:"x86-64"`
		AppVersion string                       `json:"app_version"`
	} `json:"mpc-qt"`
	IINA struct {
		ARM        struct{ URL, BLAKE3 string } `json:"arm"`
		Intel      struct{ URL, BLAKE3 string } `json:"intel"`
		AppVersion string                       `json:"app_version"`
	} `json:"iina"`
	Manager struct {
		LinuxAMD64 struct{ URL, BLAKE3 string } `json:"linux-amd64"`
		LinuxARM64 struct{ URL, BLAKE3 string } `json:"linux-arm64"`
		WinX86_64  struct{ URL, BLAKE3 string } `json:"win-x86_64"`
		WinARM64   struct{ URL, BLAKE3 string } `json:"win-arm64"`
		MacosIntel struct{ URL, BLAKE3 string } `json:"macos-intel"`
		MacosARM   struct{ URL, BLAKE3 string } `json:"macos-arm"`
	} `json:"manager"`
}

// GitLab API response types for repository statistics

// GitLabProject represents the main project data from GitLab API
type GitLabProject struct {
	StarCount      int            `json:"star_count"`
	ForksCount     int            `json:"forks_count"`
	LastActivityAt string         `json:"last_activity_at"`
	CreatedAt      string         `json:"created_at"`
	License        *GitLabLicense `json:"license"`
	Topics         []string       `json:"topics"`
	Description    string         `json:"description"`
	WebURL         string         `json:"web_url"`
	DefaultBranch  string         `json:"default_branch"`
	Visibility     string         `json:"visibility"`
}

// GitLabLicense represents license information from GitLab API
type GitLabLicense struct {
	Key     string `json:"key"`
	Name    string `json:"name"`
	URL     string `json:"url"`
	HTMLURL string `json:"html_url"`
}

// GitLabIssuesStats represents the issues statistics response from GitLab API
type GitLabIssuesStats struct {
	Statistics struct {
		Counts struct {
			Opened int `json:"opened"`
		} `json:"counts"`
	} `json:"statistics"`
}

// GitLabPipeline represents a CI/CD pipeline from GitLab API
type GitLabPipeline struct {
	Status    string `json:"status"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	Ref       string `json:"ref"`
	SHA       string `json:"sha"`
}

// RepoStatsLicense represents the license in the output JSON format
type RepoStatsLicense struct {
	Key  string `json:"key"`
	Name string `json:"name"`
	URL  string `json:"url"`
}

// RepoStatsPipeline represents CI/CD pipeline info in output
type RepoStatsPipeline struct {
	Status string `json:"status"`
	Ref    string `json:"ref"`
	SHA    string `json:"sha"`
}

// RepoStats is the combined output structure for repo-stats.json
type RepoStats struct {
	StarCount       int                `json:"star_count"`
	ForksCount      int                `json:"forks_count"`
	OpenIssuesCount int                `json:"open_issues_count"`
	CIStatus        int                `json:"ci_status"`
	CIPipeline      *RepoStatsPipeline `json:"ci_pipeline,omitempty"`
	LastActivityAt  string             `json:"last_activity_at"`
	CreatedAt       string             `json:"created_at"`
	License         *RepoStatsLicense  `json:"license"`
	Topics          []string           `json:"topics"`
	Description     string             `json:"description"`
	WebURL          string             `json:"web_url"`
	DefaultBranch   string             `json:"default_branch"`
	Visibility      string             `json:"visibility"`
	FetchedAt       string             `json:"fetched_at"`
}

// fetchLatestRelease fetches the latest release from a GitHub repository
func fetchLatestRelease(owner, repo string) (*GitHubRelease, error) {
	apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(apiURL)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch release: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	var release GitHubRelease
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return &release, nil
}

// fetchLatestGitLabRelease fetches the latest release from gitgud.io
func fetchLatestGitLabRelease(projectPath string) (*GitLabRelease, error) {
	// URL encode the project path: mike/mpv-manager -> mike%2Fmpv-manager
	encodedPath := url.PathEscape(projectPath)
	apiURL := fmt.Sprintf("https://gitgud.io/api/v4/projects/%s/releases", encodedPath)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(apiURL)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch from GitLab API: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("GitLab API returned status %d", resp.StatusCode)
	}

	var releases []GitLabRelease
	if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if len(releases) == 0 {
		return nil, fmt.Errorf("no releases found for project %s", projectPath)
	}

	return &releases[0], nil
}

// extractVersionFromTag removes 'v' prefix from tag names
func extractVersionFromTag(tag string) string {
	return strings.TrimPrefix(tag, "v")
}

// parseWindowsBuildInfo extracts timestamp and hashes from Windows build release
// MPV and FFmpeg have different commit hashes in the release
func parseWindowsBuildInfo(release *GitHubRelease) (timestamp, mpvHash, ffmpegHash string) {
	if release == nil {
		return "", "", ""
	}
	// The tag is just the timestamp
	timestamp = release.TagName

	// MPV pattern: mpv-x86_64-20260211-git-3847a81.7z
	mpvRe := regexp.MustCompile(`mpv-x86_64-\d+-git-([a-f0-9]+)\.7z$`)
	// FFmpeg pattern: ffmpeg-x86_64-git-f6d89a7be.7z
	ffmpegRe := regexp.MustCompile(`ffmpeg-x86_64-git-([a-f0-9]+)\.7z$`)

	for _, asset := range release.Assets {
		if mpvHash == "" {
			if matches := mpvRe.FindStringSubmatch(asset.URL); len(matches) > 1 {
				mpvHash = matches[1]
			}
		}
		if ffmpegHash == "" {
			if matches := ffmpegRe.FindStringSubmatch(asset.URL); len(matches) > 1 {
				ffmpegHash = matches[1]
			}
		}
		if mpvHash != "" && ffmpegHash != "" {
			break
		}
	}

	return timestamp, mpvHash, ffmpegHash
}

// fetchAllVersions fetches latest versions from all GitHub repositories
func fetchAllVersions() (map[string]string, string, string, error) {
	versions := make(map[string]string)
	var winTimestamp string

	println("\n--- Fetching Latest Versions from GitHub ---")

	// MPV
	print("  MPV... ")
	if release, err := fetchLatestRelease("mpv-player", "mpv"); err != nil {
		print("ERROR: %v\n", err)
		versions["mpv"] = "0.41.0"
	} else {
		versions["mpv"] = extractVersionFromTag(release.TagName)
		println(versions["mpv"])
	}

	// uOSC
	print("  uOSC... ")
	if release, err := fetchLatestRelease("tomasklaen", "uosc"); err != nil {
		print("ERROR: %v\n", err)
		versions["uosc"] = "5.12.0"
	} else {
		versions["uosc"] = release.TagName // uOSC uses tags without 'v' prefix
		println(versions["uosc"])
	}

	// ModernZ
	print("  ModernZ... ")
	if release, err := fetchLatestRelease("Samillion", "ModernZ"); err != nil {
		print("ERROR: %v\n", err)
		versions["modernz"] = "0.3.0"
	} else {
		versions["modernz"] = extractVersionFromTag(release.TagName)
		println(versions["modernz"])
	}

	// MPC-QT
	print("  MPC-QT... ")
	if release, err := fetchLatestRelease("mpc-qt", "mpc-qt"); err != nil {
		print("ERROR: %v\n", err)
		versions["mpcqt"] = "26.01"
	} else {
		versions["mpcqt"] = extractVersionFromTag(release.TagName)
		println(versions["mpcqt"])
	}

	// IINA
	print("  IINA... ")
	if release, err := fetchLatestRelease("iina", "iina"); err != nil {
		print("ERROR: %v\n", err)
		versions["iina"] = "1.4.1"
	} else {
		versions["iina"] = extractVersionFromTag(release.TagName)
		println(versions["iina"])
	}

	// Windows builds (shinchiro/mpv-winbuild-cmake)
	print("  Windows builds... ")
	var winMpvHash, winFFmpegHash string
	if release, err := fetchLatestRelease("shinchiro", "mpv-winbuild-cmake"); err != nil {
		print("ERROR: %v\n", err)
		winTimestamp = time.Now().Format("20060102")
		winMpvHash = "unknown"
		winFFmpegHash = "unknown"
	} else {
		winTimestamp, winMpvHash, winFFmpegHash = parseWindowsBuildInfo(release)
		print("%s (mpv: %s, ffmpeg: %s)\n", winTimestamp, winMpvHash, winFFmpegHash)
	}
	versions["win_mpv_hash"] = winMpvHash
	versions["win_ffmpeg_hash"] = winFFmpegHash

	return versions, winTimestamp, winMpvHash, nil
}

func extractFilenameFromURL(url string) string {
	parts := strings.Split(url, "/")
	return parts[len(parts)-1]
}

func getLocalFilePath(url string) string {
	filename := extractFilenameFromURL(url)
	return filepath.Join(releaseFilesDir, filename)
}

// getCachedURL reads the URL from a .url sidecar file for a cached file
func getCachedURL(localPath string) string {
	urlFile := localPath + ".url"
	data, err := os.ReadFile(urlFile)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

// setCachedURL writes the URL to a .url sidecar file for a cached file
func setCachedURL(localPath, url string) error {
	urlFile := localPath + ".url"
	return os.WriteFile(urlFile, []byte(url), 0644)
}

func downloadFileIfNotExists(url string) (string, error) {
	localPath := getLocalFilePath(url)

	// Check if file exists AND the URL matches (to handle version changes)
	if _, err := os.Stat(localPath); err == nil {
		cachedURL := getCachedURL(localPath)
		if cachedURL == url {
			print("  Using cached file: %s\n", extractFilenameFromURL(url))
			return localPath, nil
		}
		// URL changed (e.g., new version), need to re-download
		print("  Cache outdated (URL changed), re-downloading: %s\n", extractFilenameFromURL(url))
		os.Remove(localPath)
		os.Remove(localPath + ".url")
	}

	print("  Downloading: %s\n", url)
	print("  Saving to: %s\n", extractFilenameFromURL(url))

	resp, err := http.Get(url)
	if err != nil {
		return "", fmt.Errorf("failed to download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	if err := os.MkdirAll(releaseFilesDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create directory: %w", err)
	}

	out, err := os.Create(localPath)
	if err != nil {
		return "", fmt.Errorf("failed to create file: %w", err)
	}
	defer out.Close()

	if _, err := io.Copy(out, resp.Body); err != nil {
		return "", fmt.Errorf("failed to save download: %w", err)
	}

	// Save the URL to sidecar file for cache validation
	if err := setCachedURL(localPath, url); err != nil {
		print("  Warning: Failed to save cache metadata: %v\n", err)
	}

	return localPath, nil
}

func getUsedFiles(release Release) []usedFile {
	var files []usedFile

	addFile := func(url string) {
		files = append(files, usedFile{
			url:      url,
			filename: extractFilenameFromURL(url),
		})
	}

	files = append(files, usedFile{
		url:      release.UOSC.ConfURL,
		filename: extractFilenameFromURL(release.UOSC.ConfURL),
	})

	files = append(files, usedFile{
		url:      release.ModernZ.ConfURL,
		filename: extractFilenameFromURL(release.ModernZ.ConfURL),
	})

	addFile(release.Windows.I686.URL)
	addFile(release.Windows.X8664.URL)
	addFile(release.Windows.X8664v3.URL)
	addFile(release.Windows.Aarch64.URL)
	addFile(release.FFmpeg.I686.URL)
	addFile(release.FFmpeg.X8664.URL)
	addFile(release.FFmpeg.X8664v3.URL)
	addFile(release.FFmpeg.Aarch64.URL)
	addFile(release.MacOS.ARMLatest.URL)
	addFile(release.MacOS.ARM15.URL)
	addFile(release.MacOS.Intel15.URL)
	addFile(release.UOSC.URL)
	addFile(release.ModernZ.ScriptURL)
	addFile(release.ModernZ.FontURL)
	addFile(release.ModernZ.FluentFontURL)
	addFile(release.MPCQT.X8664.URL)
	addFile(release.IINA.ARM.URL)
	addFile(release.IINA.Intel.URL)
	addFile(release.Manager.LinuxAMD64.URL)
	addFile(release.Manager.LinuxARM64.URL)
	addFile(release.Manager.WinX86_64.URL)
	addFile(release.Manager.WinARM64.URL)
	addFile(release.Manager.MacosIntel.URL)
	addFile(release.Manager.MacosARM.URL)

	return files
}

func cleanupUnusedFiles(usedFiles []usedFile) {
	entries, err := os.ReadDir(releaseFilesDir)
	if err != nil {
		if os.IsNotExist(err) {
			return
		}
		if !*quiet {
			fmt.Printf("Error reading release-files directory: %v\n", err)
		}
		return
	}

	if len(entries) == 0 {
		if !*quiet {
			fmt.Println("\nNo files in release-files directory to clean up.")
		}
		return
	}

	usedFilenames := make(map[string]bool)
	for _, file := range usedFiles {
		usedFilenames[file.filename] = true
	}

	var unusedFiles []string
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		// Skip .url sidecar files - they're cleaned up with their main file
		if strings.HasSuffix(entry.Name(), ".url") {
			continue
		}
		if !usedFilenames[entry.Name()] {
			unusedFiles = append(unusedFiles, entry.Name())
		}
	}

	if len(unusedFiles) == 0 {
		if !*quiet {
			fmt.Println("\nAll files in release-files are in use.")
		}
		return
	}

	// In auto mode, remove unused files silently
	if *autoMode {
		for _, file := range unusedFiles {
			filePath := filepath.Join(releaseFilesDir, file)
			os.Remove(filePath)
			// Also remove .url sidecar file if it exists
			os.Remove(filePath + ".url")
		}
		return
	}

	// Interactive mode - show files and ask
	fmt.Printf("\nFound %d unused file(s) in release-files:\n", len(unusedFiles))
	for _, file := range unusedFiles {
		fmt.Printf("  - %s\n", file)
	}

	fmt.Printf("\nRemove unused files? [Y/n]: ")
	reader := bufio.NewReader(os.Stdin)
	input, err := reader.ReadString('\n')
	if err != nil {
		return
	}
	input = strings.TrimSpace(input)
	if strings.ToLower(input) == "n" {
		fmt.Println("Keeping unused files.")
		return
	}

	for _, file := range unusedFiles {
		filePath := filepath.Join(releaseFilesDir, file)
		if err := os.Remove(filePath); err != nil {
			fmt.Printf("Error removing %s: %v\n", file, err)
		} else {
			fmt.Printf("Removed: %s\n", file)
		}
		// Also remove .url sidecar file if it exists
		os.Remove(filePath + ".url")
	}
	fmt.Println("Cleanup complete.")
}

// print outputs text unless quiet mode is enabled
func print(format string, args ...interface{}) {
	if !*quiet {
		fmt.Printf(format, args...)
	}
}

// println outputs a line unless quiet mode is enabled
func println(args ...interface{}) {
	if !*quiet {
		fmt.Println(args...)
	}
}

// --- GitLab API functions for repository statistics ---

// mapPipelineStatus converts GitLab pipeline status string to numeric code
func mapPipelineStatus(status string) int {
	switch status {
	case "success":
		return 0
	case "pending":
		return 1
	case "running":
		return 2
	case "failed":
		return 3
	case "canceled":
		return 4
	case "skipped":
		return 5
	default:
		return -1
	}
}

// doGitLabRequest performs an HTTP request to GitLab API with optional authentication
func doGitLabRequest(url string) (*http.Response, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	if *apiToken != "" {
		req.Header.Set("Authorization", "Bearer "+*apiToken)
	}

	client := &http.Client{Timeout: 10 * time.Second}
	return client.Do(req)
}

// fetchGitLabProject fetches main project information from GitLab API
func fetchGitLabProject() (*GitLabProject, error) {
	url := fmt.Sprintf("%s/projects/%s?license=true", gitGudBaseURL, gitGudProjectID)
	print("  Fetching project info... ")

	resp, err := doGitLabRequest(url)
	if err != nil {
		print("ERROR: %v\n", err)
		return nil, fmt.Errorf("failed to fetch project: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		print("HTTP %d\n", resp.StatusCode)
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	var project GitLabProject
	if err := json.NewDecoder(resp.Body).Decode(&project); err != nil {
		print("Parse error: %v\n", err)
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	// Ensure WebURL is set
	if project.WebURL == "" {
		project.WebURL = gitGudProjectURL
	}

	println(project.StarCount, "stars,", project.ForksCount, "forks")
	return &project, nil
}

// fetchGitLabIssuesStats fetches issues statistics from GitLab API
func fetchGitLabIssuesStats() (*GitLabIssuesStats, error) {
	url := fmt.Sprintf("%s/projects/%s/issues_statistics", gitGudBaseURL, gitGudProjectID)
	print("  Fetching issues stats... ")

	resp, err := doGitLabRequest(url)
	if err != nil {
		print("ERROR: %v\n", err)
		return nil, fmt.Errorf("failed to fetch issues stats: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		print("HTTP %d\n", resp.StatusCode)
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	var stats GitLabIssuesStats
	if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
		print("Parse error: %v\n", err)
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	println(stats.Statistics.Counts.Opened, "open issues")
	return &stats, nil
}

// fetchGitLabPipelineStatus fetches the latest pipeline status from GitLab API
func fetchGitLabPipelineStatus() (*GitLabPipeline, error) {
	url := fmt.Sprintf("%s/projects/%s/pipelines?per_page=1", gitGudBaseURL, gitGudProjectID)
	print("  Fetching pipeline status... ")

	resp, err := doGitLabRequest(url)
	if err != nil {
		print("ERROR: %v\n", err)
		return nil, fmt.Errorf("failed to fetch pipeline: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		print("HTTP %d\n", resp.StatusCode)
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	var pipelines []GitLabPipeline
	if err := json.NewDecoder(resp.Body).Decode(&pipelines); err != nil {
		print("Parse error: %v\n", err)
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if len(pipelines) == 0 {
		println("no pipelines found")
		return nil, fmt.Errorf("no pipelines found")
	}

	println(pipelines[0].Status, "-", pipelines[0].Ref)
	return &pipelines[0], nil
}

// generateRepoStats creates a RepoStats structure with fetched data
// Falls back to defaults for any data that couldn't be fetched
func generateRepoStats() *RepoStats {
	stats := &RepoStats{
		StarCount:       0,
		ForksCount:      0,
		OpenIssuesCount: 0,
		CIStatus:        -1,
		LastActivityAt:  "",
		CreatedAt:       "",
		License:         nil,
		Topics:          []string{},
		Description:     "",
		WebURL:          gitGudProjectURL,
		DefaultBranch:   "master",
		Visibility:      "public",
		FetchedAt:       time.Now().UTC().Format(time.RFC3339),
	}

	// Fetch project info
	if project, err := fetchGitLabProject(); err == nil {
		stats.StarCount = project.StarCount
		stats.ForksCount = project.ForksCount
		stats.LastActivityAt = project.LastActivityAt
		stats.CreatedAt = project.CreatedAt
		stats.Topics = project.Topics
		stats.Description = project.Description
		stats.WebURL = project.WebURL
		stats.DefaultBranch = project.DefaultBranch
		stats.Visibility = project.Visibility

		// Process license
		if project.License != nil {
			licenseURL := project.License.URL
			if licenseURL == "" && project.License.HTMLURL != "" {
				licenseURL = project.License.HTMLURL
			}
			stats.License = &RepoStatsLicense{
				Key:  project.License.Key,
				Name: project.License.Name,
				URL:  licenseURL,
			}
		}
	} else {
		print("  Warning: Using defaults for project info\n")
	}

	// Fetch issues stats
	if issues, err := fetchGitLabIssuesStats(); err == nil {
		stats.OpenIssuesCount = issues.Statistics.Counts.Opened
	} else {
		print("  Warning: Using 0 for open issues count\n")
	}

	// Fetch pipeline status
	if pipeline, err := fetchGitLabPipelineStatus(); err == nil {
		stats.CIStatus = mapPipelineStatus(pipeline.Status)
		stats.CIPipeline = &RepoStatsPipeline{
			Status: pipeline.Status,
			Ref:    pipeline.Ref,
			SHA:    pipeline.SHA,
		}
	} else {
		print("  Warning: Using -1 for CI status\n")
	}

	return stats
}

// writeRepoStatsFile writes the RepoStats to a JSON file
func writeRepoStatsFile(stats *RepoStats, outputPath string) error {
	// Ensure directory exists
	dir := filepath.Dir(outputPath)
	if dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create directory: %w", err)
		}
	}

	data, err := json.MarshalIndent(stats, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal JSON: %w", err)
	}

	if err := os.WriteFile(outputPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}

	return nil
}

// computeHashForURL downloads a file and computes its BLAKE3 hash
// In auto mode, returns error on failure; in interactive mode, returns default hash
func computeHashForURL(prompt, url, defaultHash string) (string, error) {
	filePath, err := downloadFileIfNotExists(url)
	if err != nil {
		if *autoMode {
			return "", fmt.Errorf("failed to download %s: %w", url, err)
		}
		print("  Error downloading: %v\n", err)
		print("  Using default value: %s\n", defaultHash)
		return defaultHash, nil
	}

	hash, err := computeBLAKE3(filePath)
	if err != nil {
		if *autoMode {
			return "", fmt.Errorf("failed to compute hash for %s: %w", url, err)
		}
		print("  Error computing hash: %v\n", err)
		print("  Using default value: %s\n", defaultHash)
		return defaultHash, nil
	}

	allUsedFiles = append(allUsedFiles, usedFile{
		url:      url,
		filename: extractFilenameFromURL(url),
	})

	print("  Computed BLAKE3: %s\n", hash)
	return hash, nil
}

// runRepoStatsMode handles the -repo-stats flag to generate repository statistics
func runRepoStatsMode() {
	if !*quiet {
		fmt.Println("MPV.Rocks Manager - Repository Stats Generator")
		fmt.Println("==============================================")
		fmt.Println("Fetching repository statistics from GitGud.io")
		fmt.Println()
	}

	// Generate stats (with graceful error handling)
	stats := generateRepoStats()

	// Determine output path
	outputPath := *statsOutput
	if outputPath == "" {
		// Default: same directory as releases.json
		outputPath = filepath.Join(filepath.Dir(*outputFile), "repo-stats.json")
		// If releases.json path has no directory, use current directory
		if outputPath == "repo-stats.json" || outputPath == "./repo-stats.json" {
			outputPath = "repo-stats.json"
		}
	}

	// Write to file
	if err := writeRepoStatsFile(stats, outputPath); err != nil {
		fmt.Fprintf(os.Stderr, "Error writing repo-stats.json: %v\n", err)
		os.Exit(1)
	}

	print("\n✓ Generated %s\n", outputPath)
	print("  Stars: %d, Forks: %d, Open Issues: %d, CI Status: %d\n",
		stats.StarCount, stats.ForksCount, stats.OpenIssuesCount, stats.CIStatus)
}

func printUsage() {
	fmt.Fprintf(os.Stderr, "MPV.Rocks Manager - Info Generator\n")
	fmt.Fprintf(os.Stderr, "===================================\n\n")
	fmt.Fprintf(os.Stderr, "This tool generates JSON files for https://mpv.rocks\n\n")
	fmt.Fprintf(os.Stderr, "USAGE:\n")
	fmt.Fprintf(os.Stderr, "  generate-info [OPTIONS]           Generate releases.json (default)\n")
	fmt.Fprintf(os.Stderr, "  generate-info -repo-stats [OPTS]  Generate repo-stats.json\n\n")
	fmt.Fprintf(os.Stderr, "MODES:\n")
	fmt.Fprintf(os.Stderr, "  (default)  Fetch release versions from GitHub, download binaries,\n")
	fmt.Fprintf(os.Stderr, "             compute BLAKE3 hashes, generate releases.json\n")
	fmt.Fprintf(os.Stderr, "  -repo-stats  Fetch repository statistics from GitGud.io API\n")
	fmt.Fprintf(os.Stderr, "               (stars, forks, issues, CI status) and generate repo-stats.json\n\n")
	fmt.Fprintf(os.Stderr, "OPTIONS:\n")
	flag.PrintDefaults()
	fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
	fmt.Fprintf(os.Stderr, "  # Generate releases.json (interactive mode)\n")
	fmt.Fprintf(os.Stderr, "  generate-info\n\n")
	fmt.Fprintf(os.Stderr, "  # Generate releases.json (auto mode for cron)\n")
	fmt.Fprintf(os.Stderr, "  generate-info -auto -quiet\n\n")
	fmt.Fprintf(os.Stderr, "  # Generate repo-stats.json\n")
	fmt.Fprintf(os.Stderr, "  generate-info -repo-stats\n\n")
	fmt.Fprintf(os.Stderr, "  # Generate repo-stats.json with custom output path\n")
	fmt.Fprintf(os.Stderr, "  generate-info -repo-stats -stats-output /var/www/api/repo-stats.json\n\n")
	fmt.Fprintf(os.Stderr, "  # Generate repo-stats.json with API token\n")
	fmt.Fprintf(os.Stderr, "  generate-info -repo-stats -token glpat-xxxxxxxxxxxx -quiet\n")
}

func main() {
	flag.Usage = printUsage
	flag.Parse()

	// Handle repo-stats mode
	if *repoStats {
		runRepoStatsMode()
		return
	}

	if !*quiet {
		fmt.Println("MPV.Rocks Manager - Releases JSON Generator")
		fmt.Println("============================================")
		fmt.Println("This generates a releases.json file for https://mpv.rocks")
		if *autoMode {
			fmt.Println("Running in AUTO mode (non-interactive)")
		} else {
			fmt.Println("Version information is fetched automatically from GitHub.")
		}
		fmt.Println()
	}

	// In auto mode, manager version can be fetched from GitLab or provided via flag
	var managerVer string
	if *autoMode {
		// Try to fetch manager version from GitLab API
		print("  Fetching MPV Manager version from GitLab... ")
		if gitlabRelease, err := fetchLatestGitLabRelease("mike/mpv-manager"); err != nil {
			print("ERROR: %v\n", err)
			if *managerVersion == "" {
				fmt.Fprintln(os.Stderr, "Error: Failed to fetch manager version from GitLab and --manager-version not provided")
				fmt.Fprintln(os.Stderr, "Usage: generate-info --auto --manager-version 1.0.0")
				os.Exit(1)
			}
			managerVer = *managerVersion
		} else {
			managerVer = extractVersionFromTag(gitlabRelease.TagName)
			println(managerVer)
		}
	}

	// Fetch all versions from GitHub
	versions, winTimestamp, _, err := fetchAllVersions()
	if err != nil {
		if *autoMode {
			fmt.Fprintf(os.Stderr, "Error fetching versions: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Error fetching versions: %v\n", err)
		os.Exit(1)
	}

	release := Release{}

	var mpvVersion, releaseDate, winMpvHash, winFFmpegHash string
	var uoscVersion, modernZVersion, mpcqtVersion, iinaVersion string

	if *autoMode {
		// Use fetched defaults automatically
		mpvVersion = versions["mpv"]
		releaseDate = time.Now().Format("2006-01-02")
		winMpvHash = versions["win_mpv_hash"]
		winFFmpegHash = versions["win_ffmpeg_hash"]
		uoscVersion = versions["uosc"]
		modernZVersion = versions["modernz"]
		mpcqtVersion = versions["mpcqt"]
		iinaVersion = versions["iina"]
	} else {
		// Interactive mode - prompt for versions
		reader := bufio.NewReader(os.Stdin)

		println("\n--- Confirm/Override Versions ---")
		println("(Press Enter to use the fetched version)")

		mpvVersion = promptString(reader, "MPV Version", versions["mpv"])
		releaseDate = promptString(reader, "Release Date", time.Now().Format("2006-01-02"))

		fmt.Println("\n--- Windows & FFmpeg Builds ---")
		winTimestamp = promptString(reader, "Windows Build Timestamp (YYYYMMDD)", winTimestamp)
		winMpvHash = promptString(reader, "Windows MPV Commit Hash", versions["win_mpv_hash"])
		winFFmpegHash = promptString(reader, "FFmpeg Commit Hash", versions["win_ffmpeg_hash"])

		uoscVersion = promptString(reader, "uOSC Version", versions["uosc"])
		modernZVersion = promptString(reader, "ModernZ Version", versions["modernz"])
		mpcqtVersion = promptString(reader, "MPC-QT Version", versions["mpcqt"])
		iinaVersion = promptString(reader, "IINA Version", versions["iina"])

		fmt.Println("\n--- MPV Manager ---")
		if *managerVersion != "" {
			managerVer = *managerVersion
			fmt.Printf("MPV Manager Version: %s (from flag)\n", managerVer)
		} else {
			// Try to fetch from GitLab
			print("  Fetching MPV Manager version from GitLab... ")
			if gitlabRelease, err := fetchLatestGitLabRelease("mike/mpv-manager"); err != nil {
				print("ERROR: %v\n", err)
				managerVer = promptString(reader, "MPV Manager Version", "1.0.0")
			} else {
				managerVer = extractVersionFromTag(gitlabRelease.TagName)
				println(managerVer)
			}
		}
	}

	// Now process all items
	print("\n--- Processing URLs and Computing Hashes ---\n")

	release.Version = managerVer
	release.MpvVersion = mpvVersion
	release.Date = releaseDate

	// Helper to compute hash and exit on error in auto mode
	hashOrExit := func(prompt, url, defaultHash string) string {
		hash, err := computeHashForURL(prompt, url, defaultHash)
		if err != nil {
			if *autoMode {
				fmt.Fprintf(os.Stderr, "Error: %v\n", err)
				os.Exit(1)
			}
			return defaultHash
		}
		return hash
	}

	// Generate Windows URLs
	print("Computing hashes for Windows binaries...\n")
	release.Windows.I686.URL = generateWindowsURL(winTimestamp, "i686", winMpvHash)
	release.Windows.I686.BLAKE3 = hashOrExit("Windows i686", release.Windows.I686.URL, "blake3:HASH")
	release.Windows.X8664.URL = generateWindowsURL(winTimestamp, "x86_64", winMpvHash)
	release.Windows.X8664.BLAKE3 = hashOrExit("Windows x86-64", release.Windows.X8664.URL, "blake3:HASH")
	release.Windows.X8664v3.URL = generateWindowsURL(winTimestamp, "x86_64-v3", winMpvHash)
	release.Windows.X8664v3.BLAKE3 = hashOrExit("Windows x86-64-v3", release.Windows.X8664v3.URL, "blake3:HASH")
	release.Windows.Aarch64.URL = generateWindowsURL(winTimestamp, "aarch64", winMpvHash)
	release.Windows.Aarch64.BLAKE3 = hashOrExit("Windows aarch64", release.Windows.Aarch64.URL, "blake3:HASH")

	// Generate FFmpeg URLs (uses different hash than Windows MPV builds)
	print("Computing hashes for FFmpeg binaries...\n")
	release.FFmpeg.I686.URL = generateFFmpegURL(winTimestamp, "i686", winFFmpegHash)
	release.FFmpeg.I686.BLAKE3 = hashOrExit("FFmpeg i686", release.FFmpeg.I686.URL, "blake3:HASH")
	release.FFmpeg.X8664.URL = generateFFmpegURL(winTimestamp, "x86_64", winFFmpegHash)
	release.FFmpeg.X8664.BLAKE3 = hashOrExit("FFmpeg x86-64", release.FFmpeg.X8664.URL, "blake3:HASH")
	release.FFmpeg.X8664v3.URL = generateFFmpegURL(winTimestamp, "x86_64-v3", winFFmpegHash)
	release.FFmpeg.X8664v3.BLAKE3 = hashOrExit("FFmpeg x86-64-v3", release.FFmpeg.X8664v3.URL, "blake3:HASH")
	release.FFmpeg.Aarch64.URL = generateFFmpegURL(winTimestamp, "aarch64", winFFmpegHash)
	release.FFmpeg.Aarch64.BLAKE3 = hashOrExit("FFmpeg aarch64", release.FFmpeg.Aarch64.URL, "blake3:HASH")
	release.FFmpeg.AppVersion = fmt.Sprintf("%s-%s", winTimestamp, winFFmpegHash)

	// macOS binaries
	print("Computing hashes for macOS binaries...\n")
	release.MacOS.ARMLatest.URL = generateMacOSURL(mpvVersion, "macos", "26-arm")
	release.MacOS.ARMLatest.BLAKE3 = hashOrExit("macOS ARM Latest", release.MacOS.ARMLatest.URL, "blake3:HASH")
	release.MacOS.ARM15.URL = generateMacOSURL(mpvVersion, "macos", "15-arm")
	release.MacOS.ARM15.BLAKE3 = hashOrExit("macOS ARM 15", release.MacOS.ARM15.URL, "blake3:HASH")
	release.MacOS.Intel15.URL = generateMacOSURL(mpvVersion, "macos", "15-intel")
	release.MacOS.Intel15.BLAKE3 = hashOrExit("macOS Intel 15", release.MacOS.Intel15.URL, "blake3:HASH")

	// uOSC
	print("Computing hashes for uOSC...\n")
	release.UOSC.URL = generateUOSCURL(uoscVersion)
	release.UOSC.BLAKE3 = hashOrExit("uOSC", release.UOSC.URL, "blake3:HASH")
	release.UOSC.ConfURL = fmt.Sprintf("https://github.com/tomasklaen/uosc/releases/download/%s/uosc.conf", uoscVersion)
	release.UOSC.ConfBLAKE3 = hashOrExit("uOSC Config", release.UOSC.ConfURL, "blake3:HASH")
	release.UOSC.AppVersion = uoscVersion

	// ModernZ
	print("Computing hashes for ModernZ...\n")
	release.ModernZ.ScriptURL = generateModernZScriptURL(modernZVersion)
	release.ModernZ.ScriptBLAKE3 = hashOrExit("ModernZ Script", release.ModernZ.ScriptURL, "blake3:HASH")
	release.ModernZ.FontURL = generateModernZFontURL(modernZVersion)
	release.ModernZ.FontBLAKE3 = hashOrExit("ModernZ Font", release.ModernZ.FontURL, "blake3:HASH")
	release.ModernZ.FluentFontURL = generateModernZFluentFontURL(modernZVersion)
	release.ModernZ.FluentFontBLAKE3 = hashOrExit("ModernZ Fluent Font", release.ModernZ.FluentFontURL, "blake3:HASH")
	release.ModernZ.ConfURL = fmt.Sprintf("https://github.com/Samillion/ModernZ/releases/download/v%s/modernz.conf", modernZVersion)
	release.ModernZ.ConfBLAKE3 = hashOrExit("ModernZ Config", release.ModernZ.ConfURL, "blake3:HASH")
	release.ModernZ.AppVersion = modernZVersion

	// MPC-QT
	print("Computing hashes for MPC-QT...\n")
	release.MPCQT.X8664.URL = generateMPCQTURL(mpcqtVersion)
	release.MPCQT.X8664.BLAKE3 = hashOrExit("MPC-QT x86-64", release.MPCQT.X8664.URL, "blake3:HASH")
	release.MPCQT.AppVersion = mpcqtVersion

	// IINA
	print("Computing hashes for IINA...\n")
	release.IINA.ARM.URL = generateIINAURL(iinaVersion)
	release.IINA.ARM.BLAKE3 = hashOrExit("IINA ARM", release.IINA.ARM.URL, "blake3:HASH")
	release.IINA.Intel.URL = generateIINAURL(iinaVersion)
	release.IINA.Intel.BLAKE3 = hashOrExit("IINA Intel", release.IINA.Intel.URL, "blake3:HASH")
	release.IINA.AppVersion = iinaVersion

	// Manager - generate all platforms (using GitLab Generic Package Registry for direct downloads)
	// URL format: https://gitgud.io/api/v4/projects/mike%2Fmpv-manager/packages/generic/mpv-manager/v1.0.0/mpv-manager-linux-amd64
	print("Computing hashes for MPV Manager...\n")
	release.Manager.LinuxAMD64.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-linux-amd64", managerVer)
	release.Manager.LinuxAMD64.BLAKE3 = hashOrExit("Manager Linux AMD64", release.Manager.LinuxAMD64.URL, "blake3:HASH")

	release.Manager.LinuxARM64.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-linux-arm64", managerVer)
	release.Manager.LinuxARM64.BLAKE3 = hashOrExit("Manager Linux ARM64", release.Manager.LinuxARM64.URL, "blake3:HASH")

	release.Manager.WinX86_64.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-win-x86_64.exe", managerVer)
	release.Manager.WinX86_64.BLAKE3 = hashOrExit("Manager Windows x86-64", release.Manager.WinX86_64.URL, "blake3:HASH")

	release.Manager.WinARM64.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-win-arm64.exe", managerVer)
	release.Manager.WinARM64.BLAKE3 = hashOrExit("Manager Windows ARM64", release.Manager.WinARM64.URL, "blake3:HASH")

	release.Manager.MacosIntel.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-macos-intel", managerVer)
	release.Manager.MacosIntel.BLAKE3 = hashOrExit("Manager macOS Intel", release.Manager.MacosIntel.URL, "blake3:HASH")

	release.Manager.MacosARM.URL = fmt.Sprintf("https://gitgud.io/api/v4/projects/mike%%2Fmpv-manager/packages/generic/mpv-manager/v%s/mpv-manager-macos-arm", managerVer)
	release.Manager.MacosARM.BLAKE3 = hashOrExit("Manager macOS ARM", release.Manager.MacosARM.URL, "blake3:HASH")

	data, err := json.MarshalIndent(release, "", "\t")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error generating JSON: %v\n", err)
		os.Exit(1)
	}

	if err := os.WriteFile(*outputFile, []byte(data), 0644); err != nil {
		fmt.Fprintf(os.Stderr, "Error writing to %s: %v\n", *outputFile, err)
		os.Exit(1)
	}

	print("\n✓ Generated %s with %d bytes\n", *outputFile, len(data))

	cleanupUnusedFiles(allUsedFiles)
}

func computeBLAKE3(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	hash := blake3.New(32, nil)
	if _, err := io.Copy(hash, file); err != nil {
		return "", err
	}

	return "blake3:" + hex.EncodeToString(hash.Sum(nil)), nil
}

func generateWindowsURL(timestamp, arch, hash string) string {
	return fmt.Sprintf("https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/%s/mpv-%s-%s-git-%s.7z", timestamp, arch, timestamp, hash)
}

func generateFFmpegURL(timestamp, arch, hash string) string {
	return fmt.Sprintf("https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/%s/ffmpeg-%s-git-%s.7z", timestamp, arch, hash)
}

func generateMacOSURL(version, osType, arch string) string {
	return fmt.Sprintf("https://github.com/mpv-player/mpv/releases/download/v%s/mpv-v%s-%s-%s.zip", version, version, osType, arch)
}

func generateIINAURL(version string) string {
	return fmt.Sprintf("https://github.com/iina/iina/releases/download/v%s/IINA.v%s.dmg", version, version)
}

func generateUOSCURL(version string) string {
	return fmt.Sprintf("https://github.com/tomasklaen/uosc/releases/download/%s/uosc.zip", version)
}

func generateModernZScriptURL(version string) string {
	return fmt.Sprintf("https://github.com/Samillion/ModernZ/releases/download/v%s/modernz.lua", version)
}

func generateModernZFontURL(version string) string {
	return fmt.Sprintf("https://github.com/Samillion/ModernZ/releases/download/v%s/material-design-icons.ttf", version)
}

func generateModernZFluentFontURL(version string) string {
	return fmt.Sprintf("https://github.com/Samillion/ModernZ/releases/download/v%s/fluent-system-icons.ttf", version)
}

func generateMPCQTURL(version string) string {
	return fmt.Sprintf("https://github.com/mpc-qt/mpc-qt/releases/download/v%s/mpc-qt-win-x64-%s-installer.exe", version, version)
}

func generateInstallerURL(version, osType, arch, ext string) string {
	return fmt.Sprintf("https://gitgud.io/mike/mpv-manager/-/releases/%s/downloads/install-mpv-%s-%s%s", version, osType, arch, ext)
}

func promptString(reader *bufio.Reader, prompt, defaultVal string) string {
	fmt.Printf("%s [%s]: ", prompt, defaultVal)
	input, err := reader.ReadString('\n')
	if err != nil {
		return defaultVal
	}
	input = strings.TrimSpace(input)
	if input == "" {
		return defaultVal
	}
	return input
}
