package checkpoint

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"path/filepath"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

const digestPrefixLength = 16

type Fingerprint struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}
type IdentityInput struct {
	Pipeline                               pipeline.ResolvedPipeline
	InputKey, RawInputDigest, SourceDigest string
	SelectedLanes                          []string
	RuntimeOverrides                       []Fingerprint
	References                             []artifacts.ReferenceProvenance
	ProvenanceFingerprints                 []Fingerprint
}
type Identity struct {
	Digest                 string        `json:"digest"`
	PipelineID             string        `json:"pipeline_id"`
	PipelineDigest         string        `json:"pipeline_digest"`
	PipelineTopology       []string      `json:"pipeline_topology,omitempty"`
	InputKey               string        `json:"input_key"`
	RawInputDigest         string        `json:"raw_input_digest,omitempty"`
	SourceDigest           string        `json:"source_digest,omitempty"`
	SelectedLanes          []string      `json:"selected_lanes,omitempty"`
	RuntimeOverrides       []Fingerprint `json:"runtime_overrides,omitempty"`
	ReferenceDigests       []Fingerprint `json:"reference_digests,omitempty"`
	ProvenanceFingerprints []Fingerprint `json:"provenance_fingerprints,omitempty"`
}

func NewIdentity(input IdentityInput) (Identity, error) {
	pipelineID, pipelineDigest, inputKey := strings.TrimSpace(input.Pipeline.ID), strings.TrimSpace(input.Pipeline.Digest), strings.TrimSpace(input.InputKey)
	if pipelineID == "" {
		return Identity{}, fmt.Errorf("checkpoint identity pipeline id must not be empty")
	}
	if pipelineDigest == "" {
		return Identity{}, fmt.Errorf("checkpoint identity pipeline digest must not be empty")
	}
	if inputKey == "" {
		inputKey = strings.TrimSpace(input.Pipeline.Input.Module)
	}
	if inputKey == "" {
		return Identity{}, fmt.Errorf("checkpoint identity input key must not be empty")
	}
	if strings.TrimSpace(input.RawInputDigest) == "" && strings.TrimSpace(input.SourceDigest) == "" {
		return Identity{}, fmt.Errorf("checkpoint identity raw input digest or source digest must be set")
	}
	v := Identity{PipelineID: pipelineID, PipelineDigest: pipelineDigest, PipelineTopology: pipelineTopology(input.Pipeline), InputKey: inputKey, RawInputDigest: strings.TrimSpace(input.RawInputDigest), SourceDigest: strings.TrimSpace(input.SourceDigest), SelectedLanes: normalizedLanes(input.SelectedLanes, input.Pipeline.AllArtifactLanes()), RuntimeOverrides: normalizeIdentityFingerprints(input.RuntimeOverrides), ReferenceDigests: referenceFingerprints(input.References), ProvenanceFingerprints: normalizeIdentityFingerprints(input.ProvenanceFingerprints)}
	data, err := json.Marshal(Identity{PipelineID: v.PipelineID, PipelineDigest: v.PipelineDigest, PipelineTopology: v.PipelineTopology, InputKey: v.InputKey, RawInputDigest: v.RawInputDigest, SourceDigest: v.SourceDigest, SelectedLanes: v.SelectedLanes, RuntimeOverrides: v.RuntimeOverrides, ReferenceDigests: v.ReferenceDigests, ProvenanceFingerprints: v.ProvenanceFingerprints})
	if err != nil {
		return Identity{}, fmt.Errorf("marshal checkpoint identity: %w", err)
	}
	sum := sha256.Sum256(data)
	v.Digest = "sha256:" + hex.EncodeToString(sum[:])
	return v, nil
}

