// Package filesystem implements the local-disk Storage backend used for
// development and single-node deployments.
package filesystem

import (
	"context"
	"crypto/md5" // #nosec G501 -- MD5 used for file checksums, not cryptographic security
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/lukaszraczylo/gohoarder/pkg/errors"
	"github.com/lukaszraczylo/gohoarder/pkg/metrics"
	"github.com/lukaszraczylo/gohoarder/pkg/storage"
	"github.com/rs/zerolog/log"
)

// FilesystemStorage implements storage.StorageBackend for local filesystem
type FilesystemStorage struct {
	basePath string
	quota    int64
	mu       sync.RWMutex
	used     int64
}

// New creates a new filesystem storage backend
func New(basePath string, quota int64) (*FilesystemStorage, error) {
	// Create base directory if it doesn't exist
	if err := os.MkdirAll(basePath, 0750); err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create base directory")
	}

	fs := &FilesystemStorage{
		basePath: basePath,
		quota:    quota,
	}

	// Calculate initial usage
	if err := fs.calculateUsage(); err != nil {
		log.Warn().Err(err).Msg("Failed to calculate initial storage usage")
	}

	return fs, nil
}

// Get retrieves a file
func (fs *FilesystemStorage) Get(ctx context.Context, key string) (io.ReadCloser, error) {
	// Check context
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		metrics.RecordStorageOperation("filesystem", "get", "error")
		return nil, err
	}

	file, err := os.Open(path) // #nosec G304 -- Path is sanitized storage key
	if err != nil {
		if os.IsNotExist(err) {
			metrics.RecordStorageOperation("filesystem", "get", "not_found")
			return nil, errors.NotFound(fmt.Sprintf("file not found: %s", key))
		}
		metrics.RecordStorageOperation("filesystem", "get", "error")
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to open file")
	}

	metrics.RecordStorageOperation("filesystem", "get", "success")
	return file, nil
}

// Put stores a file atomically
func (fs *FilesystemStorage) Put(ctx context.Context, key string, data io.Reader, opts *storage.PutOptions) error {
	// Check context
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return err
	}
	dir := filepath.Dir(path)

	// Create directory
	if mkErr := os.MkdirAll(dir, 0750); mkErr != nil {
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return errors.Wrap(mkErr, errors.ErrCodeStorageFailure, "failed to create directory")
	}

	// Create temp file for atomic write
	tempPath := path + ".tmp"
	tempFile, err := os.Create(tempPath) // #nosec G304 -- Temp path is constructed from sanitized storage key
	if err != nil {
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create temp file")
	}

	// Calculate checksums while writing
	// NOTE: MD5 is used for integrity verification (checksums), not cryptographic security
	md5Hash := md5.New() // #nosec G401 -- MD5 used for file integrity check, not cryptographic security
	sha256Hash := sha256.New()
	multiWriter := io.MultiWriter(tempFile, md5Hash, sha256Hash)

	written, err := io.Copy(multiWriter, data)
	if err != nil {
		_ = tempFile.Close()    // #nosec G104 -- Cleanup, error not critical
		_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to write data")
	}

	if err := tempFile.Close(); err != nil {
		_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to close temp file")
	}

	// Verify checksums if provided
	if opts != nil {
		md5Sum := hex.EncodeToString(md5Hash.Sum(nil))
		sha256Sum := hex.EncodeToString(sha256Hash.Sum(nil))

		if opts.ChecksumMD5 != "" && opts.ChecksumMD5 != md5Sum {
			_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
			metrics.RecordStorageOperation("filesystem", "put", "checksum_error")
			return errors.New(errors.ErrCodeChecksumMismatch, "MD5 checksum mismatch")
		}

		if opts.ChecksumSHA256 != "" && opts.ChecksumSHA256 != sha256Sum {
			_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
			metrics.RecordStorageOperation("filesystem", "put", "checksum_error")
			return errors.New(errors.ErrCodeChecksumMismatch, "SHA256 checksum mismatch")
		}
	}

	// Atomic rename and quota update under lock so that fs.used reflects
	// only successfully renamed files. Quota check happens before increment
	// to avoid transient inflation seen by concurrent Puts.
	fs.mu.Lock()
	if fs.quota > 0 && fs.used+written > fs.quota {
		fs.mu.Unlock()
		_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
		metrics.RecordStorageOperation("filesystem", "put", "quota_exceeded")
		return errors.QuotaExceeded(fs.quota)
	}
	if err := os.Rename(tempPath, path); err != nil {
		fs.mu.Unlock()
		_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
		metrics.RecordStorageOperation("filesystem", "put", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to rename temp file")
	}
	fs.used += written
	currentUsed := fs.used
	fs.mu.Unlock()

	metrics.RecordStorageOperation("filesystem", "put", "success")
	metrics.UpdateCacheSize("filesystem", currentUsed)
	return nil
}

// Delete removes a file
func (fs *FilesystemStorage) Delete(ctx context.Context, key string) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		metrics.RecordStorageOperation("filesystem", "delete", "error")
		return err
	}

	// Get size before deletion
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			metrics.RecordStorageOperation("filesystem", "delete", "not_found")
			return errors.NotFound(fmt.Sprintf("file not found: %s", key))
		}
		metrics.RecordStorageOperation("filesystem", "delete", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to stat file")
	}

	size := info.Size()

	if err := os.Remove(path); err != nil {
		metrics.RecordStorageOperation("filesystem", "delete", "error")
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to delete file")
	}

	fs.mu.Lock()
	fs.used -= size
	currentUsed := fs.used
	fs.mu.Unlock()

	metrics.RecordStorageOperation("filesystem", "delete", "success")
	metrics.UpdateCacheSize("filesystem", currentUsed)
	return nil
}

