package gatewayrelease

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strconv"
)

const (
	// Cosign v3.0.6 wraps a SLSA v1 predicate in the stable in-toto v0.1
	// statement envelope. The predicate version and statement version are
	// independent facts.
	inTotoStatementV01    = "https://in-toto.io/Statement/v0.1"
	slsaProvenanceV1      = "https://slsa.dev/provenance/v1"
	dssePayloadType       = "application/vnd.in-toto+json"
	candidateJob          = "candidate"
	candidateDockerfile   = "Dockerfile.gateway"
	candidatePlatform     = "linux/amd64"
	gitSourceURIPrefix    = "git+https://github.com/"
	githubRunURLPrefix    = "https://github.com/"
	maxProvenanceEvidence = 4 << 20
)

// ProvenancePredicate is the source-owned SLSA v1 claim produced by the
// isolated release control plane. The candidate supplies only the subject
// digest; all provenance fields are derived from GitHub-owned run context and
// the immutable owner policy.
type ProvenancePredicate struct {
	BuildDefinition provenanceBuildDefinition `json:"buildDefinition"`
	RunDetails      provenanceRunDetails      `json:"runDetails"`
}

type provenanceBuildDefinition struct {
	BuildType            string                       `json:"buildType"`
	ExternalParameters   provenanceExternalParameters `json:"externalParameters"`
	InternalParameters   provenanceInternalParameters `json:"internalParameters"`
	ResolvedDependencies []provenanceResource         `json:"resolvedDependencies"`
}

type provenanceExternalParameters struct {
	Source     provenanceSource `json:"source"`
	Workflow   string           `json:"workflow"`
	Job        string           `json:"job"`
	Dockerfile string           `json:"dockerfile"`
	Platform   string           `json:"platform"`
}

type provenanceSource struct {
	Repository string `json:"repository"`
	Ref        string `json:"ref"`
	Commit     string `json:"commit"`
}

type provenanceInternalParameters struct {
	RunID           string `json:"runId"`
	RunAttempt      string `json:"runAttempt"`
	ImageRepository string `json:"imageRepository"`
}

type provenanceResource struct {
	URI    string           `json:"uri"`
	Digest provenanceDigest `json:"digest"`
}

type provenanceDigest struct {
	GitCommit string `json:"gitCommit,omitempty"`
	SHA256    string `json:"sha256,omitempty"`
}

type provenanceRunDetails struct {
	Builder  provenanceBuilder  `json:"builder"`
	Metadata provenanceMetadata `json:"metadata"`
}

type provenanceBuilder struct {
	ID string `json:"id"`
}

type provenanceMetadata struct {
	InvocationID string `json:"invocationId"`
}

type provenanceStatement struct {
	Type          string              `json:"_type"`
	Subject       []provenanceSubject `json:"subject"`
	PredicateType string              `json:"predicateType"`
	Predicate     ProvenancePredicate `json:"predicate"`
}

type provenanceSubject struct {
	Name   string           `json:"name"`
	Digest provenanceDigest `json:"digest"`
}

type dsseEnvelope struct {
	PayloadType string          `json:"payloadType"`
	Payload     string          `json:"payload"`
	Signatures  []dsseSignature `json:"signatures"`
}

type dsseSignature struct {
	KeyID string `json:"keyid"`
	Sig   string `json:"sig"`
}

// BuildProvenancePredicate creates the only build-provenance grammar accepted
// for Gateway releases. It deliberately does not copy candidate-authored
// BuildKit predicates.
func BuildProvenancePredicate(policy Policy, sourceCommit, runID, runAttempt string) ([]byte, error) {
	if err := policy.validateForRelease(); err != nil {
		return nil, err
	}
	if !canonicalCommit(sourceCommit) {
		return nil, errors.New("source commit must be a full lowercase SHA")
	}
	if !positiveDecimal(runID) || !positiveDecimal(runAttempt) {
		return nil, errors.New("provenance run identity must use positive decimal values")
	}
	sourceURI := gitSourceURIPrefix + policy.OwnerRepository + "@" + policy.SourceRef
	predicate := ProvenancePredicate{
		BuildDefinition: provenanceBuildDefinition{
			BuildType: policy.ProvenanceBuildType,
			ExternalParameters: provenanceExternalParameters{
				Source: provenanceSource{
					Repository: policy.OwnerRepository,
					Ref:        policy.SourceRef,
					Commit:     sourceCommit,
				},
				Workflow:   policy.WorkflowRef,
				Job:        candidateJob,
				Dockerfile: candidateDockerfile,
				Platform:   candidatePlatform,
			},
			InternalParameters: provenanceInternalParameters{
				RunID:           runID,
				RunAttempt:      runAttempt,
				ImageRepository: policy.ImageRepository,
			},
			ResolvedDependencies: []provenanceResource{{
				URI:    sourceURI,
				Digest: provenanceDigest{GitCommit: sourceCommit},
			}},
		},
		RunDetails: provenanceRunDetails{
			Builder: provenanceBuilder{ID: policy.ProvenanceBuilderID},
			Metadata: provenanceMetadata{InvocationID: fmt.Sprintf(
				"%s%s/actions/runs/%s/attempts/%s",
				githubRunURLPrefix,
				policy.OwnerRepository,
				runID,
				runAttempt,
			)},
		},
	}
	return json.Marshal(predicate)
}

