// SPDX-License-Identifier: GPL-3.0-or-later

// Package updater handles automatic updates for the Private Archive client suite.
//
// It fetches a signed manifest from the public update host, verifies the
// ed25519 signature, downloads new component binaries through the existing
// Tor dialer, and atomically replaces the running binary on disk before
// asking the service manager to restart. The same manifest also drives
// CLI and desktop updates: the daemon proxies download requests so all
// update traffic uses Tor.
package updater

import (
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"time"
)

// Component identifies one of the installable client artifacts.
type Component string

const (
	ComponentBackend Component = "backend"
	ComponentCLI     Component = "cli"
	ComponentDesktop Component = "desktop"
)

// Artifact describes a single downloadable, signed binary or bundle.
type Artifact struct {
	URL    string `json:"url"`
	SHA256 string `json:"sha256"`
	Size   int64  `json:"size"`
	// MinisignSignature is the optional minisign signature, used by the
	// Tauri updater plugin for desktop bundles. May be empty for non-desktop
	// components.
	MinisignSignature string `json:"minisign_signature,omitempty"`
}

// PlatformArtifacts groups the artifacts available for a single platform.
type PlatformArtifacts struct {
	Backend *Artifact `json:"backend,omitempty"`
	CLI     *Artifact `json:"cli,omitempty"`
	Desktop *Artifact `json:"desktop,omitempty"`
}

// Manifest is the signed update manifest published at the update endpoint.
//
// The Signature field covers the canonical JSON representation of every other
// field. Clients verify the signature against an embedded ed25519 public key
// before trusting any value in the manifest.
type Manifest struct {
	Version         string                       `json:"version"`
	Channel         string                       `json:"channel"`
	Released        time.Time                    `json:"released"`
	MinimumVersion  string                       `json:"minimum_version,omitempty"`
	ReleaseNotesURL string                       `json:"release_notes_url,omitempty"`
	Platforms       map[string]PlatformArtifacts `json:"platforms"`
	// Signature is base64-encoded ed25519 over the canonical JSON of the
	// manifest with this field omitted.
	Signature string `json:"signature"`
}

// Get returns the platform-specific artifacts for a goos/goarch pair, e.g.
// "darwin", "arm64". The platform key is "<os>-<arch>".
func (m *Manifest) Get(goos, goarch string) (PlatformArtifacts, bool) {
	key := goos + "-" + goarch
	p, ok := m.Platforms[key]
	return p, ok
}

// Canonical returns the bytes that the Signature field signs over.
//
// We re-marshal with sorted keys and the Signature field cleared so that
// signing and verification produce identical input even after JSON
// round-trips reorder map keys.
func (m *Manifest) Canonical() ([]byte, error) {
	clone := *m
	clone.Signature = ""
	return canonicalJSON(&clone)
}

// Verify checks the manifest signature against the supplied public key.
func (m *Manifest) Verify(pub ed25519.PublicKey) error {
	if len(pub) != ed25519.PublicKeySize {
		return fmt.Errorf("invalid public key length: %d", len(pub))
	}
	if m.Signature == "" {
		return errors.New("manifest has no signature")
	}
	sig, err := base64.StdEncoding.DecodeString(m.Signature)
	if err != nil {
		return fmt.Errorf("decoding signature: %w", err)
	}
	body, err := m.Canonical()
	if err != nil {
		return fmt.Errorf("canonicalising manifest: %w", err)
	}
	if !ed25519.Verify(pub, body, sig) {
		return errors.New("manifest signature verification failed")
	}
	return nil
}

// Sign produces a signed copy of the manifest using the provided private key.
// The original is not mutated.
func (m *Manifest) Sign(priv ed25519.PrivateKey) (*Manifest, error) {
	if len(priv) != ed25519.PrivateKeySize {
		return nil, fmt.Errorf("invalid private key length: %d", len(priv))
	}
	body, err := m.Canonical()
	if err != nil {
		return nil, err
	}
	sig := ed25519.Sign(priv, body)
	signed := *m
	signed.Signature = base64.StdEncoding.EncodeToString(sig)
	return &signed, nil
}

// VerifyArtifact checks that the on-disk bytes of an artifact match the
// SHA256 declared in the manifest. The manifest signature must have been
// verified before calling this.
func VerifyArtifact(a *Artifact, data []byte) error {
	if a == nil {
		return errors.New("nil artifact")
	}
	sum := sha256.Sum256(data)
	got := hex.EncodeToString(sum[:])
	if got != a.SHA256 {
		return fmt.Errorf("sha256 mismatch: manifest=%s actual=%s", a.SHA256, got)
	}
	if a.Size > 0 && int64(len(data)) != a.Size {
		return fmt.Errorf("size mismatch: manifest=%d actual=%d", a.Size, len(data))
	}
	return nil
}

// canonicalJSON marshals v with deterministic, sorted keys.
//
// We do this by round-tripping through generic interface{} and walking the
// tree to sort map keys before re-encoding. This is sufficient for our flat
// struct shape and avoids pulling in a third-party canonical-JSON library.
func canonicalJSON(v any) ([]byte, error) {
	raw, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	var generic any
	if err := json.Unmarshal(raw, &generic); err != nil {
		return nil, err
	}
	return marshalSorted(generic)
}

func marshalSorted(v any) ([]byte, error) {
	switch t := v.(type) {
	case map[string]any:
		keys := make([]string, 0, len(t))
		for k := range t {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		buf := []byte{'{'}
		for i, k := range keys {
			if i > 0 {
				buf = append(buf, ',')
			}
			kb, err := json.Marshal(k)
			if err != nil {
				return nil, err
			}
			buf = append(buf, kb...)
			buf = append(buf, ':')
			vb, err := marshalSorted(t[k])
			if err != nil {
				return nil, err
			}
			buf = append(buf, vb...)
		}
		buf = append(buf, '}')
		return buf, nil
	case []any:
		buf := []byte{'['}
		for i, item := range t {
			if i > 0 {
				buf = append(buf, ',')
			}
			b, err := marshalSorted(item)
			if err != nil {
				return nil, err
			}
			buf = append(buf, b...)
		}
		buf = append(buf, ']')
		return buf, nil
	default:
		return json.Marshal(t)
	}
}
