package microsandbox

import (
	"context"
	"errors"
	"fmt"

	"github.com/superradcompany/microsandbox/sdk/go/internal/ffi"
)

// ErrorKind identifies the specific type of microsandbox error.
//
// The set of kinds is aligned with the Node.js and Python SDKs so that
// portable code written against one SDK compiles against another. Some
// kinds are reserved for error paths that the current Rust runtime does
// not yet emit; switch statements should include a default case.
type ErrorKind int

const (
	// ErrUnknown is the fallback when the Rust side reports a kind this
	// version of the SDK does not recognize.
	ErrUnknown ErrorKind = iota

	// ErrSandboxNotFound indicates the requested sandbox does not exist.
	ErrSandboxNotFound

	// ErrSandboxNotRunning indicates the sandbox exists but is not running.
	// Reserved for future use; currently surfaces as ErrInternal.
	ErrSandboxNotRunning

	// ErrSandboxAlreadyExists indicates a sandbox with the given name
	// already exists. Returned by CreateSandbox when the name is taken
	// and WithReplace was not set, and by CreateSandbox with WithReplace
	// when a live in-process Sandbox handle for that name is still held.
	ErrSandboxAlreadyExists

	// ErrSandboxStillRunning indicates a sandbox cannot be removed while
	// it is still running.
	ErrSandboxStillRunning

	// ErrVolumeNotFound indicates the requested volume does not exist.
	ErrVolumeNotFound

	// ErrVolumeAlreadyExists indicates a volume with the given name
	// already exists.
	ErrVolumeAlreadyExists

	// ErrExecTimeout indicates a command execution exceeded its timeout.
	ErrExecTimeout

	// ErrExecFailed indicates a command failed for a reason other than a
	// timeout or non-zero exit (e.g. the guest agent dropped the request).
	// Reserved for future use; currently surfaces as ErrInternal.
	ErrExecFailed

	// ErrFilesystem indicates a filesystem operation inside the sandbox
	// failed (e.g. read/write/list returned an error from the guest).
	ErrFilesystem

	// ErrPathNotFound indicates a sandbox filesystem path does not exist.
	// Reserved for future use; currently surfaces as ErrFilesystem.
	ErrPathNotFound

	// ErrImageNotFound indicates the OCI image reference could not be
	// resolved.
	ErrImageNotFound

	// ErrImageInUse indicates the image cannot be removed because one or
	// more sandboxes are still referencing it.
	ErrImageInUse

	// ErrImagePullFailed indicates an image pull failed after resolution
	// succeeded. Reserved for future use; currently surfaces as
	// ErrImageNotFound.
	ErrImagePullFailed

	// ErrSnapshotNotFound indicates the requested snapshot artifact or index
	// entry does not exist.
	ErrSnapshotNotFound

	// ErrSnapshotAlreadyExists indicates a snapshot destination already exists.
	ErrSnapshotAlreadyExists

	// ErrSnapshotSandboxRunning indicates snapshotting was requested for a
	// sandbox that is still running.
	ErrSnapshotSandboxRunning

	// ErrSnapshotImageMissing indicates the image pinned by a snapshot is not
	// present in the local cache.
	ErrSnapshotImageMissing

	// ErrSnapshotIntegrity indicates snapshot verification failed.
	ErrSnapshotIntegrity

	// ErrSnapshotMigration indicates an adjacent-release snapshot migration is blocked.
	ErrSnapshotMigration

	// ErrPatchFailed indicates a rootfs patch could not be applied before
	// the VM booted.
	ErrPatchFailed

	// ErrNetworkPolicy indicates a network policy configuration or
	// runtime violation. Reserved for future use; currently surfaces as
	// ErrInvalidConfig.
	ErrNetworkPolicy

	// ErrSecretViolation indicates a secret was sent to a disallowed host.
	// Reserved for future use; currently surfaces as ErrInternal.
	ErrSecretViolation

	// ErrTLS indicates a TLS interception error. Reserved for future use;
	// currently surfaces as ErrInternal.
	ErrTLS

	// ErrIO indicates a host-side I/O error.
	ErrIO

	// ErrInvalidConfig indicates the sandbox or volume configuration was
	// rejected by the runtime.
	ErrInvalidConfig

	// ErrInvalidArgument indicates a malformed argument was passed across
	// the FFI boundary (typically an SDK bug or a caller passing bad JSON).
	ErrInvalidArgument

	// ErrInvalidHandle indicates the sandbox handle is stale, closed, or
	// was never valid.
	ErrInvalidHandle

	// ErrBufferTooSmall indicates the FFI response exceeded the fixed
	// output buffer. For file reads, stream instead.
	ErrBufferTooSmall

	// ErrCancelled indicates the operation was cancelled by the caller's
	// context before the Rust runtime completed it.
	ErrCancelled

	// ErrLibraryNotLoaded indicates the microsandbox library has not been
	// loaded. SDK operations load the embedded FFI library automatically.
	ErrLibraryNotLoaded

	// ErrMetricsDisabled indicates metrics sampling is disabled for this sandbox.
	ErrMetricsDisabled

	// ErrMetricsUnavailable indicates metrics have no current sample for this sandbox.
	ErrMetricsUnavailable

	// ErrUnsupportedOperation indicates that the selected backend or sandbox
	// runtime does not provide the requested operation.
	ErrUnsupportedOperation

	// ErrInternal is every other error from the runtime.
	ErrInternal

	// ErrNoDefaultCommand indicates that neither the effective entrypoint nor CMD is executable.
	ErrNoDefaultCommand

	// ErrSandboxReplaced indicates that a receiver's name now refers to a
	// different persisted sandbox identity.
	ErrSandboxReplaced

	// ErrSnapshotSourceRecovery indicates capture succeeded but source recovery failed.
	ErrSnapshotSourceRecovery

	// ErrStopTimeout indicates graceful shutdown exceeded its budget without requesting a kill.
	ErrStopTimeout
	// ErrRuntimeNotInstalled indicates no runtime pair resolves.
	ErrRuntimeNotInstalled
	// ErrRuntimeIncomplete indicates a partial or invalid explicitly selected runtime.
	ErrRuntimeIncomplete
	// ErrSandboxStopTimedOut indicates graceful shutdown was not observed
	// before the SDK's deadline and may still complete asynchronously.
	ErrSandboxStopTimedOut

	// ErrModificationNotConfirmed indicates a Cloud change did not complete as
	// asked: it may be saved without being in effect on the running sandbox, or
	// its result is unknown. The message says whether sending the same change
	// again retries it.
	ErrModificationNotConfirmed
)