// Exists checks if a file exists
func (fs *FilesystemStorage) Exists(ctx context.Context, key string) (bool, error) {
	select {
	case <-ctx.Done():
		return false, ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		return false, err
	}
	_, err = os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to check existence")
	}
	return true, nil
}

// List lists files with prefix
func (fs *FilesystemStorage) List(ctx context.Context, prefix string, opts *storage.ListOptions) ([]storage.StorageObject, error) {
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	searchPath, err := fs.keyToPath(prefix)
	if err != nil {
		return nil, err
	}
	var objects []storage.StorageObject

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

		if info.IsDir() {
			return nil
		}

		// Convert path back to key
		relPath, _ := filepath.Rel(fs.basePath, path)
		key := filepath.ToSlash(relPath)

		objects = append(objects, storage.StorageObject{
			Key:      key,
			Size:     info.Size(),
			Modified: info.ModTime(),
		})

		return nil
	})

	if err != nil && !os.IsNotExist(err) {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to list files")
	}

	// Apply pagination if requested
	if opts != nil {
		start := opts.Offset
		end := len(objects)
		if opts.MaxResults > 0 && start+opts.MaxResults < end {
			end = start + opts.MaxResults
		}
		if start < len(objects) {
			objects = objects[start:end]
		}
	}

	return objects, nil
}

// Stat gets file metadata
func (fs *FilesystemStorage) Stat(ctx context.Context, key string) (*storage.StorageInfo, error) {
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		return nil, err
	}
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, errors.NotFound(fmt.Sprintf("file not found: %s", key))
		}
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to stat file")
	}

	return &storage.StorageInfo{
		Key:      key,
		Size:     info.Size(),
		Modified: info.ModTime(),
	}, nil
}

// GetQuota returns quota information
func (fs *FilesystemStorage) GetQuota(ctx context.Context) (*storage.QuotaInfo, error) {
	fs.mu.RLock()
	used := fs.used
	fs.mu.RUnlock()

	available := fs.quota - used
	if available < 0 {
		available = 0
	}

	return &storage.QuotaInfo{
		Used:      used,
		Available: available,
		Limit:     fs.quota,
	}, nil
}

// Health checks filesystem health
func (fs *FilesystemStorage) Health(ctx context.Context) error {
	// Check if base path is accessible
	if _, err := os.Stat(fs.basePath); err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "base path not accessible")
	}

	// Try to create a temp file (sanitize path to prevent traversal)
	tempPath := filepath.Clean(filepath.Join(fs.basePath, ".health_check"))
	f, err := os.Create(tempPath)
	if err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "cannot write to storage")
	}
	_ = f.Close()           // #nosec G104 -- Cleanup, error not critical
	_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical

	return nil
}

// Close closes the storage backend
func (fs *FilesystemStorage) Close() error {
	// Nothing to close for filesystem
	return nil
}

// GetLocalPath returns the local filesystem path for a storage key
// This implements storage.LocalPathProvider interface
func (fs *FilesystemStorage) GetLocalPath(ctx context.Context, key string) (string, error) {
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	default:
	}

	path, err := fs.keyToPath(key)
	if err != nil {
		return "", err
	}

	// Verify file exists
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return "", errors.NotFound(fmt.Sprintf("file not found: %s", key))
		}
		return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to stat file")
	}

	return path, nil
}

// keyToPath converts a storage key to filesystem path.
// It sanitizes the key to prevent path traversal and verifies that the
// resulting absolute path stays within the configured base directory as a
// defense-in-depth check on top of filepath.Clean/Join semantics.
func (fs *FilesystemStorage) keyToPath(key string) (string, error) {
	// Sanitize key to prevent path traversal
	cleaned := filepath.Clean(key)

	// Remove any leading slashes or dots
	cleaned = strings.TrimPrefix(cleaned, "/")

	// Keep removing ../ until there are no more
	for strings.HasPrefix(cleaned, "../") || strings.HasPrefix(cleaned, "..\\") {
		cleaned = strings.TrimPrefix(cleaned, "../")
		cleaned = strings.TrimPrefix(cleaned, "..\\")
	}

	// Final clean and ensure it's within base path
	cleaned = filepath.Clean(cleaned)
	if cleaned == ".." || strings.HasPrefix(cleaned, "../") || strings.HasPrefix(cleaned, "..\\") {
		cleaned = ""
	}

	target := filepath.Join(fs.basePath, cleaned)

	// Defense-in-depth: verify the resolved absolute path is contained
	// within the base directory.
	targetAbs, err := filepath.Abs(target)
	if err != nil {
		return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to resolve path")
	}
	baseAbs, err := filepath.Abs(fs.basePath)
	if err != nil {
		return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to resolve base path")
	}
	if targetAbs != baseAbs && !strings.HasPrefix(targetAbs, baseAbs+string(os.PathSeparator)) {
		return "", errors.New(errors.ErrCodeStorageFailure, fmt.Sprintf("path traversal rejected: %s", key))
	}

	return target, nil
}

// calculateUsage calculates current storage usage
func (fs *FilesystemStorage) calculateUsage() error {
	var total int64

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

	if err != nil {
		return err
	}

	fs.mu.Lock()
	fs.used = total
	fs.mu.Unlock()

	metrics.UpdateCacheSize("filesystem", total)
	return nil
}
