// Package oci implements an OCI Distribution Specification v1.1 registry.
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
//
// Storage layout under ociRoot:
//
//	blobs/sha256/<hex64>   — content-addressable layer/config blobs
//	uploads/<uuid>         — temporary files for in-progress chunked uploads
package oci

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

// Registry manages the on-disk blob store and is used by the HTTP handler.
type Registry struct {
	root string // absolute path to the OCI storage root
}

// New creates a Registry rooted at ociRoot, creating the directory tree if needed.
func New(ociRoot string) (*Registry, error) {
	for _, sub := range []string{"blobs/sha256", "uploads"} {
		if err := os.MkdirAll(filepath.Join(ociRoot, sub), 0700); err != nil {
			return nil, fmt.Errorf("oci: init storage %s: %w", sub, err)
		}
	}
	return &Registry{root: ociRoot}, nil
}

// Root returns the storage root path.
func (r *Registry) Root() string { return r.root }

// ─── Blob paths ───────────────────────────────────────────────────────────────

// BlobPath returns the filesystem path for a blob identified by its digest.
// digest must be in the form "sha256:<hex>".
func (r *Registry) BlobPath(digest string) (string, error) {
	hex, err := digestHex(digest)
	if err != nil {
		return "", err
	}
	return filepath.Join(r.root, "blobs", "sha256", hex), nil
}

// UploadPath returns the filesystem path for a chunked upload session.
func (r *Registry) UploadPath(uploadID string) string {
	return filepath.Join(r.root, "uploads", sanitiseID(uploadID))
}

// BlobExists reports whether a blob with the given digest exists on disk.
func (r *Registry) BlobExists(digest string) bool {
	p, err := r.BlobPath(digest)
	if err != nil {
		return false
	}
	_, err = os.Stat(p)
	return err == nil
}

// BlobSize returns the size of the blob in bytes, or -1 if it doesn't exist.
func (r *Registry) BlobSize(digest string) int64 {
	p, err := r.BlobPath(digest)
	if err != nil {
		return -1
	}
	info, err := os.Stat(p)
	if err != nil {
		return -1
	}
	return info.Size()
}

// ReadBlob opens a blob for streaming.  Caller must close the returned file.
func (r *Registry) ReadBlob(digest string) (*os.File, error) {
	p, err := r.BlobPath(digest)
	if err != nil {
		return nil, err
	}
	return os.Open(p)
}

// WriteBlob writes src into the blob store, verifies the digest, and returns
// the computed digest string ("sha256:<hex>") and size.
// If a blob with the same digest already exists it is not overwritten.
func (r *Registry) WriteBlob(src io.Reader) (digest string, size int64, err error) {
	tmp, err := os.CreateTemp(filepath.Join(r.root, "uploads"), "blob-*")
	if err != nil {
		return "", 0, fmt.Errorf("oci: create tmp blob: %w", err)
	}
	tmpPath := tmp.Name()
	defer func() {
		tmp.Close()
		if err != nil {
			os.Remove(tmpPath)
		}
	}()

	h := sha256.New()
	mw := io.MultiWriter(tmp, h)
	size, err = io.Copy(mw, src)
	if err != nil {
		return "", 0, fmt.Errorf("oci: write blob: %w", err)
	}
	tmp.Close()

	digest = "sha256:" + hex.EncodeToString(h.Sum(nil))
	dest, err2 := r.BlobPath(digest)
	if err2 != nil {
		os.Remove(tmpPath)
		return "", 0, err2
	}

	if _, statErr := os.Stat(dest); statErr == nil {
		// Already exists — deduplication.
		os.Remove(tmpPath)
		return digest, size, nil
	}

	if err = os.Rename(tmpPath, dest); err != nil {
		return "", 0, fmt.Errorf("oci: commit blob: %w", err)
	}
	return digest, size, nil
}

