package policy

import (
	"encoding/json"
	"fmt"
	"maps"
	"slices"
	"strings"
	"time"

	slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
	slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
	gwpb "github.com/moby/buildkit/frontend/gateway/pb"
	provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
	"github.com/sirupsen/logrus"
)

const predicateTypeAnnotation = "in-toto.io/predicate-type"

var resolveProvenanceAttestations = []string{
	slsa02.PredicateSLSAProvenance,
	slsa1.PredicateSLSAProvenance,
}

type inTotoStatement struct {
	PredicateType string          `json:"predicateType"`
	Predicate     json.RawMessage `json:"predicate"`
}

func parseProvenance(ac *gwpb.AttestationChain, logf func(logrus.Level, string)) (*ImageProvenance, error) {
	if ac == nil || len(ac.Blobs) == 0 {
		return nil, nil
	}

	for _, b := range ac.Blobs {
		if b == nil || b.Descriptor_ == nil || len(b.Data) == 0 {
			continue
		}
		pt := b.Descriptor_.Annotations[predicateTypeAnnotation]
		if pt == "" {
			continue
		}
		if !slices.Contains(resolveProvenanceAttestations, pt) {
			continue
		}
		prv, err := parseProvenanceBlob(b.Data, pt, logf)
		if err != nil {
			return nil, err
		}
		if prv != nil {
			return prv, nil
		}
	}

	return nil, nil
}

func parseProvenanceBlob(dt []byte, pt string, logf func(logrus.Level, string)) (*ImageProvenance, error) {
	var stmt inTotoStatement
	if err := json.Unmarshal(dt, &stmt); err != nil {
		return nil, nil
	}
	if len(stmt.Predicate) == 0 {
		return nil, nil
	}

	predicateType := stmt.PredicateType
	if predicateType == "" {
		predicateType = pt
	}
	switch predicateType {
	case slsa1.PredicateSLSAProvenance:
		return parseSLSA1Provenance(stmt.Predicate, logf)
	case slsa02.PredicateSLSAProvenance:
		return parseSLSA02Provenance(stmt.Predicate, logf)
	default:
		return nil, nil
	}
}

func parseSLSA1Provenance(dt []byte, logf func(logrus.Level, string)) (*ImageProvenance, error) {
	var pred provenancetypes.ProvenancePredicateSLSA1
	if err := json.Unmarshal(dt, &pred); err != nil {
		return nil, nil
	}
	if pred.BuildDefinition.BuildType == "" && pred.RunDetails.Builder.ID == "" {
		return nil, nil
	}
	prv := &ImageProvenance{
		PredicateType: slsa1.PredicateSLSAProvenance,
		BuildType:     pred.BuildDefinition.BuildType,
		BuilderID:     pred.RunDetails.Builder.ID,
		ConfigSource: &ImageProvenanceConfigSource{
			URI:    pred.BuildDefinition.ExternalParameters.ConfigSource.URI,
			Digest: pred.BuildDefinition.ExternalParameters.ConfigSource.Digest,
			Path:   pred.BuildDefinition.ExternalParameters.ConfigSource.Path,
		},
		Frontend:  pred.BuildDefinition.ExternalParameters.Request.Frontend,
		BuildArgs: extractBuildArgs(pred.BuildDefinition.ExternalParameters.Request.Args),
		RawArgs:   pred.BuildDefinition.ExternalParameters.Request.Args,
	}
	prv.materialsRaw = rawMaterialsFromSLSA1(pred.BuildDefinition.ResolvedDependencies, logf)

	if md := pred.RunDetails.Metadata; md != nil {
		prv.InvocationID = md.InvocationID
		prv.StartedOn = formatProvenanceTime(md.StartedOn)
		prv.FinishedOn = formatProvenanceTime(md.FinishedOn)
		prv.Reproducible = new(md.Reproducible)
		prv.Hermetic = new(md.Hermetic)
		prv.Completeness = &ImageProvenanceCompleteness{
			Parameters: new(md.Completeness.Request),
			Materials:  new(md.Completeness.ResolvedDependencies),
		}
	}

	return prv, nil
}

