// Package installerconf reads per-package releases.conf files.
//
// The format is simple key=value, one per line. Blank lines and lines
// starting with # are ignored. Keys and values are trimmed of whitespace.
// Multi-value keys are whitespace-delimited.
//
// The source type is inferred from the primary key:
//
// GitHub binary releases:
//
//	github_releases = sharkdp/bat
//	github_releases = https://github.com/sharkdp/bat
//
// GitHub source archives (for source-installable packages):
//
//	github_sources = BeyondCodeBootcamp/aliasman
//	git_url = https://github.com/BeyondCodeBootcamp/aliasman.git
//
// Gitea binary releases (self-hosted, requires full URL or base_url):
//
//	gitea_releases = https://git.rootprojects.org/root/pathman
//
// GitLab binary releases (defaults to gitlab.com):
//
//	gitlab_releases = owner/repo
//	gitlab_releases = https://gitlab.example.com/owner/repo
//
// Git tag enumeration (vim plugins, etc.):
//
//	git_url = https://github.com/tpope/vim-commentary.git
//
// HashiCorp releases:
//
//	hashicorp_product = terraform
//
// Other sources (one-off scrapers):
//
//	source = nodedist
//	url = https://nodejs.org/download/release
//
// Complex packages that need custom logic beyond what the classifier
// auto-detects (e.g. ollama's universal binaries, ffmpeg's non-standard
// naming) should put that logic in Go code, not in the config.
// The variants key documents known build variants for human readers;
// actual variant detection logic lives in Go.
package installerconf

import (
	"bufio"
	"fmt"
	"net/url"
	"os"
	"strings"
)

// Conf holds the parsed per-package release configuration.
type Conf struct {
	// Source is the fetch source type: "github", "githubsource",
	// "gitea", "giteasource", "gitlab", "gitlabsource",
	// "gittag", "nodedist", etc.
	Source string

	// Owner is the repository owner (org or user).
	Owner string

	// Repo is the repository name.
	Repo string

	// BaseURL is a custom base URL for non-GitHub sources
	// (e.g. a Gitea instance or nodedist index URL).
	BaseURL string

	// GitURL is the git clone URL for source-installable packages.
	// Present alongside github_sources/gitea_sources to provide a
	// git clone fallback in addition to release tarballs.
	GitURL string

	// TagPrefix filters releases in monorepos. Only tags starting with
	// this prefix are included, and the prefix is stripped from the
	// version string. Example: "tools/monorel/"
	TagPrefix string

	// VersionPrefixes are stripped from version/tag strings.
	// Whitespace-delimited. Each release tag is checked against these
	// in order; the first match is stripped. Projects may change tag
	// conventions across versions (e.g. "jq-1.7.1" older, "1.8.0" later).
	VersionPrefixes []string

	// Exclude lists filename substrings to filter out.
	// Whitespace-delimited. Assets whose name contains any of these
	// are skipped entirely (not stored).
	Exclude []string

	// AssetFilter is a substring that asset filenames must contain.
	// Used when multiple packages share a GitHub release (e.g.
	// kubectx/kubens) to select only the relevant assets.
	AssetFilter string

	// Variants documents known build variant names for this package.
	// Whitespace-delimited. This is a human-readable cue — actual
	// variant detection logic lives in Go code per-package.
	Variants []string

	// OS restricts all assets to this OS value when set.
	// Use "posix_2017" for POSIX-only shell packages that don't
	// support Windows.
	OS string

	// AliasOf names another package that this one mirrors.
	// When set, the package has no releases of its own — it shares
	// the cache output of the named target (e.g. dashd → dashcore).
	AliasOf string

	// Extra holds any unrecognized keys for forward compatibility.
	Extra map[string]string
}

// parseRepoRef parses a value that is either "owner/repo" or a full URL
// like "https://github.com/owner/repo". Returns baseURL, owner, repo.
// For short form, baseURL is empty (caller uses the default for the forge).
// For full URL form, baseURL is the scheme+host (e.g. "https://github.com").
func parseRepoRef(val, defaultBase string) (baseURL, owner, repo string) {
	if strings.Contains(val, "://") {
		u, err := url.Parse(val)
		if err == nil {
			baseURL = u.Scheme + "://" + u.Host
			path := strings.Trim(u.Path, "/")
			owner, repo, _ = strings.Cut(path, "/")
			return baseURL, owner, repo
		}
	}
	// Short form: "owner/repo"
	owner, repo, _ = strings.Cut(val, "/")
	return defaultBase, owner, repo
}