func (k ErrorKind) String() string {
	switch k {
	case ErrSandboxNotFound:
		return "SandboxNotFound"
	case ErrSandboxNotRunning:
		return "SandboxNotRunning"
	case ErrSandboxAlreadyExists:
		return "SandboxAlreadyExists"
	case ErrSandboxReplaced:
		return "SandboxReplaced"
	case ErrSandboxStopTimedOut:
		return "SandboxStopTimedOut"
	case ErrSandboxStillRunning:
		return "SandboxStillRunning"
	case ErrVolumeNotFound:
		return "VolumeNotFound"
	case ErrVolumeAlreadyExists:
		return "VolumeAlreadyExists"
	case ErrExecTimeout:
		return "ExecTimeout"
	case ErrStopTimeout:
		return "StopTimeout"
	case ErrExecFailed:
		return "ExecFailed"
	case ErrFilesystem:
		return "Filesystem"
	case ErrPathNotFound:
		return "PathNotFound"
	case ErrImageNotFound:
		return "ImageNotFound"
	case ErrImageInUse:
		return "ImageInUse"
	case ErrImagePullFailed:
		return "ImagePullFailed"
	case ErrSnapshotNotFound:
		return "SnapshotNotFound"
	case ErrSnapshotAlreadyExists:
		return "SnapshotAlreadyExists"
	case ErrSnapshotSandboxRunning:
		return "SnapshotSandboxRunning"
	case ErrSnapshotImageMissing:
		return "SnapshotImageMissing"
	case ErrSnapshotIntegrity:
		return "SnapshotIntegrity"
	case ErrSnapshotMigration:
		return "SnapshotMigration"
	case ErrSnapshotSourceRecovery:
		return "SnapshotSourceRecovery"
	case ErrPatchFailed:
		return "PatchFailed"
	case ErrNetworkPolicy:
		return "NetworkPolicy"
	case ErrSecretViolation:
		return "SecretViolation"
	case ErrTLS:
		return "TLS"
	case ErrIO:
		return "IO"
	case ErrInvalidConfig:
		return "InvalidConfig"
	case ErrInvalidArgument:
		return "InvalidArgument"
	case ErrInvalidHandle:
		return "InvalidHandle"
	case ErrBufferTooSmall:
		return "BufferTooSmall"
	case ErrCancelled:
		return "Cancelled"
	case ErrRuntimeNotInstalled:
		return "RuntimeNotInstalled"
	case ErrRuntimeIncomplete:
		return "RuntimeIncomplete"
	case ErrLibraryNotLoaded:
		return "LibraryNotLoaded"
	case ErrMetricsDisabled:
		return "MetricsDisabled"
	case ErrMetricsUnavailable:
		return "MetricsUnavailable"
	case ErrUnsupportedOperation:
		return "UnsupportedOperation"
	case ErrModificationNotConfirmed:
		return "ModificationNotConfirmed"
	case ErrInternal:
		return "Internal"
	case ErrNoDefaultCommand:
		return "NoDefaultCommand"
	default:
		return "Unknown"
	}
}

