// Package appupdate decides whether a TV should be told to update, and how firmly.
//
// The gateway is the authority: the client reports the version it is running and renders
// whatever verdict comes back. That keeps "everyone must move to 0.1.54 now" a one-field
// change on the admin page rather than a release.
package appupdate

import (
	"strconv"
	"strings"
	"time"
)

// Status is how hard the TV should push the update.
const (
	// StatusNone — nothing to say; the client is current enough.
	StatusNone = "none"
	// StatusOptional — a newer build exists and the viewer may dismiss the prompt.
	StatusOptional = "optional"
	// StatusMandatory — the client is below the minimum supported version and must
	// update before it can be used.
	StatusMandatory = "mandatory"
)

// Policy is the operator-controlled setting, stored in app_settings.
type Policy struct {
	// Enabled turns the whole mechanism off without losing the values.
	Enabled bool `json:"enabled"`
	// LatestVersion is what clients below it are *offered*.
	LatestVersion string `json:"latestVersion"`
	// MinimumVersion is what clients below it are *forced* to. Leave blank (or equal to
	// an old release) to keep updates optional; set it to LatestVersion to make the
	// current release mandatory for everyone.
	MinimumVersion string `json:"minimumVersion"`
	// DownloadURL points at the APK — normally the same file the landing page serves.
	DownloadURL string `json:"downloadUrl"`
	// SHA256 and SizeBytes let the TV reject a truncated, stale or substituted download
	// before handing it to Android's package installer. Blank/zero remain valid for
	// older policies entered manually through the admin page.
	SHA256    string    `json:"sha256,omitempty"`
	SizeBytes int64     `json:"sizeBytes,omitempty"`
	Notes     string    `json:"notes"`
	UpdatedAt time.Time `json:"updatedAt"`
}

// Decision is what the client receives.
type Decision struct {
	Status      string `json:"status"`
	Version     string `json:"version"`
	Notes       string `json:"notes"`
	DownloadURL string `json:"downloadUrl"`
	SHA256      string `json:"sha256,omitempty"`
	SizeBytes   int64  `json:"sizeBytes,omitempty"`
}

// Decide compares the version a client reported against the policy.
//
// An unreadable or absent client version yields StatusNone on purpose: a client that
// cannot say what it is would otherwise be locked out by a blocking prompt it may not
// even know how to satisfy. Those builds still have the manual check in Settings.
func Decide(policy Policy, clientVersion string) Decision {
	none := Decision{Status: StatusNone}

	if !policy.Enabled || strings.TrimSpace(policy.LatestVersion) == "" {
		return none
	}
	client := parseVersion(clientVersion)
	if len(client) == 0 {
		return none
	}

	offer := Decision{
		Status:      StatusOptional,
		Version:     normalize(policy.LatestVersion),
		Notes:       strings.TrimSpace(policy.Notes),
		DownloadURL: strings.TrimSpace(policy.DownloadURL),
		SHA256:      strings.ToLower(strings.TrimSpace(policy.SHA256)),
		SizeBytes:   policy.SizeBytes,
	}

	if minimum := parseVersion(policy.MinimumVersion); len(minimum) > 0 && compare(client, minimum) < 0 {
		offer.Status = StatusMandatory
		return offer
	}
	if compare(client, parseVersion(policy.LatestVersion)) < 0 {
		return offer
	}
	return none
}

// CompareVersions returns -1, 0, or 1 and is shared by policy decisions and the release
// publisher's downgrade guard.
func CompareVersions(a, b string) int { return compare(parseVersion(a), parseVersion(b)) }

// compare returns -1, 0 or 1. Missing components count as zero, so 0.1 == 0.1.0.
func compare(a, b []int) int {
	for i := 0; i < len(a) || i < len(b); i++ {
		av, bv := 0, 0
		if i < len(a) {
			av = a[i]
		}
		if i < len(b) {
			bv = b[i]
		}
		if av != bv {
			if av < bv {
				return -1
			}
			return 1
		}
	}
	return 0
}

// parseVersion mirrors the client's own parsing, so both sides agree on ordering.
func parseVersion(v string) []int {
	parts := strings.FieldsFunc(normalize(v), func(r rune) bool {
		return r == '.' || r == '-' || r == '+' || r == ' '
	})
	out := make([]int, 0, len(parts))
	for _, part := range parts {
		n, err := strconv.Atoi(part)
		if err != nil {
			// Stop at the first non-numeric component ("0.1.54-beta" -> 0.1.54).
			break
		}
		out = append(out, n)
	}
	return out
}

func normalize(v string) string { return strings.TrimLeft(strings.TrimSpace(v), "vV") }
