// Package local provides a local filesystem storage adapter for Tutus.
//
// This adapter is suitable for:
//   - Development and testing
//   - Single-node deployments
//   - NAS or SAN-backed storage
//   - Air-gapped government networks
package local

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"git.marketally.com/tutus-one/tutus-chain/pkg/storage"
)

const providerName = "local"

// Adapter implements the storage.Provider interface for local filesystem.
type Adapter struct {
	basePath string
	maxSize  int64
	mu       sync.RWMutex
}

// Config holds local adapter configuration.
type Config struct {
	// Path is the base directory for storage
	Path string
	// MaxSize is the maximum storage size in bytes (0 = unlimited)
	MaxSize int64
}

// New creates a new local filesystem storage adapter.
func New(cfg Config) (*Adapter, error) {
	path := cfg.Path
	if path == "" {
		path = "./storage"
	}

	// Ensure base directory exists
	if err := os.MkdirAll(path, 0755); err != nil {
		return nil, fmt.Errorf("local: create storage directory: %w", err)
	}

	// Create subdirectories
	for _, sub := range []string{"objects", "meta", "blocks", "states"} {
		if err := os.MkdirAll(filepath.Join(path, sub), 0755); err != nil {
			return nil, fmt.Errorf("local: create %s directory: %w", sub, err)
		}
	}

	return &Adapter{
		basePath: path,
		maxSize:  cfg.MaxSize,
	}, nil
}

// Name returns the provider identifier.
func (a *Adapter) Name() string {
	return providerName
}

// Put stores data on the local filesystem.
func (a *Adapter) Put(ctx context.Context, data io.Reader, opts storage.PutOptions) (storage.ObjectID, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	// Read all data
	content, err := io.ReadAll(data)
	if err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: read data: %w", err)
	}

	// Generate content-addressed ID
	hash := sha256.Sum256(content)
	id := hex.EncodeToString(hash[:])

	// Check size limits
	if a.maxSize > 0 {
		currentSize, _ := a.calculateSize()
		if currentSize+int64(len(content)) > a.maxSize {
			return storage.ObjectID{}, storage.ErrQuotaExceeded
		}
	}

	// Write object data
	objPath := a.objectPath(id)
	if err := os.MkdirAll(filepath.Dir(objPath), 0755); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: create object dir: %w", err)
	}

	if err := os.WriteFile(objPath, content, 0644); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: write object: %w", err)
	}

	// Write metadata
	meta := objectMeta{
		Size:        int64(len(content)),
		ContentType: opts.ContentType,
		Created:     time.Now(),
		Modified:    time.Now(),
		Checksum:    id,
		Attributes:  opts.Attributes,
	}

	metaPath := a.metaPath(id)
	if err := os.MkdirAll(filepath.Dir(metaPath), 0755); err != nil {
		os.Remove(objPath) // Clean up on error
		return storage.ObjectID{}, fmt.Errorf("local: create meta dir: %w", err)
	}
	metaData, _ := json.Marshal(meta)
	if err := os.WriteFile(metaPath, metaData, 0644); err != nil {
		os.Remove(objPath) // Clean up on error
		return storage.ObjectID{}, fmt.Errorf("local: write metadata: %w", err)
	}

	return storage.ObjectID{
		Provider:  providerName,
		Container: "default",
		ID:        id,
	}, nil
}

// Get retrieves data from the local filesystem.
func (a *Adapter) Get(ctx context.Context, id storage.ObjectID) (io.ReadCloser, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	objPath := a.objectPath(id.ID)
	file, err := os.Open(objPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf("local: open object: %w", err)
	}

	return file, nil
}

// Delete removes an object from the local filesystem.
func (a *Adapter) Delete(ctx context.Context, id storage.ObjectID) error {
	a.mu.Lock()
	defer a.mu.Unlock()

	objPath := a.objectPath(id.ID)
	metaPath := a.metaPath(id.ID)

	if _, err := os.Stat(objPath); os.IsNotExist(err) {
		return storage.ErrNotFound
	}

	if err := os.Remove(objPath); err != nil {
		return fmt.Errorf("local: remove object: %w", err)
	}

	os.Remove(metaPath) // Best effort metadata removal

	return nil
}

// Exists checks if an object exists on the local filesystem.
func (a *Adapter) Exists(ctx context.Context, id storage.ObjectID) (bool, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	objPath := a.objectPath(id.ID)
	_, err := os.Stat(objPath)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("local: stat object: %w", err)
	}
	return true, nil
}