// Error is the standard error type returned by microsandbox operations.
// Use errors.As to extract detailed information.
type Error struct {
	Kind    ErrorKind
	Message string
	Cause   error
}

// PublishedSnapshotArtifact names an artifact published despite source recovery failure.
type PublishedSnapshotArtifact struct {
	Kind       string
	Path       string
	SnapshotID string
	Digest     string
}

// SnapshotSourceRecoveryDetails contains recovery locators. Their presence does not
// imply the source is running, safely paused, or eligible for ordinary resume.
type SnapshotSourceRecoveryDetails struct {
	SourceSandbox    string
	CheckpointID     string
	CheckpointRoot   string
	CheckpointPath   string
	Artifact         *PublishedSnapshotArtifact
	Detail           string
	PublicationError *string
}

// SnapshotSourceRecoveryError reports partial capture success without losing recovery metadata.
// Use errors.As to obtain this type; IsKind also recognizes ErrSnapshotSourceRecovery.
type SnapshotSourceRecoveryError struct {
	Recovery SnapshotSourceRecoveryDetails
	err      *Error
}

func (e *SnapshotSourceRecoveryError) Error() string { return e.err.Error() }

// Unwrap preserves access to the standard SDK error kind.
func (e *SnapshotSourceRecoveryError) Unwrap() error { return e.err }

// Error implements the error interface.
//
// The string form deliberately omits the Kind to avoid duplicating the
// category when the underlying message already describes it (e.g. a
// sandbox-not-found kind paired with the runtime message "sandbox not
// found: foo"). Callers that need to branch on the category should use
// IsKind or errors.As against *Error — Kind is still preserved on the
// struct.
func (e *Error) Error() string {
	switch {
	case e.Message == "" && e.Cause != nil:
		return e.Cause.Error()
	case e.Cause != nil:
		return fmt.Sprintf("%s: %v", e.Message, e.Cause)
	case e.Message == "":
		return e.Kind.String()
	default:
		return e.Message
	}
}

// Unwrap supports errors.Is / errors.As.
func (e *Error) Unwrap() error { return e.Cause }

// IsKind reports whether err (or any wrapped error) is a microsandbox.Error
// with the given kind.
func IsKind(err error, kind ErrorKind) bool {
	var e *Error
	if errors.As(err, &e) {
		return e.Kind == kind
	}
	return false
}