// VerifyProvenanceEvidence consumes only output that Cosign has already
// verified against the committed owner public key. It then validates the
// complete SLSA statement and binds it to the same GitHub run as the image
// signature's independently verified OIDC evidence.
func VerifyProvenanceEvidence(policy Policy, sourceCommit, imageDigest, runID, runAttempt string, evidence []byte) (string, error) {
	if !canonicalDigest(imageDigest) {
		return "", errors.New("image digest must be canonical sha256")
	}
	if len(evidence) == 0 || len(evidence) > maxProvenanceEvidence {
		return "", errors.New("provenance verification evidence has an invalid size")
	}
	expectedPredicate, err := BuildProvenancePredicate(policy, sourceCommit, runID, runAttempt)
	if err != nil {
		return "", err
	}
	var expected ProvenancePredicate
	if err := decodeStrictJSON(expectedPredicate, &expected); err != nil {
		return "", fmt.Errorf("decode generated provenance: %w", err)
	}
	expectedHex := imageDigest[len("sha256:"):]

	decoder := json.NewDecoder(bytes.NewReader(evidence))
	decoder.DisallowUnknownFields()
	count := 0
	var statementDigest string
	for {
		var envelope dsseEnvelope
		if err := decoder.Decode(&envelope); err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return "", fmt.Errorf("decode provenance verification evidence: %w", err)
		}
		count++
		if count > 64 {
			return "", errors.New("provenance verification evidence has too many attestations")
		}
		if envelope.PayloadType != dssePayloadType || envelope.Payload == "" || len(envelope.Signatures) == 0 {
			return "", errors.New("provenance verification evidence is not a signed in-toto envelope")
		}
		for _, signature := range envelope.Signatures {
			signatureBytes, err := base64.StdEncoding.DecodeString(signature.Sig)
			if err != nil || len(signatureBytes) == 0 {
				return "", errors.New("provenance verification evidence contains an empty signature")
			}
		}
		payload, err := base64.StdEncoding.DecodeString(envelope.Payload)
		if err != nil {
			return "", errors.New("provenance DSSE payload is not canonical base64")
		}
		var statement provenanceStatement
		if err := decodeStrictJSON(payload, &statement); err != nil {
			return "", fmt.Errorf("decode signed provenance statement: %w", err)
		}
		if statement.Type != inTotoStatementV01 || statement.PredicateType != policy.ProvenancePredicateType {
			return "", errors.New("signed provenance statement has an unexpected type")
		}
		if len(statement.Subject) != 1 || statement.Subject[0].Name != policy.ImageRepository || statement.Subject[0].Digest.SHA256 != expectedHex || statement.Subject[0].Digest.GitCommit != "" {
			return "", errors.New("signed provenance subject does not match the Gateway image")
		}
		actualPredicate, err := json.Marshal(statement.Predicate)
		if err != nil {
			return "", fmt.Errorf("normalize signed provenance predicate: %w", err)
		}
		canonicalExpected, err := json.Marshal(expected)
		if err != nil {
			return "", fmt.Errorf("normalize expected provenance predicate: %w", err)
		}
		if !bytes.Equal(actualPredicate, canonicalExpected) {
			return "", errors.New("signed provenance predicate is not bound to the expected build")
		}
		digest := sha256.Sum256(payload)
		currentDigest := fmt.Sprintf("sha256:%x", digest)
		if statementDigest != "" && statementDigest != currentDigest {
			return "", errors.New("multiple conflicting owner-signed provenance statements exist")
		}
		statementDigest = currentDigest
	}
	if count == 0 {
		return "", errors.New("no owner-signed SLSA provenance was verified")
	}
	return statementDigest, nil
}

func positiveDecimal(value string) bool {
	parsed, err := strconv.ParseUint(value, 10, 64)
	return err == nil && parsed > 0 && strconv.FormatUint(parsed, 10) == value
}