// List returns objects matching the given prefix.
func (a *Adapter) List(ctx context.Context, prefix string, opts storage.ListOptions) ([]storage.ObjectInfo, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	objectsDir := filepath.Join(a.basePath, "objects")
	var results []storage.ObjectInfo

	err := filepath.Walk(objectsDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // Skip errors
		}
		if info.IsDir() {
			return nil
		}

		// Get relative path as ID
		rel, _ := filepath.Rel(objectsDir, path)
		id := strings.ReplaceAll(rel, string(filepath.Separator), "")

		if prefix != "" && !strings.HasPrefix(id, prefix) {
			return nil
		}

		objInfo := storage.ObjectInfo{
			ID: storage.ObjectID{
				Provider:  providerName,
				Container: "default",
				ID:        id,
			},
			Size:     info.Size(),
			Modified: info.ModTime(),
		}

		if opts.IncludeMetadata {
			if meta, err := a.loadMeta(id); err == nil {
				objInfo.ContentType = meta.ContentType
				objInfo.Created = meta.Created
				objInfo.Checksum = meta.Checksum
				objInfo.Attributes = meta.Attributes
			}
		}

		results = append(results, objInfo)

		if opts.MaxResults > 0 && len(results) >= opts.MaxResults {
			return filepath.SkipAll
		}
		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("local: list objects: %w", err)
	}

	return results, nil
}

// Head retrieves object metadata.
func (a *Adapter) Head(ctx context.Context, id storage.ObjectID) (storage.ObjectInfo, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	objPath := a.objectPath(id.ID)
	info, err := os.Stat(objPath)
	if err != nil {
		if os.IsNotExist(err) {
			return storage.ObjectInfo{}, storage.ErrNotFound
		}
		return storage.ObjectInfo{}, fmt.Errorf("local: stat object: %w", err)
	}

	result := storage.ObjectInfo{
		ID:       id,
		Size:     info.Size(),
		Modified: info.ModTime(),
	}

	if meta, err := a.loadMeta(id.ID); err == nil {
		result.ContentType = meta.ContentType
		result.Created = meta.Created
		result.Checksum = meta.Checksum
		result.Attributes = meta.Attributes
	}

	return result, nil
}

// Close is a no-op for local storage.
func (a *Adapter) Close() error {
	return nil
}

// =============================================================================
// BlockStorage Interface Implementation
// =============================================================================

// PutBlock stores a block with its index.
func (a *Adapter) PutBlock(ctx context.Context, index uint32, data []byte) (storage.ObjectID, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	// Check size limits
	if a.maxSize > 0 {
		currentSize, _ := a.calculateSize()
		if currentSize+int64(len(data)) > a.maxSize {
			return storage.ObjectID{}, storage.ErrQuotaExceeded
		}
	}

	// Write block file
	blockPath := a.blockPath(index)
	if err := os.MkdirAll(filepath.Dir(blockPath), 0755); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: create block dir: %w", err)
	}

	if err := os.WriteFile(blockPath, data, 0644); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: write block: %w", err)
	}

	// Generate content hash for ID
	hash := sha256.Sum256(data)
	id := hex.EncodeToString(hash[:])

	return storage.ObjectID{
		Provider:  providerName,
		Container: "blocks",
		ID:        id,
	}, nil
}

// GetBlock retrieves a block by its index.
func (a *Adapter) GetBlock(ctx context.Context, index uint32) ([]byte, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	blockPath := a.blockPath(index)
	data, err := os.ReadFile(blockPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf("local: read block: %w", err)
	}

	return data, nil
}

// GetBlockRange retrieves multiple blocks efficiently.
func (a *Adapter) GetBlockRange(ctx context.Context, start, end uint32) ([][]byte, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	if start > end {
		return nil, fmt.Errorf("local: invalid block range: start %d > end %d", start, end)
	}

	blocks := make([][]byte, 0, end-start+1)
	for i := start; i <= end; i++ {
		blockPath := a.blockPath(i)
		data, err := os.ReadFile(blockPath)
		if err != nil {
			if os.IsNotExist(err) {
				return nil, storage.ErrNotFound
			}
			return nil, fmt.Errorf("local: read block %d: %w", i, err)
		}
		blocks = append(blocks, data)
	}

	return blocks, nil
}

// GetLatestBlockIndex returns the highest block index in storage.
func (a *Adapter) GetLatestBlockIndex(ctx context.Context) (uint32, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	blocksDir := filepath.Join(a.basePath, "blocks")
	var latest uint32
	var found bool

	err := filepath.Walk(blocksDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // Skip errors
		}
		if info.IsDir() {
			return nil
		}

		// Parse block index from filename
		name := info.Name()
		if !strings.HasSuffix(name, ".block") {
			return nil
		}

		var index uint32
		if _, err := fmt.Sscanf(name, "%d.block", &index); err != nil {
			return nil
		}

		if !found || index > latest {
			latest = index
			found = true
		}
		return nil
	})

	if err != nil {
		return 0, fmt.Errorf("local: scan blocks: %w", err)
	}

	if !found {
		return 0, storage.ErrNotFound
	}

	return latest, nil
}

// =============================================================================
// StateStorage Interface Implementation
// =============================================================================