// FinishUpload finalises a chunked upload: reads the temp file, verifies
// clientDigest (if non-empty), atomically moves it to the blob store, and
// returns the canonical digest and size.
func (r *Registry) FinishUpload(uploadID, clientDigest string) (digest string, size int64, err error) {
	src := r.UploadPath(uploadID)
	f, err := os.Open(src)
	if err != nil {
		return "", 0, fmt.Errorf("oci: open upload: %w", err)
	}

	h := sha256.New()
	size, err = io.Copy(h, f)
	f.Close()
	if err != nil {
		return "", 0, fmt.Errorf("oci: hash upload: %w", err)
	}

	digest = "sha256:" + hex.EncodeToString(h.Sum(nil))

	if clientDigest != "" && clientDigest != digest {
		os.Remove(src)
		return "", 0, &DigestMismatch{Expected: clientDigest, Actual: digest}
	}

	dest, err := r.BlobPath(digest)
	if err != nil {
		return "", 0, err
	}

	if _, statErr := os.Stat(dest); statErr == nil {
		// Blob already exists — dedup.
		os.Remove(src)
		return digest, size, nil
	}

	if err = os.Rename(src, dest); err != nil {
		return "", 0, fmt.Errorf("oci: commit upload: %w", err)
	}
	return digest, size, nil
}

// AppendUpload appends src to an existing upload session file and returns the
// new total offset.
func (r *Registry) AppendUpload(uploadID string, src io.Reader) (newOffset int64, err error) {
	path := r.UploadPath(uploadID)
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {
		return 0, fmt.Errorf("oci: open upload for append: %w", err)
	}
	defer f.Close()

	n, err := io.Copy(f, src)
	if err != nil {
		return 0, fmt.Errorf("oci: append upload: %w", err)
	}

	info, err := f.Stat()
	if err != nil {
		return n, nil
	}
	return info.Size(), nil
}

// UploadOffset returns the number of bytes written to an upload session so far.
func (r *Registry) UploadOffset(uploadID string) int64 {
	info, err := os.Stat(r.UploadPath(uploadID))
	if err != nil {
		return 0
	}
	return info.Size()
}

// CancelUpload removes the temporary upload file.
func (r *Registry) CancelUpload(uploadID string) {
	os.Remove(r.UploadPath(uploadID))
}

// DeleteBlob removes a blob from disk.
func (r *Registry) DeleteBlob(digest string) error {
	p, err := r.BlobPath(digest)
	if err != nil {
		return err
	}
	return os.Remove(p)
}

// ─── Manifest helpers ─────────────────────────────────────────────────────────

// ManifestDescriptor extracts the digest and size from a raw manifest body.
func ManifestDescriptor(body []byte) (digest string, size int64) {
	h := sha256.Sum256(body)
	return "sha256:" + hex.EncodeToString(h[:]), int64(len(body))
}

// IsDigestRef returns true when ref looks like a digest ("sha256:<hex>").
func IsDigestRef(ref string) bool {
	return strings.HasPrefix(ref, "sha256:")
}

// ─── OCI error types ─────────────────────────────────────────────────────────

// ErrorCode is an OCI Distribution API error code.
type ErrorCode string

const (
	ErrBlobUnknown         ErrorCode = "BLOB_UNKNOWN"
	ErrBlobUploadInvalid   ErrorCode = "BLOB_UPLOAD_INVALID"
	ErrBlobUploadUnknown   ErrorCode = "BLOB_UPLOAD_UNKNOWN"
	ErrDigestInvalid       ErrorCode = "DIGEST_INVALID"
	ErrManifestBlobUnknown ErrorCode = "MANIFEST_BLOB_UNKNOWN"
	ErrManifestInvalid     ErrorCode = "MANIFEST_INVALID"
	ErrManifestUnknown     ErrorCode = "MANIFEST_UNKNOWN"
	ErrNameInvalid         ErrorCode = "NAME_INVALID"
	ErrNameUnknown         ErrorCode = "NAME_UNKNOWN"
	ErrTagInvalid          ErrorCode = "TAG_INVALID"
	ErrUnauthorized        ErrorCode = "UNAUTHORIZED"
	ErrDenied              ErrorCode = "DENIED"
	ErrUnsupported         ErrorCode = "UNSUPPORTED"
)

