package updater

import (
	"context"
	"crypto/ecdsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"fmt"
	"net/http"
	"strings"
)

// MaxSignatureBytes caps the cosign signature download. A blob
// signature is ~70 bytes raw (an ECDSA P-256 ASN.1 signature) plus
// some base64 overhead and a trailing newline; 1 KiB is generous.
const MaxSignatureBytes int64 = 1024

// BangerReleasePublicKey is the cosign-generated public key used to
// verify SHA256SUMS for every banger release. SET ME BEFORE THE
// FIRST RELEASE. The placeholder below is intentionally invalid so
// `banger update` refuses every download until a real key lands.
//
// Production-cut workflow (for the maintainer cutting v0.1.0):
//
//  1. Generate the keypair (one-time, store the private key offline):
//     cosign generate-key-pair
//     Produces cosign.key (private) and cosign.pub (public). The
//     private key is password-protected; remember the password.
//
//  2. Replace the PEM block below with the contents of cosign.pub.
//     Commit. From this point on, every banger CLI baked from this
//     repo will only trust signatures made with cosign.key.
//
//  3. At release time, sign SHA256SUMS:
//     cosign sign-blob --key cosign.key --output-signature \
//     SHA256SUMS.sig SHA256SUMS
//     Publish SHA256SUMS.sig alongside SHA256SUMS in the bucket;
//     the manifest's `sha256sums_sig_url` field references it.
//
//  4. Rotating the key after publication means publishing a new
//     banger release that embeds the new key, then re-signing
//     every release artifact with the new key. v0.1.x is too
//     early to design a clean rotation story; defer.
//
// var (rather than const) only because tests need to swap it for an
// in-test-generated key; production sets it at compile time and
// never mutates it.
var BangerReleasePublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElWFSLKLosBrdjfuF8ZS6U01Ufky4
zNeVPCkA6HEJ/oe634fRqwFxkXKGWg03eGFSnlwRxnUxN2+duXQSsR0pzQ==
-----END PUBLIC KEY-----`

// ErrSignatureRequired is returned by VerifyManifestRelease when the
// embedded public key is the placeholder. Surfaces as a clear "the
// release maintainer hasn't published their cosign key yet, refusing
// to update" rather than a cryptic crypto error.
var ErrSignatureRequired = errors.New("banger release public key is the placeholder; the maintainer must replace it and re-cut a release before update can proceed")

// VerifyBlobSignature checks that sigBase64 is a valid cosign-blob
// signature over body, made with the private counterpart of
// BangerReleasePublicKey.
func VerifyBlobSignature(body, sigBase64 []byte) error {
	return VerifyBlobSignatureWithKey(body, sigBase64, BangerReleasePublicKey)
}

// VerifyBlobSignatureWithKey is VerifyBlobSignature against an
// explicit PEM-encoded public key. Used by the smoke suite (via
// `banger update --pubkey-file …`) so an end-to-end update test can
// trust a locally-generated keypair without rebuilding the binary.
//
// Refuses outright if pubKeyPEM is the build-time placeholder so an
// unset key can't slip through as "verification disabled".
//
// cosign's blob signature format is a base64-encoded ASN.1-DER ECDSA
// signature over SHA256(body) — that's what ecdsa.VerifyASN1 takes.
func VerifyBlobSignatureWithKey(body, sigBase64 []byte, pubKeyPEM string) error {
	if isPlaceholderKey(pubKeyPEM) {
		return ErrSignatureRequired
	}
	block, _ := pem.Decode([]byte(pubKeyPEM))
	if block == nil {
		return fmt.Errorf("decode banger release public key: no PEM block")
	}
	pubAny, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return fmt.Errorf("parse banger release public key: %w", err)
	}
	pub, ok := pubAny.(*ecdsa.PublicKey)
	if !ok {
		return fmt.Errorf("banger release public key is not ECDSA")
	}
	sigBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(sigBase64)))
	if err != nil {
		return fmt.Errorf("decode signature base64: %w", err)
	}
	digest := sha256.Sum256(body)
	if !ecdsa.VerifyASN1(pub, digest[:], sigBytes) {
		return fmt.Errorf("signature does not verify against banger release public key")
	}
	return nil
}

// FetchAndVerifySignature pulls the SHA256SUMS.sig URL from the
// release, downloads it (capped), and verifies it against sumsBody.
// Returns nil on a clean pass, or an error describing exactly why
// verification failed.
//
// If release.SHA256SumsSigURL is empty, treat that as "release was
// not signed" — refuse rather than silently proceeding. v0.1.0
// requires every release to be cosign-signed; an unsigned release
// is a manifest publishing bug we'd rather catch loudly.
func FetchAndVerifySignature(ctx context.Context, client *http.Client, release Release, sumsBody []byte) error {
	return FetchAndVerifySignatureWithKey(ctx, client, release, sumsBody, BangerReleasePublicKey)
}

// FetchAndVerifySignatureWithKey is FetchAndVerifySignature against
// an explicit PEM-encoded public key.
func FetchAndVerifySignatureWithKey(ctx context.Context, client *http.Client, release Release, sumsBody []byte, pubKeyPEM string) error {
	if strings.TrimSpace(release.SHA256SumsSigURL) == "" {
		return fmt.Errorf("release %s has no sha256sums_sig_url; refusing to install an unsigned release", release.Version)
	}
	if client == nil {
		client = http.DefaultClient
	}
	sig, err := fetchBounded(ctx, client, release.SHA256SumsSigURL, MaxSignatureBytes)
	if err != nil {
		return fmt.Errorf("fetch signature: %w", err)
	}
	if err := VerifyBlobSignatureWithKey(sumsBody, sig, pubKeyPEM); err != nil {
		return fmt.Errorf("verify SHA256SUMS signature: %w", err)
	}
	return nil
}

// isPlaceholderKey detects the build-time placeholder constant. A
// real cosign-generated PEM never contains the string "PLACEHOLDER";
// a real ECDSA P-256 key block decodes to ~91 bytes of content,
// nowhere near our padded constant.
func isPlaceholderKey(pem string) bool {
	return strings.Contains(pem, "PLACEHOLDER")
}
