package sim

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

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

// Tolerance defines simulation tolerance parameters.
type Tolerance struct {
	// Latency to wait before executing an operation.
	Latency time.Duration
	// FailureChance is the probability (0.0–1.0) that an operation fails.
	FailureChance float64
	// CorruptionChance is the probability (0.0–1.0) that a write is corrupted.
	CorruptionChance float64
}

// simEntry represents an entry in our in–memory file system.
type simEntry struct {
	name    string
	isDir   bool
	perm    fs.FileMode
	modTime time.Time

	// For files, data holds the file contents.
	data []byte
	mu   sync.Mutex
}

// simFile is a simulated file object. It implements fs.File and io.Writer.
// The same underlying simEntry is shared among all handles to a given file.
type simFile struct {
	entry *simEntry
	// offset to simulate reading sequentially.
	offset int
	// readOnly indicates whether writes are allowed.
	readOnly bool
	// closed is set once Close() has been called.
	closed bool
	// You could add a mutex here if you want to protect concurrent access per handle.
	mu sync.Mutex

	// tol copied from the SimDisk for per–file simulation.
	tol Tolerance
}

// Ensure simFile implements the interfaces.
var _ disk.File = (*simFile)(nil)

// Read reads from the simulated file starting at the current offset.
func (sf *simFile) Read(p []byte) (int, error) {
	sf.mu.Lock()
	defer sf.mu.Unlock()

	if sf.closed {
		return 0, errors.New("read from closed file")
	}

	// Lock the underlying data.
	sf.entry.mu.Lock()
	defer sf.entry.mu.Unlock()

	if sf.offset >= len(sf.entry.data) {
		return 0, io.EOF
	}
	n := copy(p, sf.entry.data[sf.offset:])
	sf.offset += n
	return n, nil
}

// Write writes the given bytes at the end of the file (for a writable file).
// It may simulate corruption.
func (sf *simFile) Write(p []byte) (int, error) {
	if sf.readOnly {
		return 0, errors.New("cannot write to read-only file")
	}
	sf.mu.Lock()
	defer sf.mu.Unlock()

	if sf.closed {
		return 0, errors.New("write to closed file")
	}

	// simulate latency
	time.Sleep(sf.tol.Latency)
	// simulate failure
	if rand.Float64() < sf.tol.FailureChance {
		return 0, errors.New("simulated write failure")
	}

	// prepare data to write
	dataToWrite := make([]byte, len(p))
	copy(dataToWrite, p)

	// simulate corruption by flipping bits in the data (if triggered)
	if rand.Float64() < sf.tol.CorruptionChance {
		for i := range dataToWrite {
			dataToWrite[i] = ^dataToWrite[i] // simple corruption: bitwise complement
		}
	}

	// lock the underlying entry and append data
	sf.entry.mu.Lock()
	defer sf.entry.mu.Unlock()
	sf.entry.data = append(sf.entry.data, dataToWrite...)
	// update modification time
	sf.entry.modTime = time.Now()
	return len(p), nil
}

// Close marks the file as closed.
func (sf *simFile) Close() error {
	sf.mu.Lock()
	defer sf.mu.Unlock()
	if sf.closed {
		return errors.New("file already closed")
	}
	sf.closed = true
	return nil
}

// Stat returns file information for the simulated file.
func (sf *simFile) Stat() (fs.FileInfo, error) {
	sf.mu.Lock()
	defer sf.mu.Unlock()
	if sf.closed {
		return nil, errors.New("stat on closed file")
	}
	return &simFileInfo{entry: sf.entry}, nil
}

// simFileInfo implements fs.FileInfo for a simEntry.
type simFileInfo struct {
	entry *simEntry
}

var _ fs.FileInfo = (*simFileInfo)(nil)

func (fi *simFileInfo) Name() string { return fi.entry.name }
func (fi *simFileInfo) Size() int64 {
	fi.entry.mu.Lock()
	defer fi.entry.mu.Unlock()
	return int64(len(fi.entry.data))
}
func (fi *simFileInfo) Mode() fs.FileMode  { return fi.entry.perm }
func (fi *simFileInfo) ModTime() time.Time { return fi.entry.modTime }
func (fi *simFileInfo) IsDir() bool        { return fi.entry.isDir }
func (fi *simFileInfo) Sys() interface{}   { return nil }

