package git_pages

import (
	"context"
	"errors"
	"fmt"
	"io"
	"iter"
	"strings"
	"time"
)

var ErrObjectNotFound = errors.New("not found")
var ErrPreconditionFailed = errors.New("precondition failed")
var ErrWriteConflict = errors.New("write conflict")
var ErrDomainFrozen = errors.New("domain administratively frozen")

func splitBlobName(name string) []string {
	if algo, hash, found := strings.Cut(name, "-"); found {
		return []string{algo, hash[0:2], hash[2:4], hash[4:]}
	} else {
		panic("malformed blob name")
	}
}

func joinBlobName(parts []string) string {
	return fmt.Sprintf("%s-%s", parts[0], strings.Join(parts[1:], ""))
}

type BackendFeature string

const (
	FeatureCheckDomainMarker BackendFeature = "check-domain-marker"
)

type BlobMetadata struct {
	Name         string
	Size         int64
	LastModified time.Time
}

type GetManifestOptions struct {
	// If true and the manifest is past the cache `MaxAge`, `GetManifest` blocks and returns
	// a fresh object instead of revalidating in background and returning a stale object.
	BypassCache bool
}

type ManifestMetadata struct {
	Name         string
	Size         int64
	LastModified time.Time
	ETag         string
}

type ModifyManifestOptions struct {
	// If non-zero, the request will only succeed if the manifest hasn't been changed since
	// the given time. Whether this is racy or not is can be determined via `HasAtomicCAS()`.
	IfUnmodifiedSince time.Time
	// If non-empty, the request will only succeed if the manifest hasn't changed from
	// the state corresponding to the ETag. Whether this is racy or not is can be determined
	// via `HasAtomicCAS()`.
	IfMatch string
}

type SearchAuditLogOptions struct {
	// Inclusive lower bound on returned audit records, per their Snowflake ID (which may differ
	// slightly from the embedded timestamp). If zero, audit records are returned since beginning
	// of time.
	Since time.Time
	// Inclusive upper bound on returned audit records, per their Snowflake ID (which may differ
	// slightly from the embedded timestamp). If zero, audit records are returned until the end
	// of time.
	Until time.Time
}

type SearchAuditLogResult struct {
	ID  AuditID
	Err error
}

type Backend interface {
	// Returns true if the feature has been enabled for this store, false otherwise.
	HasFeature(ctx context.Context, feature BackendFeature) bool

	// Enables the feature for this store.
	EnableFeature(ctx context.Context, feature BackendFeature) error

	// Retrieve a blob. Returns `reader, size, mtime, err`.
	GetBlob(ctx context.Context, name string) (
		reader io.ReadSeeker, metadata BlobMetadata, err error,
	)

	// Store a blob. If a blob called `name` already exists, this function returns `nil` without
	// regards to the old or new contents. It is expected that blobs are content-addressed, i.e.
	// the `name` contains a cryptographic hash of `data`, but the backend is ignorant of this.
	PutBlob(ctx context.Context, name string, data []byte) error

	// Delete a blob. This is an unconditional operation that can break integrity of manifests.
	DeleteBlob(ctx context.Context, name string) error

	// Iterate through all blobs. Whether blobs that are newly added during iteration will appear
	// in the results is unspecified.
	EnumerateBlobs(ctx context.Context) iter.Seq2[BlobMetadata, error]

	// Retrieve a manifest.
	GetManifest(ctx context.Context, name string, opts GetManifestOptions) (
		manifest *Manifest, metadata ManifestMetadata, err error,
	)

	// Stage a manifest. This operation stores a new version of a manifest, locking any blobs
	// referenced from it in place (for garbage collection purposes) but without any other side
	// effects.
	StageManifest(ctx context.Context, manifest *Manifest) error

	// Whether a compare-and-swap operation on a manifest is truly race-free, or only best-effort
	// atomic with a small but non-zero window where two requests may race where the one committing
	// first will have its update lost. (Plain swap operations are always guaranteed to be atomic.)
	HasAtomicCAS(ctx context.Context) bool

	// Commit a manifest. This is an atomic operation; `GetManifest` calls will return either
	// the old version or the new version of the manifest, never anything else.
	CommitManifest(ctx context.Context, name string, manifest *Manifest, opts ModifyManifestOptions) error

	// Delete a manifest.
	DeleteManifest(ctx context.Context, name string, opts ModifyManifestOptions) error

	// Iterate through all manifests. Whether manifests that are newly added during iteration
	// will appear in the results is unspecified.
	EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error]

	// Check whether a domain has any deployments.
	CheckDomain(ctx context.Context, domain string) (found bool, err error)

	// Create a domain. This allows us to start serving content for the domain.
	CreateDomain(ctx context.Context, domain string) error

	// Freeze  a domain. This allows a site to be administratively locked, e.g. if it
	// is discovered serving abusive content.
	FreezeDomain(ctx context.Context, domain string) error

	// Thaw a domain. This removes the previously placed administrative lock (if any).
	UnfreezeDomain(ctx context.Context, domain string) error

	// Append a record to the audit log.
	AppendAuditLog(ctx context.Context, id AuditID, record *AuditRecord) error

	// Retrieve a single record from the audit log.
	QueryAuditLog(ctx context.Context, id AuditID) (record *AuditRecord, err error)

	// Retrieve records from the audit log by time range.
	SearchAuditLog(ctx context.Context, opts SearchAuditLogOptions) iter.Seq2[AuditID, error]
}

func CreateBackend(ctx context.Context, config *StorageConfig) (backend Backend, err error) {
	switch config.Type {
	case "fs":
		if backend, err = NewFSBackend(ctx, &config.FS); err != nil {
			err = fmt.Errorf("fs backend: %w", err)
		}
	case "s3":
		if backend, err = NewS3Backend(ctx, &config.S3); err != nil {
			err = fmt.Errorf("s3 backend: %w", err)
		}
	default:
		err = fmt.Errorf("unknown backend: %s", config.Type)
	}
	backend = NewAuditedBackend(backend)
	return
}
