package main

import (
	"crypto/sha1"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"runtime"
	"strings"
	"time"
)

// PackageAPPINDEX updates the local APPINDEX file by removing any old
// entry matching (P, R, A, o, p) and appending a new entry.
//
// It includes:
//   - Compressed/uncompressed file measurement (size + checksums).
//   - A user-defined remotePath for the final download URL.
//   - Additional flags for icon, screenshots, tags, and notes.
func PackageAPPINDEX(
	name string,         // P: Package name, e.g., "spitfire-browser"
	release string,      // R: Release type, e.g., "nightly"
	version string,      // V: Version, e.g., "2025.02.07"
	arch string,         // A: Architecture, e.g., "amd64"
	description string,  // X: Short description, e.g., "Short summary"
	url string,          // U: Project URL, e.g., "https://spitfirebrowser.xyz/"
	license string,      // L: License, e.g., "AGPL-3.0"
	origin string,       // o: Origin, e.g., "browser"
	maintainer string,   // m: Maintainer, e.g., "Internet Addict"
	dependencies string, // D: Dependencies, e.g., "default-theme, browser"
	platform string,     // p: Platform, e.g., "linux"
	remotePath string,   // d: Remote file path, e.g., "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz"
	icon string,         // I: Icon URL, e.g., "https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg"
	screenshots string,  // S: Screenshots URL, e.g., "https://spitfirebrowser.xyz/static/images/screenshots/1.png"
	tags string,         // T: Tags, e.g., "browser,experimental,testing"
	notes string,        // r: Notes, e.g., "Automated build of Spitfire"
	compressedFile string,   // Path to the compressed file, used for size & checksum
	uncompressedFile string, // Path to the uncompressed file, used for size & checksum
) error {
	//----------------------------------------------------------------------
	// 1) Measure compressed file
	//----------------------------------------------------------------------
	compSizeBytes := measureFileSize(compressedFile)
	compSize := fmt.Sprintf("%d", compSizeBytes)
	compChecksum := calcFileChecksum(compressedFile)

	//----------------------------------------------------------------------
	// 2) Measure uncompressed file
	//----------------------------------------------------------------------
	uncompSizeBytes := measureFileSize(uncompressedFile)
	uncompSize := fmt.Sprintf("%d", uncompSizeBytes)
	uncompChecksum := calcFileChecksum(uncompressedFile)

	//----------------------------------------------------------------------
	// 3) Remove old entry from APPINDEX (based on P,R,A,o,p).
	//----------------------------------------------------------------------
	removeExistingEntry(name, release, arch, origin, platform)

	//----------------------------------------------------------------------
	// 4) Check for existence of APPINDEX
	//----------------------------------------------------------------------
	appIndexPath := "./APPINDEX"
	if _, err := os.Stat(appIndexPath); os.IsNotExist(err) {
		fmt.Println("APPINDEX does not exist. Creating a new one...")
	}

	//----------------------------------------------------------------------
	// 5) Open (or create) the APPINDEX file for appending.
	//----------------------------------------------------------------------
	file, err := os.OpenFile(appIndexPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to open APPINDEX file: %v", err)
	}
	defer file.Close()

	//----------------------------------------------------------------------
	// 6) Build the final download URL from `remotePath`
	//----------------------------------------------------------------------
	downloadURL := fmt.Sprintf("https://downloads.sourceforge.net/project/spitfire-browser/%s", remotePath)

	//----------------------------------------------------------------------
	// 7) Current Unix timestamp for t:
	//----------------------------------------------------------------------
	timestamp := time.Now().Unix()

	//----------------------------------------------------------------------
	// 8) Format final entry
	//
	// We'll store:
	//    C:%s => compressed checksum
	//    S:%s => compressed size
	//    I:%s => uncompressed size
	//    c:%s => uncompressed checksum
	//    X:%s => short text/description (custom field, formerly "T:")
	//----------------------------------------------------------------------
	entry := fmt.Sprintf(`
C:%s
P:%s
R:%s
V:%s
A:%s
S:%s
I:%s
X:%s
U:%s
L:%s
o:%s
m:%s
t:%d
D:%s
p:%s
q:
d:%s
I:%s
S:%s
T:%s
r:%s
c:%s
`,
		compChecksum,  // C: compressed checksum
		name,          // P:
		release,       // R:
		version,       // V:
		arch,          // A:
		compSize,      // S: compressed size
		uncompSize,    // I: uncompressed size
		description,   // X: short text description
		url,           // U:
		license,       // L:
		origin,        // o:
		maintainer,    // m:
		timestamp,     // t:
		dependencies,  // D:
		platform,      // p:
		downloadURL,   // d:
		icon,          // I: icon URL
		screenshots,   // S: screenshots URL
		tags,          // T: tags
		notes,         // r: notes
		uncompChecksum, // c: uncompressed checksum
	)

	// Trim leading newline for neatness
	entry = strings.TrimPrefix(entry, "\n")

	//----------------------------------------------------------------------
	// 9) Write the new entry to APPINDEX
	//----------------------------------------------------------------------
	if _, err := file.WriteString(entry + "\n"); err != nil {
		return fmt.Errorf("failed to write to APPINDEX: %v", err)
	}

	fmt.Println("APPINDEX has been updated successfully.")
	return nil
}