// SimDisk is our in–memory simulation that implements the disk.Disk interface.
type SimDisk struct {
	// tol holds the tolerance parameters to simulate latency, failures, corruption.
	tol Tolerance

	// entries is a map from file or directory name to its corresponding simEntry.
	entries map[string]*simEntry

	mu sync.Mutex
}

// NewSimDisk returns a new simulated disk with the given tolerance parameters.
func NewSimDisk(tol Tolerance) disk.Disk {
	return &SimDisk{
		tol:     tol,
		entries: make(map[string]*simEntry),
	}
}

// simulateOp sleeps for the configured latency and then returns an error if a failure is simulated.
func (sd *SimDisk) simulateOp() error {
	time.Sleep(sd.tol.Latency)
	if rand.Float64() < sd.tol.FailureChance {
		return errors.New("simulated disk operation failure")
	}
	return nil
}

// Mkdir creates a directory in the simulated file system.
func (sd *SimDisk) Mkdir(name string, perm fs.FileMode) error {
	if err := sd.simulateOp(); err != nil {
		return err
	}

	sd.mu.Lock()
	defer sd.mu.Unlock()

	if _, exists := sd.entries[name]; exists {
		return errors.New("directory already exists")
	}
	sd.entries[name] = &simEntry{
		name:    name,
		isDir:   true,
		perm:    perm,
		modTime: time.Now(),
	}
	return nil
}

// MkdirAll creates a directory and any necessary parent directories in the simulated file system.
// It behaves like os.MkdirAll.
// If the directory already exists, it does nothing.
func (sd *SimDisk) MkdirAll(name string, perm fs.FileMode) error {
	if err := sd.simulateOp(); err != nil {
		return err
	}

	sd.mu.Lock()
	defer sd.mu.Unlock()

	if _, exists := sd.entries[name]; exists {
		return errors.New("directory already exists")
	}
	sd.entries[name] = &simEntry{
		name:    name,
		isDir:   true,
		perm:    perm,
		modTime: time.Now(),
	}
	return nil
}

// Create creates (or truncates) a file in the simulated file system and returns a writable file.
func (sd *SimDisk) Create(name string) (disk.File, error) {
	if err := sd.simulateOp(); err != nil {
		return nil, err
	}

	sd.mu.Lock()
	// For simplicity, Create always truncates (or creates new)
	se := &simEntry{
		name:    name,
		isDir:   false,
		perm:    0644,
		modTime: time.Now(),
		data:    []byte{},
	}
	sd.entries[name] = se
	sd.mu.Unlock()

	// Return a writable file handle
	return &simFile{
		entry:    se,
		offset:   0,
		readOnly: false,
		tol:      sd.tol,
	}, nil
}

// Open opens an existing file in the simulated file system.
// The returned file is "read-only" (write attempts will error),
func (sd *SimDisk) Open(name string) (disk.File, error) {
	if err := sd.simulateOp(); err != nil {
		return nil, err
	}

	sd.mu.Lock()
	se, exists := sd.entries[name]
	sd.mu.Unlock()
	if !exists {
		return nil, errors.New("file does not exist")
	}
	if se.isDir {
		return nil, errors.New("cannot open directory")
	}

	// Return a new file handle starting at offset 0; marked as readOnly.
	return &simFile{
		entry:    se,
		offset:   0,
		readOnly: true,
		tol:      sd.tol,
	}, nil
}

// Stat returns information about the file or directory.
func (sd *SimDisk) Stat(name string) (fs.FileInfo, error) {
	if err := sd.simulateOp(); err != nil {
		return nil, err
	}

	sd.mu.Lock()
	se, exists := sd.entries[name]
	sd.mu.Unlock()
	if !exists {
		return nil, errors.New("no such file or directory")
	}

	return &simFileInfo{entry: se}, nil
}

// Remove deletes a file or directory from the simulated file system.
func (sd *SimDisk) Remove(name string) error {
	if err := sd.simulateOp(); err != nil {
		return err
	}

	sd.mu.Lock()
	defer sd.mu.Unlock()
	if _, exists := sd.entries[name]; !exists {
		return errors.New("file or directory does not exist")
	}
	delete(sd.entries, name)
	return nil
}