package main

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
)

// section: code
// Modern best practices for sentinel errors and error checking

// ============================================
// PATTERN 1: Exported Error Variables (Traditional)
// ============================================
// Use for well-known error conditions in public APIs

var (
	// ErrNotFound indicates a resource was not found
	ErrNotFound = errors.New("not found")

	// ErrInvalidInput indicates invalid input was provided
	ErrInvalidInput = errors.New("invalid input")

	// ErrTimeout indicates an operation timed out
	ErrTimeout = errors.New("operation timed out")
)

// ============================================
// PATTERN 2: Unexported Error Types (RECOMMENDED)
// ============================================
// Keep error types unexported to maintain API flexibility
// Use predicate functions to check for error conditions

// notFoundError is unexported - implementation detail
type notFoundError struct {
	Resource string
	ID       string
}

func (e *notFoundError) Error() string {
	return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}

// IMPORTANT: No Is() method on the error type!
// Adding Is() to the type makes it part of your API contract forever.
// You can't remove it without breaking changes.

// ============================================
// PATTERN 3: Predicate Functions (BEST PRACTICE)
// ============================================
// Predicate functions give you flexibility to change implementation

// IsNotFound checks if an error represents a "not found" condition
// This is the public API - you can change the implementation anytime
func IsNotFound(err error) bool {
	// Check for sentinel error
	if errors.Is(err, ErrNotFound) {
		return true
	}
	// Check for unexported custom type using errors.As
	var nfe *notFoundError
	return errors.As(err, &nfe)
}

// IsTimeout checks if an error represents a timeout condition
func IsTimeout(err error) bool {
	return errors.Is(err, ErrTimeout)
}

// IsTemporary checks if an error is temporary (like network errors)
// This follows the pattern used by net.Error
type temporary interface {
	Temporary() bool
}

func IsTemporary(err error) bool {
	var t temporary
	return errors.As(err, &t) && t.Temporary()
}

// ============================================
// PATTERN 4: Wrapping Sentinel Errors
// ============================================

// findUser simulates a database lookup
func findUser(id string) error {
	// Simulate not found - return unexported error type
	if id == "" {
		return &notFoundError{Resource: "user", ID: id}
	}
	return nil
}

// getUserProfile demonstrates proper error wrapping with sentinels
func getUserProfile(userID string) error {
	err := findUser(userID)
	if err != nil {
		// Wrap the error while preserving the sentinel
		return fmt.Errorf("failed to get user profile: %w", err)
	}
	return nil
}

// ============================================
// PATTERN 5: Constant Errors
// ============================================
// For truly immutable error values

type constError string

func (e constError) Error() string {
	return string(e)
}

const (
	ErrInvalidFormat constError = "invalid format"
	ErrOutOfRange    constError = "value out of range"
)

// ============================================
// EXAMPLES
// ============================================

func demonstrateSentinelErrors() {
	fmt.Println("=== Sentinel Error Patterns ===\n")

	// Example 1: Traditional sentinel error
	fmt.Println("1. Traditional Sentinel Error:")
	err1 := ErrNotFound
	if errors.Is(err1, ErrNotFound) {
		fmt.Printf("   ✓ Matched sentinel error: %v\n", err1)
	}

	// Example 2: Wrapped sentinel error
	fmt.Println("\n2. Wrapped Sentinel Error:")
	err2 := fmt.Errorf("operation failed: %w", ErrTimeout)
	if errors.Is(err2, ErrTimeout) {
		fmt.Printf("   ✓ Matched wrapped sentinel: %v\n", err2)
	}

	// Example 3: Unexported custom error type via predicate
	fmt.Println("\n3. Unexported Custom Error Type:")
	err3 := &notFoundError{Resource: "user", ID: "123"}
	if IsNotFound(err3) {
		fmt.Printf("   ✓ Matched via predicate function: %v\n", err3)
	}

	// Example 4: Wrapped custom error
	fmt.Println("\n4. Wrapped Custom Error:")
	err4 := getUserProfile("")
	if IsNotFound(err4) {
		fmt.Printf("   ✓ Matched wrapped custom error: %v\n", err4)
	}

	// Example 5: Standard library errors
	fmt.Println("\n5. Standard Library Errors:")
	err5 := io.EOF
	if errors.Is(err5, io.EOF) {
		fmt.Printf("   ✓ Matched io.EOF: %v\n", err5)
	}

	err6 := fs.ErrNotExist
	if errors.Is(err6, fs.ErrNotExist) {
		fmt.Printf("   ✓ Matched fs.ErrNotExist: %v\n", err6)
	}

	// Example 6: Constant errors
	fmt.Println("\n6. Constant Errors:")
	err7 := ErrInvalidFormat
	if err7 == ErrInvalidFormat {
		fmt.Printf("   ✓ Matched constant error: %v\n", err7)
	}
}

// ============================================
// BEST PRACTICES SUMMARY
// ============================================

func main() {
	demonstrateSentinelErrors()

	fmt.Println("\n=== Best Practices ===")
	fmt.Println("✓ Use predicate functions (IsNotFound) instead of exposing sentinels")
	fmt.Println("✓ Keep error types UNEXPORTED to maintain API flexibility")
	fmt.Println("✓ Always wrap errors with %w to preserve error chains")
	fmt.Println("✓ Use errors.Is() for sentinel error comparison")
	fmt.Println("✓ Use errors.As() inside predicate functions for type checking")
	fmt.Println("✓ Consider constant errors for truly immutable values")
	fmt.Println("✗ NEVER add Is() method to error types (locks you into the API forever)")
	fmt.Println("✗ NEVER export error types (prevents deprecation and changes)")
	fmt.Println("✗ Avoid direct equality checks (err == ErrNotFound) on wrapped errors")
}

// section: code

/*
// section: output
=== Sentinel Error Patterns ===

1. Traditional Sentinel Error:
   ✓ Matched sentinel error: not found

2. Wrapped Sentinel Error:
   ✓ Matched wrapped sentinel: operation failed: operation timed out

3. Unexported Custom Error Type:
   ✓ Matched via predicate function: user not found: 123

4. Wrapped Custom Error:
   ✓ Matched wrapped custom error: failed to get user profile: user not found:

5. Standard Library Errors:
   ✓ Matched io.EOF: EOF
   ✓ Matched fs.ErrNotExist: file does not exist

6. Constant Errors:
   ✓ Matched constant error: invalid format

=== Best Practices ===
✓ Use predicate functions (IsNotFound) instead of exposing sentinels
✓ Keep error types UNEXPORTED to maintain API flexibility
✓ Always wrap errors with %w to preserve error chains
✓ Use errors.Is() for sentinel error comparison
✓ Use errors.As() inside predicate functions for type checking
✓ Consider constant errors for truly immutable values
✗ NEVER add Is() method to error types (locks you into the API forever)
✗ NEVER export error types (prevents deprecation and changes)
✗ Avoid direct equality checks (err == ErrNotFound) on wrapped errors
// section: output
*/