// calcFileChecksum calculates SHA-1 of an entire file. Returns "" if file not found or error.
func calcFileChecksum(path string) string {
	f, err := os.Open(path)
	if err != nil {
		return ""
	}
	defer f.Close()

	h := sha1.New()
	if _, err := io.Copy(h, f); err != nil {
		return ""
	}
	return fmt.Sprintf("%x", h.Sum(nil))
}

// measureFileSize returns file size in bytes. Returns 0 if file not found or error.
func measureFileSize(path string) int64 {
	info, err := os.Stat(path)
	if err != nil {
		return 0
	}
	return info.Size()
}

// removeExistingEntry removes an existing entry from APPINDEX if it matches
//   P (name) = "P:"
//   R (release) = "R:"
//   A (arch) = "A:"
//   o (origin) = "o:"
//   p (platform) = "p:"
func removeExistingEntry(name, release, arch, origin, platform string) {
	content, err := os.ReadFile("./APPINDEX")
	if err != nil {
		if os.IsNotExist(err) {
			// No file to remove from
			return
		}
		log.Fatalf("Failed to read APPINDEX: %v", err)
	}

	lines := strings.Split(string(content), "\n")
	var newLines []string
	var currentEntry []string
	inEntry := false

	// We'll track P:, R:, A:, o:, p: fields for each entry
	var pVal, rVal, aVal, oVal, plVal string

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)

		// Each entry *starts* with "C:..."
		if strings.HasPrefix(trimmed, "C:") {
			// We've hit the start of a new entry. Decide whether
			// we keep the old one we were collecting.
			if inEntry && len(currentEntry) > 0 {
				if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) {
					newLines = append(newLines, currentEntry...)
					newLines = append(newLines, "") // optional blank line
				}
			}
			// Start a new entry
			currentEntry = []string{trimmed}
			inEntry = true
			pVal, rVal, aVal, oVal, plVal = "", "", "", "", ""
			continue
		}

		if inEntry {
			currentEntry = append(currentEntry, trimmed)
			// Check if it's one of the fields we track
			switch {
			case strings.HasPrefix(trimmed, "P:"):
				pVal = strings.TrimPrefix(trimmed, "P:")
			case strings.HasPrefix(trimmed, "R:"):
				rVal = strings.TrimPrefix(trimmed, "R:")
			case strings.HasPrefix(trimmed, "A:"):
				aVal = strings.TrimPrefix(trimmed, "A:")
			case strings.HasPrefix(trimmed, "o:"):
				oVal = strings.TrimPrefix(trimmed, "o:")
			case strings.HasPrefix(trimmed, "p:"):
				plVal = strings.TrimPrefix(trimmed, "p:")
			}
		} else if trimmed != "" {
			// Lines outside an entry
			newLines = append(newLines, trimmed)
		}
	}

	// After the loop, handle any last entry we collected
	if inEntry && len(currentEntry) > 0 {
		if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) {
			newLines = append(newLines, currentEntry...)
		}
	}

	finalContent := strings.Join(newLines, "\n")
	if !strings.HasSuffix(finalContent, "\n") {
		finalContent += "\n"
	}

	if err := os.WriteFile("./APPINDEX", []byte(finalContent), 0644); err != nil {
		log.Fatalf("Failed to update APPINDEX: %v", err)
	}
}

