package blob

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

type filesystemStore struct {
	root string
}

// NewFilesystem constructs a filesystem-backed automation blob store rooted at one directory.
func NewFilesystem(root string) (Store, error) {
	trimmed := strings.TrimSpace(root)
	if trimmed == "" {
		return nil, fmt.Errorf("automation blob filesystem root is required")
	}
	return &filesystemStore{root: trimmed}, nil
}

func (store *filesystemStore) Put(ctx context.Context, key string, r io.Reader, size int64) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	if size < 0 {
		return fmt.Errorf("automation blob size is invalid")
	}
	targetPath, err := store.path(key)
	if err != nil {
		return err
	}
	if _, err := os.Stat(targetPath); err == nil {
		return store.compareExisting(ctx, targetPath, r, size)
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("stat automation blob %s: %w", targetPath, err)
	}
	if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
		return fmt.Errorf("create automation blob directory: %w", err)
	}
	file, err := os.CreateTemp(filepath.Dir(targetPath), ".meshx-automation-blob-*.tmp")
	if err != nil {
		return fmt.Errorf("create automation blob temp file: %w", err)
	}
	tempPath := file.Name()
	defer os.Remove(tempPath)
	written, err := io.Copy(file, io.LimitReader(r, size+1))
	if err != nil {
		file.Close()
		return fmt.Errorf("write automation blob temp file: %w", err)
	}
	if written != size {
		file.Close()
		return fmt.Errorf("automation blob size mismatch: got %d bytes, want %d", written, size)
	}
	if err := file.Close(); err != nil {
		return fmt.Errorf("close automation blob temp file: %w", err)
	}
	if err := os.Link(tempPath, targetPath); err != nil {
		if errors.Is(err, os.ErrExist) {
			temp, openErr := os.Open(tempPath)
			if openErr != nil {
				return fmt.Errorf("open automation blob temp file: %w", openErr)
			}
			defer temp.Close()
			return store.compareExisting(ctx, targetPath, temp, size)
		}
		return fmt.Errorf("link automation blob: %w", err)
	}
	return nil
}

func (store *filesystemStore) Get(ctx context.Context, key string) (io.ReadCloser, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	targetPath, err := store.path(key)
	if err != nil {
		return nil, err
	}
	file, err := os.Open(targetPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, ErrNotFound
		}
		return nil, fmt.Errorf("open automation blob %s: %w", targetPath, err)
	}
	return file, nil
}

func (store *filesystemStore) Delete(ctx context.Context, key string) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	targetPath, err := store.path(key)
	if err != nil {
		return err
	}
	if err := os.Remove(targetPath); err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("delete automation blob %s: %w", targetPath, err)
	}
	return nil
}

func (store *filesystemStore) List(ctx context.Context, prefix string, limit int) ([]Object, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 100
	}
	if limit > 1000 {
		limit = 1000
	}
	prefix = strings.TrimSpace(prefix)
	if prefix == "" {
		prefix = "workspaces"
	}
	cleanedPrefix, err := cleanKey(prefix)
	if err != nil {
		return nil, err
	}
	rootPath := filepath.Join(store.root, filepath.FromSlash(cleanedPrefix))
	if _, err := os.Stat(rootPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, fmt.Errorf("stat automation blob prefix %s: %w", rootPath, err)
	}
	objects := make([]Object, 0, limit)
	stop := errors.New("automation blob list limit reached")
	err = filepath.WalkDir(rootPath, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		if entry.IsDir() {
			return nil
		}
		if strings.HasPrefix(entry.Name(), ".meshx-automation-blob-") {
			return nil
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(store.root, path)
		if err != nil {
			return err
		}
		key := filepath.ToSlash(rel)
		objects = append(objects, Object{Key: key, SizeBytes: info.Size(), ModTime: info.ModTime()})
		if len(objects) >= limit {
			return stop
		}
		return nil
	})
	if errors.Is(err, stop) {
		err = nil
	}
	if err != nil {
		return nil, fmt.Errorf("list automation blobs: %w", err)
	}
	return objects, nil
}

func (store *filesystemStore) compareExisting(ctx context.Context, targetPath string, r io.Reader, size int64) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	existing, err := os.ReadFile(targetPath)
	if err != nil {
		return fmt.Errorf("read existing automation blob %s: %w", targetPath, err)
	}
	incoming, err := io.ReadAll(io.LimitReader(r, size+1))
	if err != nil {
		return fmt.Errorf("read duplicate automation blob payload: %w", err)
	}
	if int64(len(incoming)) != size {
		return fmt.Errorf("automation blob size mismatch: got %d bytes, want %d", len(incoming), size)
	}
	if bytes.Equal(existing, incoming) {
		return nil
	}
	return fmt.Errorf("%w: %s", ErrAlreadyExists, targetPath)
}

func (store *filesystemStore) path(key string) (string, error) {
	cleaned, err := cleanKey(key)
	if err != nil {
		return "", err
	}
	return filepath.Join(store.root, filepath.FromSlash(cleaned)), nil
}