func pipelineTopology(resolved pipeline.ResolvedPipeline) []string {
	var topology []string
	for _, step := range resolved.Steps {
		for _, lane := range step.ArtifactLanes {
			topology = append(topology, strings.TrimSpace(step.ID)+"\x00"+strings.TrimSpace(lane.ID))
		}
	}
	if len(topology) == 0 {
		return nil
	}
	return topology
}
func (i Identity) RelativePath() (string, error) {
	p, err := safeComponent(i.PipelineID)
	if err != nil {
		return "", fmt.Errorf("checkpoint identity pipeline id: %w", err)
	}
	k, err := safeComponent(i.InputKey)
	if err != nil {
		return "", fmt.Errorf("checkpoint identity input key: %w", err)
	}
	s := digestPrefix(i.SourceDigest)
	if s == "" {
		s = digestPrefix(i.RawInputDigest)
	}
	d := digestPrefix(i.PipelineDigest)
	x := digestPrefix(i.Digest)
	if s == "" || d == "" || x == "" {
		return "", fmt.Errorf("checkpoint identity digest prefix must not be empty")
	}
	s, err = safeComponent(s)
	if err != nil {
		return "", fmt.Errorf("checkpoint identity source digest: %w", err)
	}
	d, err = safeComponent(d)
	if err != nil {
		return "", fmt.Errorf("checkpoint identity pipeline digest: %w", err)
	}
	x, err = safeComponent(x)
	if err != nil {
		return "", fmt.Errorf("checkpoint identity digest: %w", err)
	}
	return filepath.ToSlash(filepath.Join(p, k+"-"+s, d, x)), nil
}
func normalizedLanes(selected []string, resolved []pipeline.ResolvedArtifactLane) []string {
	if len(selected) == 0 {
		for _, lane := range resolved {
			selected = append(selected, lane.ID)
		}
	}
	return normalizeStrings(selected)
}
func normalizeIdentityFingerprints(values []Fingerprint) []Fingerprint {
	by := map[string]string{}
	for _, v := range values {
		if n, x := strings.TrimSpace(v.Name), strings.TrimSpace(v.Value); n != "" && x != "" {
			by[n] = x
		}
	}
	names := make([]string, 0, len(by))
	for n := range by {
		names = append(names, n)
	}
	sort.Strings(names)
	out := make([]Fingerprint, 0, len(names))
	for _, n := range names {
		out = append(out, Fingerprint{Name: n, Value: by[n]})
	}
	if len(out) == 0 {
		return nil
	}
	return out
}
func referenceFingerprints(refs []artifacts.ReferenceProvenance) []Fingerprint {
	var values []Fingerprint
	for _, r := range refs {
		if d := strings.TrimSpace(r.Digest); d != "" {
			values = append(values, Fingerprint{Name: strings.Join([]string{strings.TrimSpace(r.Stage), strings.TrimSpace(r.LaneID), strings.TrimSpace(r.SlotName), strings.TrimSpace(r.OriginURI)}, ":"), Value: d})
		}
	}
	return normalizeIdentityFingerprints(values)
}
func normalizeStrings(values []string) []string {
	seen := map[string]struct{}{}
	for _, v := range values {
		if v = strings.TrimSpace(v); v != "" {
			seen[v] = struct{}{}
		}
	}
	out := make([]string, 0, len(seen))
	for v := range seen {
		out = append(out, v)
	}
	sort.Strings(out)
	if len(out) == 0 {
		return nil
	}
	return out
}
func digestPrefix(v string) string {
	v = strings.TrimSpace(v)
	if n := strings.Index(v, ":"); n >= 0 {
		v = v[n+1:]
	}
	if len(v) > digestPrefixLength {
		return v[:digestPrefixLength]
	}
	return v
}
func safeComponent(v string) (string, error) {
	v = strings.TrimSpace(v)
	if v == "" {
		return "", fmt.Errorf("must not be empty")
	}
	var b strings.Builder
	for _, r := range v {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
			b.WriteRune(r)
		} else {
			b.WriteString(fmt.Sprintf("~%x", r))
		}
	}
	out := b.String()
	if out == "." || out == ".." || strings.Contains(out, "..") || strings.ContainsAny(out, `/\\`) {
		return "", fmt.Errorf("%q is not filesystem safe", v)
	}
	return out, nil
}