func main() {
	//----------------------------------------------------------------------
	// Default flags for basic fields
	//----------------------------------------------------------------------
	defaultRelease := "nightly"
	defaultVersion := time.Now().Format("2006.01.02") // e.g. "2025.02.07"
	defaultArch := runtime.GOARCH                     // e.g. "amd64"

	//----------------------------------------------------------------------
	// Primary fields: P, R, V, A, X (desc), U, L, o, m, D, p
	//----------------------------------------------------------------------
	name := flag.String("name", "spitfire-browser", "P: Package name")
	release := flag.String("release", defaultRelease, "R: Release (nightly, stable, etc.)")
	version := flag.String("version", defaultVersion, "V: Version (date or semantic)")
	arch := flag.String("arch", defaultArch, "A: Architecture (default=GOARCH)")
	description := flag.String("description", "Spitfire build", "X: Short text description")
	url := flag.String("url", "https://spitfirebrowser.xyz/", "U: Project URL")
	license := flag.String("license", "AGPL-3.0", "L: License")
	origin := flag.String("origin", "browser", "o: Origin name")
	maintainer := flag.String("maintainer", "Internet Addict", "m: Maintainer name")
	dependencies := flag.String("dependencies", "", "D: Dependencies (comma-separated)")
	platform := flag.String("platform", "windows", "p: Platform (linux, windows, etc.)")

	//----------------------------------------------------------------------
	// Additional flags for icon, screenshots, tags, notes
	//----------------------------------------------------------------------
	icon := flag.String("icon",
		"https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg",
		"I: Icon URL")
	screenshots := flag.String("screenshots",
		"https://spitfirebrowser.xyz/static/images/screenshots/1.png",
		"S: Screenshot(s) URL(s)")
	tags := flag.String("tags",
		"browser,experimental,testing",
		"T: Comma-separated tags")
	notes := flag.String("notes",
		"Automated build of Spitfire",
		"r: Additional notes")

	//----------------------------------------------------------------------
	// remotePath is the path under:
	//   https://downloads.sourceforge.net/project/spitfire-browser/<remotePath>
	// Example: "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz"
	//----------------------------------------------------------------------
	remotePath := flag.String("remotePath", "",
		"Path under https://downloads.sourceforge.net/project/spitfire-browser/...")

	//----------------------------------------------------------------------
	// Compressed & uncompressed artifact paths
	//----------------------------------------------------------------------
	compressedFile := flag.String("compressedFile",
		"browser-amd64-nightly-windows.tar.gz",
		"Local path to compressed artifact (for size/checksum)")

	uncompressedFile := flag.String("uncompressedFile",
		"browser-amd64-nightly-windows",
		"Local path to uncompressed artifact (for size/checksum)")

	flag.Parse()

	//----------------------------------------------------------------------
	// If user didn't specify remotePath, we build a naive default:
	// "browser/<arch>/<release>/<version>/<origin>-<arch>-<release>-<platform>.tar.gz"
	//----------------------------------------------------------------------
	if *remotePath == "" {
		*remotePath = fmt.Sprintf(
			"browser/%s/%s/%s/%s-%s-%s-%s.tar.gz",
			*arch, *release, *version, *origin, *arch, *release, *platform,
		)
	}

	//----------------------------------------------------------------------
	// Execute the logic
	//----------------------------------------------------------------------
	err := PackageAPPINDEX(
		*name,
		*release,
		*version,
		*arch,
		*description,
		*url,
		*license,
		*origin,
		*maintainer,
		*dependencies,
		*platform,
		*remotePath,
		*icon,
		*screenshots,
		*tags,
		*notes,
		*compressedFile,
		*uncompressedFile,
	)
	if err != nil {
		log.Fatalf("APPINDEX update failed: %v", err)
	}
}