package catalogsnapshot

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

// Store exposes only exact content-addressed reads and idempotent writes. It
// intentionally has no latest/current/list/watch/TTL operation.
type Store interface {
	Put(context.Context, Artifact) error
	Get(context.Context, ArtifactKind, Digest) (Artifact, error)
}

type DirectoryStore struct {
	root string
}

func NewDirectoryStore(root string) (*DirectoryStore, error) {
	if strings.TrimSpace(root) == "" {
		return nil, fmt.Errorf("catalog snapshot store root is required")
	}
	clean, err := filepath.Abs(filepath.Clean(root))
	if err != nil {
		return nil, fmt.Errorf("resolve catalog snapshot store root: %w", err)
	}
	if err := mkdirAllDurable(clean, 0o750); err != nil {
		return nil, fmt.Errorf("create catalog snapshot store: %w", err)
	}
	return &DirectoryStore{root: clean}, nil
}

func (s *DirectoryStore) Put(ctx context.Context, artifact Artifact) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	if err := validateArtifact(artifact); err != nil {
		return err
	}
	path, err := s.artifactPath(artifact.kind, artifact.digest)
	if err != nil {
		return err
	}
	dir := filepath.Dir(path)
	if err := mkdirAllDurable(dir, 0o750); err != nil {
		return fmt.Errorf("create artifact kind directory: %w", err)
	}

	temp, err := os.CreateTemp(dir, ".pending-*")
	if err != nil {
		return fmt.Errorf("create immutable artifact staging file: %w", err)
	}
	tempName := temp.Name()
	defer os.Remove(tempName)
	closed := false
	defer func() {
		if !closed {
			_ = temp.Close()
		}
	}()
	if _, err := temp.Write(artifact.bytes); err != nil {
		return fmt.Errorf("write immutable artifact: %w", err)
	}
	if err := temp.Sync(); err != nil {
		return fmt.Errorf("sync immutable artifact: %w", err)
	}
	if err := temp.Chmod(0o440); err != nil {
		return fmt.Errorf("set immutable artifact permissions: %w", err)
	}
	if err := temp.Sync(); err != nil {
		return fmt.Errorf("sync immutable artifact metadata: %w", err)
	}
	if err := temp.Close(); err != nil {
		return fmt.Errorf("close immutable artifact: %w", err)
	}
	closed = true
	if err := ctx.Err(); err != nil {
		return err
	}

	// A hard-link publish is atomic and cannot replace an existing digest path.
	// Concurrent identical writers converge; a conflicting existing file fails
	// integrity validation below.
	if err := os.Link(tempName, path); err != nil {
		if !errors.Is(err, os.ErrExist) {
			return fmt.Errorf("publish immutable artifact: %w", err)
		}
		existing, readErr := os.ReadFile(path)
		if readErr != nil {
			return fmt.Errorf("read existing immutable artifact: %w", readErr)
		}
		if !bytes.Equal(existing, artifact.bytes) {
			return fmt.Errorf("%w: digest path %s already contains different bytes", ErrArtifactCorrupt, artifact.digest)
		}
		existingArtifact := artifactFromBytes(artifact.kind, artifact.digest, existing)
		if err := validateArtifact(existingArtifact); err != nil {
			return err
		}
		// A concurrent publisher may have linked the target but not yet synced
		// the directory. Every successful Put establishes its own durability
		// barrier rather than depending on that publisher reaching its sync.
		if err := syncDirectory(dir); err != nil {
			return err
		}
		return nil
	}
	if err := syncDirectory(dir); err != nil {
		return err
	}
	return nil
}

// mkdirAllDurable creates the missing directory chain and fsyncs every new
// child and parent entry from the outside in. MkdirAll alone does not make the
// new directory entries crash-durable.
func mkdirAllDurable(path string, perm fs.FileMode) error {
	clean := filepath.Clean(path)
	missing := []string{}
	for current := clean; ; current = filepath.Dir(current) {
		info, err := os.Stat(current)
		if err == nil {
			if !info.IsDir() {
				return fmt.Errorf("catalog snapshot store path %q is not a directory", current)
			}
			break
		}
		if !errors.Is(err, os.ErrNotExist) {
			return err
		}
		missing = append(missing, current)
		parent := filepath.Dir(current)
		if parent == current {
			return fmt.Errorf("catalog snapshot store has no existing directory ancestor for %q", clean)
		}
	}
	if len(missing) == 0 {
		// Another process may have created this directory without yet making
		// its parent entry durable. Re-establish both barriers so this caller's
		// success never depends on the creator reaching its own fsync.
		if err := syncDirectory(filepath.Dir(clean)); err != nil {
			return err
		}
		return syncDirectory(clean)
	}
	if err := os.MkdirAll(clean, perm); err != nil {
		return err
	}
	for i := len(missing) - 1; i >= 0; i-- {
		if err := syncDirectory(filepath.Dir(missing[i])); err != nil {
			return err
		}
		if err := syncDirectory(missing[i]); err != nil {
			return err
		}
	}
	return nil
}

func (s *DirectoryStore) Get(ctx context.Context, kind ArtifactKind, digest Digest) (Artifact, error) {
	if err := ctx.Err(); err != nil {
		return Artifact{}, err
	}
	path, err := s.artifactPath(kind, digest)
	if err != nil {
		return Artifact{}, err
	}
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return Artifact{}, fmt.Errorf("%w: %s/%s", ErrArtifactNotFound, kind, digest)
	}
	if err != nil {
		return Artifact{}, fmt.Errorf("read immutable artifact: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return Artifact{}, err
	}
	artifact := artifactFromBytes(kind, digest, data)
	if err := validateArtifact(artifact); err != nil {
		return Artifact{}, err
	}
	return artifact, nil
}

func validateArtifact(artifact Artifact) error {
	if err := verifyArtifactBytes(artifact); err != nil {
		return err
	}
	switch artifact.kind {
	case ArtifactCatalogSnapshot:
		_, err := OpenCatalogSnapshot(artifact)
		return err
	case ArtifactRuntimeMetadataView:
		_, err := OpenRuntimeMetadataView(artifact)
		return err
	case ArtifactServingCompatibilityReceipt:
		_, err := OpenServingCompatibilityReceipt(artifact)
		return err
	default:
		return fmt.Errorf("%w: unknown artifact kind %q", ErrArtifactCorrupt, artifact.kind)
	}
}

func (s *DirectoryStore) artifactPath(kind ArtifactKind, digest Digest) (string, error) {
	if s == nil || s.root == "" {
		return "", fmt.Errorf("catalog snapshot store is not initialized")
	}
	if !validDigest(digest) {
		return "", fmt.Errorf("%w: malformed digest %q", ErrArtifactCorrupt, digest)
	}
	var directory string
	switch kind {
	case ArtifactCatalogSnapshot:
		directory = "catalog-snapshot-v2"
	case ArtifactRuntimeMetadataView:
		directory = "runtime-metadata-view-v2"
	case ArtifactServingCompatibilityReceipt:
		directory = "runtime-serving-compatibility-receipt-v1"
	default:
		return "", fmt.Errorf("%w: unknown artifact kind %q", ErrArtifactCorrupt, kind)
	}
	hexDigest := strings.TrimPrefix(string(digest), "sha256:")
	return filepath.Join(s.root, directory, hexDigest+".json"), nil
}

func syncDirectory(path string) error {
	directory, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("open artifact directory for sync: %w", err)
	}
	defer directory.Close()
	if err := directory.Sync(); err != nil {
		return fmt.Errorf("sync artifact directory: %w", err)
	}
	return nil
}