// APIError is a single OCI error entry.
type APIError struct {
	Code    ErrorCode   `json:"code"`
	Message string      `json:"message"`
	Detail  interface{} `json:"detail,omitempty"`
}

// ErrorResponse is the top-level OCI error response body.
type ErrorResponse struct {
	Errors []APIError `json:"errors"`
}

// NewError builds an ErrorResponse JSON body.
func NewError(code ErrorCode, msg string) []byte {
	b, _ := json.Marshal(ErrorResponse{Errors: []APIError{{Code: code, Message: msg}}})
	return b
}

// DigestMismatch is returned when a client-provided digest doesn't match the computed one.
type DigestMismatch struct {
	Expected string
	Actual   string
}

func (e *DigestMismatch) Error() string {
	return fmt.Sprintf("digest mismatch: expected %s, got %s", e.Expected, e.Actual)
}

// ─── path helpers ─────────────────────────────────────────────────────────────

// digestHex validates a "sha256:<hex>" digest string and returns the hex part.
func digestHex(digest string) (string, error) {
	if !strings.HasPrefix(digest, "sha256:") {
		return "", fmt.Errorf("oci: only sha256 digests are supported, got %q", digest)
	}
	h := strings.TrimPrefix(digest, "sha256:")
	if len(h) != 64 {
		return "", fmt.Errorf("oci: invalid sha256 digest length: %d", len(h))
	}
	return h, nil
}

// sanitiseID returns only the last path component of an upload ID,
// preventing any path traversal regardless of encoding.
func sanitiseID(id string) string {
	return filepath.Base(id)
}

// ParseOCIPath extracts the image name and the operation kind from a path
// under /v2/.  name may contain slashes (e.g. "alice/myapp").
//
// Returns: name, kind, ref where kind is one of:
//
//	"tags"     → ref = ""
//	"manifest" → ref = tag or digest
//	"blob"     → ref = digest
//	"upload"   → ref = uploadID (empty for new upload)
//	""         → unrecognised path
func ParseOCIPath(rawPath string) (name, kind, ref string) {
	// Strip leading /v2/
	p := strings.TrimPrefix(rawPath, "/v2/")
	if p == "" || p == "/" {
		return "", "", ""
	}

	// Try known suffixes from most to least specific.
	type suffix struct {
		needle string
		kind   string
	}
	suffixes := []suffix{
		{"/blobs/uploads/", "upload"},
		{"/blobs/sha256:", "blob"},
		{"/blobs/", "blob"},
		{"/manifests/", "manifest"},
		{"/tags/list", "tags"},
	}

	for _, s := range suffixes {
		idx := strings.Index(p, s.needle)
		if idx < 0 {
			continue
		}
		name = p[:idx]
		rest := p[idx+len(s.needle):]
		kind = s.kind
		switch s.kind {
		case "blob":
			// ref is digest: re-attach the sha256: prefix if needed
			if strings.HasSuffix(s.needle, ":") {
				ref = "sha256:" + rest
			} else {
				ref = rest
			}
		case "upload":
			ref = rest // upload UUID or empty for new session
		default:
			ref = rest
		}
		return name, kind, ref
	}
	return "", "", ""
}

// ValidateName returns an error if the image name is empty or contains
// invalid characters.
func ValidateName(name string) error {
	if name == "" {
		return errors.New("empty image name")
	}
	for _, c := range name {
		if !isNameChar(c) {
			return fmt.Errorf("invalid character %q in image name", c)
		}
	}
	return nil
}

func isNameChar(c rune) bool {
	return (c >= 'a' && c <= 'z') ||
		(c >= 'A' && c <= 'Z') ||
		(c >= '0' && c <= '9') ||
		c == '.' || c == '-' || c == '_' || c == '/'
}