// wrapFFI converts an error returned by the internal/ffi package into a
// typed *microsandbox.Error. context.Canceled and context.DeadlineExceeded
// map to ErrCancelled (preserving the original via Cause); other non-ffi
// errors surface as ErrInternal. Returns nil for a nil err.
func wrapFFI(err error) error {
	if err == nil {
		return nil
	}
	var fe *ffi.Error
	if errors.As(err, &fe) {
		if fe.Kind == ffi.KindSnapshotSourceRecovery && fe.Recovery != nil {
			r := fe.Recovery
			recovery := SnapshotSourceRecoveryDetails{
				SourceSandbox: r.SourceSandbox, CheckpointID: r.CheckpointID,
				CheckpointRoot: r.CheckpointRoot, CheckpointPath: r.CheckpointPath,
				Detail: r.Detail, PublicationError: cloneStringPtr(r.PublicationError),
			}
			if a := r.Artifact; a != nil {
				recovery.Artifact = &PublishedSnapshotArtifact{
					Kind: a.Kind, Path: a.Path, SnapshotID: a.SnapshotID, Digest: a.Digest,
				}
			}
			return &SnapshotSourceRecoveryError{
				Recovery: recovery, err: &Error{Kind: ErrSnapshotSourceRecovery, Message: fe.Message},
			}
		}
		return &Error{Kind: kindFromFFI(fe.Kind), Message: fe.Message}
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return &Error{Kind: ErrCancelled, Message: err.Error(), Cause: err}
	}
	return &Error{Kind: ErrInternal, Message: err.Error(), Cause: err}
}

func kindFromFFI(kind string) ErrorKind {
	switch kind {
	case ffi.KindSandboxNotFound:
		return ErrSandboxNotFound
	case ffi.KindSandboxAlreadyExists:
		return ErrSandboxAlreadyExists
	case ffi.KindSandboxReplaced:
		return ErrSandboxReplaced
	case ffi.KindSandboxStopTimedOut:
		return ErrSandboxStopTimedOut
	case ffi.KindSandboxStillRunning:
		return ErrSandboxStillRunning
	case ffi.KindVolumeNotFound:
		return ErrVolumeNotFound
	case ffi.KindVolumeAlreadyExists:
		return ErrVolumeAlreadyExists
	case ffi.KindExecTimeout:
		return ErrExecTimeout
	case ffi.KindStopTimeout:
		return ErrStopTimeout
	case ffi.KindNoDefaultCommand:
		return ErrNoDefaultCommand
	case ffi.KindFilesystem:
		return ErrFilesystem
	case ffi.KindImageNotFound:
		return ErrImageNotFound
	case ffi.KindImageInUse:
		return ErrImageInUse
	case ffi.KindSnapshotNotFound:
		return ErrSnapshotNotFound
	case ffi.KindSnapshotAlreadyExists:
		return ErrSnapshotAlreadyExists
	case ffi.KindSnapshotSandboxRunning:
		return ErrSnapshotSandboxRunning
	case ffi.KindSnapshotImageMissing:
		return ErrSnapshotImageMissing
	case ffi.KindSnapshotIntegrity:
		return ErrSnapshotIntegrity
	case ffi.KindSnapshotMigration:
		return ErrSnapshotMigration
	case ffi.KindSnapshotSourceRecovery:
		return ErrSnapshotSourceRecovery
	case ffi.KindPatchFailed:
		return ErrPatchFailed
	case ffi.KindIO:
		return ErrIO
	case ffi.KindInvalidConfig:
		return ErrInvalidConfig
	case ffi.KindInvalidArgument:
		return ErrInvalidArgument
	case ffi.KindInvalidHandle:
		return ErrInvalidHandle
	case ffi.KindBufferTooSmall:
		return ErrBufferTooSmall
	case ffi.KindCancelled:
		return ErrCancelled
	case "runtime_not_installed":
		return ErrRuntimeNotInstalled
	case "runtime_incomplete":
		return ErrRuntimeIncomplete
	case ffi.KindLibraryNotLoaded:
		return ErrLibraryNotLoaded
	case ffi.KindMetricsDisabled:
		return ErrMetricsDisabled
	case ffi.KindMetricsUnavailable:
		return ErrMetricsUnavailable
	case ffi.KindUnsupportedOperation:
		return ErrUnsupportedOperation
	case ffi.KindModificationNotConfirmed:
		return ErrModificationNotConfirmed
	default:
		return ErrInternal
	}
}