// Read parses a releases.conf file.
func Read(path string) (*Conf, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("installerconf: %w", err)
	}
	defer f.Close()

	raw := make(map[string]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || line[0] == '#' {
			continue
		}
		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		raw[strings.TrimSpace(key)] = strings.TrimSpace(val)
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("installerconf: read %s: %w", path, err)
	}

	c := &Conf{}

	// Infer source from primary key, falling back to explicit "source".
	// When both github_releases and source are set, parse the repo ref
	// from github_releases but use the explicit source for classification.
	switch {
	// GitHub binary releases.
	case raw["github_releases"] != "":
		c.Source = "github"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["github_releases"], "https://github.com")

	// GitHub source tarballs.
	case raw["github_sources"] != "":
		c.Source = "githubsource"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["github_sources"], "https://github.com")

	// Gitea binary releases (self-hosted only — requires full URL or base_url).
	case raw["gitea_releases"] != "":
		c.Source = "gitea"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["gitea_releases"], raw["base_url"])

	// Gitea source tarballs (self-hosted only).
	case raw["gitea_sources"] != "":
		c.Source = "giteasource"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["gitea_sources"], raw["base_url"])

	// GitLab binary releases (defaults to gitlab.com).
	case raw["gitlab_releases"] != "":
		c.Source = "gitlab"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["gitlab_releases"], "https://gitlab.com")

	// GitLab source tarballs (defaults to gitlab.com).
	case raw["gitlab_sources"] != "":
		c.Source = "gitlabsource"
		c.BaseURL, c.Owner, c.Repo = parseRepoRef(raw["gitlab_sources"], "https://gitlab.com")

	// Explicit source type (servicemandist, nodedist, zigdist, etc.).
	// Must come before git_url so that "source = X" + "git_url = ..."
	// uses X as the primary source, not gittag.
	case raw["source"] != "":
		c.Source = raw["source"]
		c.BaseURL = raw["url"]

	// Git tag enumeration (only when no explicit source is set).
	case raw["git_url"] != "":
		c.Source = "gittag"
		c.BaseURL = raw["git_url"]

	// HashiCorp.
	case raw["hashicorp_product"] != "":
		c.Source = "hashicorp"
		c.Repo = raw["hashicorp_product"]

	default:
	}

	// Explicit "source" overrides the inferred source when both are present.
	// This lets packages like ffmpeg use github_releases for fetching but
	// a custom classifier for classification.
	if raw["source"] != "" && c.Source != "" {
		c.Source = raw["source"]
	}

	// git_url can appear alongside any source type (e.g. github_sources)
	// to provide a git clone fallback. When it's the only key, it's the
	// primary source (gittag).
	c.GitURL = raw["git_url"]

	c.TagPrefix = raw["tag_prefix"]

	if v := raw["version_prefixes"]; v != "" {
		c.VersionPrefixes = strings.Fields(v)
	} else if v := raw["version_prefix"]; v != "" {
		c.VersionPrefixes = strings.Fields(v)
	}

	// Accept both "exclude" and "asset_exclude" (back-compat).
	if v := raw["exclude"]; v != "" {
		c.Exclude = strings.Fields(v)
	} else if v := raw["asset_exclude"]; v != "" {
		c.Exclude = strings.Fields(v)
	}

	c.AssetFilter = raw["asset_filter"]
	c.OS = raw["os"]
	c.AliasOf = raw["alias_of"]

	if v := raw["variants"]; v != "" {
		c.Variants = strings.Fields(v)
	}

	// Collect unrecognized keys.
	known := map[string]bool{
		"source":            true,
		"github_releases":   true,
		"github_sources":    true,
		"gitea_releases":    true,
		"gitea_sources":     true,
		"gitlab_releases":   true,
		"gitlab_sources":    true,
		"git_url":           true,
		"hashicorp_product": true,
		"base_url":          true,
		"url":               true,
		"tag_prefix":        true,
		"version_prefix":    true,
		"version_prefixes":  true,
		"exclude":           true,
		"asset_exclude":     true,
		"asset_filter":      true,
		"os":                true,
		"variants":          true,
		"alias_of":          true,
	}
	for k, v := range raw {
		if !known[k] {
			if c.Extra == nil {
				c.Extra = make(map[string]string)
			}
			c.Extra[k] = v
		}
	}

	return c, nil
}
