package sim

import (
	"errors"
	"math/rand"
	"sync"
	"time"

	"koti.casa/numenor-labs/dsfx/internal/lib/disk"
	"koti.casa/numenor-labs/dsfx/internal/lib/system"
)

// SimSystem is a simulated implementation of system.System.
// It allows the caller to set parameters for latency and failure chance (from tol)
// and provides in–memory values for command–line arguments, environment variables,
// and standard output/error.
type SimSystem struct {
	// Simulation tolerance parameters (latency, failure chance, corruption chance).
	tol Tolerance

	// Simulated command–line arguments.
	args []string

	// In–memory environment variables. Protected by mu.
	mu  sync.Mutex
	env map[string]string

	// Simulated directory paths.
	homeDir   string
	configDir string
	cacheDir  string
	tempDir   string

	// Simulated standard output and error.
	stdout disk.File
	stderr disk.File
}

// NewSimSystem returns a new simulated system that implements system.System.
// The caller provides a tolerance value and a slice of command–line arguments.
// Other fields (directories, environment) are pre–populated for simulation purposes.
func NewSimSystem(tol Tolerance, args []string) system.System {
	s := &SimSystem{
		tol:       tol,
		args:      args,
		env:       make(map[string]string),
		homeDir:   "/home/simuser",
		configDir: "/home/simuser/.config",
		cacheDir:  "/home/simuser/.cache",
		tempDir:   "/tmp",
	}
	// Create simulated stdout and stderr.
	s.stdout = newSimOutput("stdout", tol)
	s.stderr = newSimOutput("stderr", tol)
	return s
}

// simulateOp applies the configured latency and possibly simulates an operation failure.
func (s *SimSystem) simulateOp() error {
	time.Sleep(s.tol.Latency)
	if rand.Float64() < s.tol.FailureChance {
		return errors.New("simulated system operation failure")
	}
	return nil
}

// Args returns the simulated command–line arguments (skipping the program name).
func (s *SimSystem) Args() []string {
	// Simulate latency even for argument access.
	_ = s.simulateOp()
	// Return a copy so that callers cannot modify the underlying slice.
	cpy := make([]string, len(s.args))
	copy(cpy, s.args)
	return cpy
}

// Arg returns the simulated command–line argument at index i.
// If the index is out–of–range, it returns an empty string.
func (s *SimSystem) Arg(i int) string {
	_ = s.simulateOp()
	if i < 0 || i >= len(s.args) {
		return ""
	}
	return s.args[i]
}

// UserHomeDir returns the simulated home directory.
func (s *SimSystem) UserHomeDir() (string, error) {
	if err := s.simulateOp(); err != nil {
		return "", err
	}
	return s.homeDir, nil
}

// UserConfigDir returns the simulated configuration directory.
func (s *SimSystem) UserConfigDir() (string, error) {
	if err := s.simulateOp(); err != nil {
		return "", err
	}
	return s.configDir, nil
}

// UserCacheDir returns the simulated cache directory.
func (s *SimSystem) UserCacheDir() (string, error) {
	if err := s.simulateOp(); err != nil {
		return "", err
	}
	return s.cacheDir, nil
}

// TempDir returns the simulated temporary directory.
func (s *SimSystem) TempDir() string {
	// We simulate latency even though TempDir cannot fail.
	_ = s.simulateOp()
	return s.tempDir
}

// Stdout returns a simulated disk.File representing standard output.
func (s *SimSystem) Stdout() disk.File {
	// In a real simulation you might add latency/failure to writes on stdout.
	_ = s.simulateOp()
	return s.stdout
}

// Stderr returns a simulated disk.File representing standard error.
func (s *SimSystem) Stderr() disk.File {
	_ = s.simulateOp()
	return s.stderr
}

// Exit simulates terminating the program with the given exit code.
// As with many tests, we simulate exit by panicing with a special error.
// This allows tests to catch the panic and inspect the exit code.
func (s *SimSystem) Exit(code int) {
	_ = s.simulateOp()
	panic(&SimExitError{Code: code})
}

// GetEnv retrieves the simulated value for the environment variable named by key.
func (s *SimSystem) GetEnv(key string) string {
	_ = s.simulateOp()
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.env[key]
}

// SetEnv sets the simulated environment variable named by key to value.
func (s *SimSystem) SetEnv(key, value string) error {
	_ = s.simulateOp()
	s.mu.Lock()
	defer s.mu.Unlock()
	s.env[key] = value
	return nil
}

// SimExitError is the error value used to simulate a program exit.
type SimExitError struct {
	Code int
}

// Error implements the error interface.
func (e *SimExitError) Error() string {
	return "simulated program exit"
}

// --------------------------------------------------------------------------
// Helper functions and types for simulated standard output/error
// --------------------------------------------------------------------------

// newSimOutput creates a new simulated disk.File that acts as an output.
// Internally it creates a simulated file with an in–memory entry.
func newSimOutput(name string, tol Tolerance) disk.File {
	entry := &simEntry{
		name:    name,
		isDir:   false,
		perm:    0644,
		modTime: time.Now(),
		data:    []byte{},
	}
	// Return a simFile which implements disk.File.
	// (Note: simFile and simEntry are the types defined in dsfx/sim/disk.go.)
	return &simFile{
		entry:    entry,
		offset:   0,
		readOnly: false,
		tol:      tol,
	}
}

/*
Usage example:

   // Define a tolerance for simulation.
   tol := sim.Tolerance{
       Latency:         10 * time.Millisecond,
       FailureChance:   0.01,
       CorruptionChance: 0.005,
   }

   // Simulate a system with custom command–line arguments.
   sys := sim.NewSimSystem(tol, []string{"--verbose", "--config=sim.conf"})

   // Calling system methods:
   args := sys.Args()
   home, err := sys.UserHomeDir()
   // ... etc.

   // To simulate an exit:
   func run() {
       defer func() {
           if r := recover(); r != nil {
               if exitErr, ok := r.(*sim.SimExitError); ok {
                   fmt.Printf("simulated exit(%d)\n", exitErr.Code)
               }
           }
       }()
       sys.Exit(3)
   }
*/