// PutState stores a state snapshot at a specific height.
func (a *Adapter) PutState(ctx context.Context, height uint32, data io.Reader) (storage.ObjectID, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	// Read all data
	content, err := io.ReadAll(data)
	if err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: read state data: %w", err)
	}

	// Check size limits
	if a.maxSize > 0 {
		currentSize, _ := a.calculateSize()
		if currentSize+int64(len(content)) > a.maxSize {
			return storage.ObjectID{}, storage.ErrQuotaExceeded
		}
	}

	// Write state file
	statePath := a.statePath(height)
	if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: create state dir: %w", err)
	}

	if err := os.WriteFile(statePath, content, 0644); err != nil {
		return storage.ObjectID{}, fmt.Errorf("local: write state: %w", err)
	}

	// Generate content hash for ID
	hash := sha256.Sum256(content)
	id := hex.EncodeToString(hash[:])

	return storage.ObjectID{
		Provider:  providerName,
		Container: "states",
		ID:        id,
	}, nil
}

// GetState retrieves the state snapshot for a given height.
func (a *Adapter) GetState(ctx context.Context, height uint32) (io.ReadCloser, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	statePath := a.statePath(height)
	file, err := os.Open(statePath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf("local: open state: %w", err)
	}

	return file, nil
}

// GetLatestState returns the most recent state snapshot.
func (a *Adapter) GetLatestState(ctx context.Context) (uint32, io.ReadCloser, error) {
	a.mu.RLock()
	defer a.mu.RUnlock()

	statesDir := filepath.Join(a.basePath, "states")
	var latest uint32
	var found bool

	err := filepath.Walk(statesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // Skip errors
		}
		if info.IsDir() {
			return nil
		}

		// Parse height from filename
		name := info.Name()
		if !strings.HasSuffix(name, ".state") {
			return nil
		}

		var height uint32
		if _, err := fmt.Sscanf(name, "%d.state", &height); err != nil {
			return nil
		}

		if !found || height > latest {
			latest = height
			found = true
		}
		return nil
	})

	if err != nil {
		return 0, nil, fmt.Errorf("local: scan states: %w", err)
	}

	if !found {
		return 0, nil, storage.ErrNotFound
	}

	// Open the latest state file
	statePath := a.statePath(latest)
	file, err := os.Open(statePath)
	if err != nil {
		return 0, nil, fmt.Errorf("local: open latest state: %w", err)
	}

	return latest, file, nil
}

// objectPath returns the filesystem path for an object.
func (a *Adapter) objectPath(id string) string {
	// Use first 2 chars as subdirectory for better filesystem performance
	if len(id) >= 2 {
		return filepath.Join(a.basePath, "objects", id[:2], id)
	}
	return filepath.Join(a.basePath, "objects", id)
}

// metaPath returns the filesystem path for object metadata.
func (a *Adapter) metaPath(id string) string {
	if len(id) >= 2 {
		return filepath.Join(a.basePath, "meta", id[:2], id+".json")
	}
	return filepath.Join(a.basePath, "meta", id+".json")
}

// blockPath returns the filesystem path for a block by index.
func (a *Adapter) blockPath(index uint32) string {
	// Use first 4 digits as subdirectory for better filesystem performance
	// e.g., block 12345 -> blocks/0001/12345.block
	subdir := fmt.Sprintf("%04d", index/10000)
	return filepath.Join(a.basePath, "blocks", subdir, fmt.Sprintf("%d.block", index))
}

// statePath returns the filesystem path for a state snapshot by height.
func (a *Adapter) statePath(height uint32) string {
	// Use first 4 digits as subdirectory for better filesystem performance
	// e.g., state 12345 -> states/0001/12345.state
	subdir := fmt.Sprintf("%04d", height/10000)
	return filepath.Join(a.basePath, "states", subdir, fmt.Sprintf("%d.state", height))
}

// objectMeta holds object metadata.
type objectMeta struct {
	Size        int64             `json:"size"`
	ContentType string            `json:"content_type,omitempty"`
	Created     time.Time         `json:"created"`
	Modified    time.Time         `json:"modified"`
	Checksum    string            `json:"checksum"`
	Attributes  map[string]string `json:"attributes,omitempty"`
}

// loadMeta loads object metadata from disk.
func (a *Adapter) loadMeta(id string) (objectMeta, error) {
	metaPath := a.metaPath(id)
	data, err := os.ReadFile(metaPath)
	if err != nil {
		return objectMeta{}, err
	}
	var meta objectMeta
	err = json.Unmarshal(data, &meta)
	return meta, err
}

// calculateSize returns the total size of all stored data.
func (a *Adapter) calculateSize() (int64, error) {
	var total int64

	// Calculate size across all storage directories
	for _, subdir := range []string{"objects", "blocks", "states"} {
		dir := filepath.Join(a.basePath, subdir)
		err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			if !info.IsDir() {
				total += info.Size()
			}
			return nil
		})
		if err != nil {
			return total, err
		}
	}

	return total, nil
}