func parseSLSA02Provenance(dt []byte, logf func(logrus.Level, string)) (*ImageProvenance, error) {
	var pred provenancetypes.ProvenancePredicateSLSA02
	if err := json.Unmarshal(dt, &pred); err != nil {
		return nil, nil
	}
	if pred.BuildType == "" && pred.Builder.ID == "" {
		return nil, nil
	}
	prv := &ImageProvenance{
		PredicateType: slsa02.PredicateSLSAProvenance,
		BuildType:     pred.BuildType,
		BuilderID:     pred.Builder.ID,
		ConfigSource: &ImageProvenanceConfigSource{
			URI:    pred.Invocation.ConfigSource.URI,
			Digest: pred.Invocation.ConfigSource.Digest,
			Path:   pred.Invocation.ConfigSource.EntryPoint,
		},
		Frontend:  pred.Invocation.Parameters.Frontend,
		BuildArgs: extractBuildArgs(pred.Invocation.Parameters.Args),
		RawArgs:   pred.Invocation.Parameters.Args,
	}
	prv.materialsRaw = rawMaterialsFromSLSA02(pred.Materials, logf)

	if md := pred.Metadata; md != nil {
		prv.InvocationID = md.BuildInvocationID
		prv.StartedOn = formatProvenanceTime(md.BuildStartedOn)
		prv.FinishedOn = formatProvenanceTime(md.BuildFinishedOn)
		prv.Reproducible = new(md.Reproducible)
		prv.Hermetic = new(md.Hermetic)
		prv.Completeness = &ImageProvenanceCompleteness{
			Parameters:  new(md.Completeness.Parameters),
			Environment: new(md.Completeness.Environment),
			Materials:   new(md.Completeness.Materials),
		}
	}

	return prv, nil
}

func rawMaterialsFromSLSA1(materials []slsa1.ResourceDescriptor, logf func(logrus.Level, string)) []slsa1.ResourceDescriptor {
	if len(materials) == 0 {
		return nil
	}
	out := make([]slsa1.ResourceDescriptor, 0, len(materials))
	for _, m := range materials {
		rd := slsa1.ResourceDescriptor{
			URI:    m.URI,
			Digest: maps.Clone(m.Digest),
		}
		if _, _, err := parseSLSAMaterial(rd); err != nil {
			if logf != nil {
				logf(logrus.WarnLevel, fmt.Sprintf("skipping unsupported provenance material %q: %v", m.URI, err))
			}
			continue
		}
		out = append(out, rd)
	}
	return out
}

func rawMaterialsFromSLSA02(materials []slsa02.ProvenanceMaterial, logf func(logrus.Level, string)) []slsa1.ResourceDescriptor {
	if len(materials) == 0 {
		return nil
	}
	out := make([]slsa1.ResourceDescriptor, 0, len(materials))
	for _, m := range materials {
		rd := slsa1.ResourceDescriptor{
			URI:    m.URI,
			Digest: maps.Clone(m.Digest),
		}
		if _, _, err := parseSLSAMaterial(rd); err != nil {
			if logf != nil {
				logf(logrus.WarnLevel, fmt.Sprintf("skipping unsupported provenance material %q: %v", m.URI, err))
			}
			continue
		}
		out = append(out, rd)
	}
	return out
}

func formatProvenanceTime(t *time.Time) string {
	if t == nil {
		return ""
	}
	return t.UTC().Format(time.RFC3339)
}

func extractBuildArgs(args map[string]string) map[string]string {
	if len(args) == 0 {
		return nil
	}
	const prefix = "build-arg:"
	out := make(map[string]string)
	for k, v := range args {
		if name, ok := strings.CutPrefix(k, prefix); ok && name != "" {
			out[name] = v
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}
