package archive

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

type filesystemStorage struct {
	root string
}

// NewFilesystem constructs a filesystem-backed archive storage rooted at one directory.
func NewFilesystem(root string) (Storage, error) {
	trimmedRoot := strings.TrimSpace(root)
	if trimmedRoot == "" {
		return nil, fmt.Errorf("archive filesystem root is required")
	}
	return &filesystemStorage{root: trimmedRoot}, nil
}

func (storage *filesystemStorage) ReadChunk(ctx context.Context, workspaceID string, threadID string, chunkStartSeq int64) ([]Message, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	chunkPath, err := filesystemChunkPath(storage.root, workspaceID, threadID, chunkStartSeq)
	if err != nil {
		return nil, err
	}
	file, err := os.Open(chunkPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, ErrChunkNotFound
		}
		return nil, fmt.Errorf("open archive chunk %s: %w", chunkPath, err)
	}
	defer file.Close()

	messages, err := decodeChunk(file)
	if err != nil {
		return nil, fmt.Errorf("decode archive chunk %s: %w", chunkPath, err)
	}
	if err := validateChunkMessages(chunkStartSeq, messages); err != nil {
		return nil, fmt.Errorf("validate archive chunk %s: %w", chunkPath, err)
	}
	return messages, nil
}

func (storage *filesystemStorage) WriteChunk(ctx context.Context, workspaceID string, threadID string, chunkStartSeq int64, messages []Message) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	if err := validateChunkMessages(chunkStartSeq, messages); err != nil {
		return err
	}
	chunkPath, err := filesystemChunkPath(storage.root, workspaceID, threadID, chunkStartSeq)
	if err != nil {
		return err
	}

	if _, err := os.Stat(chunkPath); err == nil {
		existing, err := storage.ReadChunk(ctx, workspaceID, threadID, chunkStartSeq)
		if err != nil {
			return fmt.Errorf("read existing archive chunk %s: %w", chunkPath, err)
		}
		if chunkMessagesEqual(existing, messages) {
			return nil
		}
		return fmt.Errorf("%w: %s", ErrChunkAlreadyExists, chunkPath)
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("stat archive chunk %s: %w", chunkPath, err)
	}

	payload, err := encodeChunk(messages)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(chunkPath), 0o755); err != nil {
		return fmt.Errorf("create archive directory: %w", err)
	}
	file, err := os.CreateTemp(filepath.Dir(chunkPath), ".agentteam-archive-*.tmp")
	if err != nil {
		return fmt.Errorf("create archive temp file: %w", err)
	}
	tempPath := file.Name()
	defer os.Remove(tempPath)
	if _, err := file.Write(payload); err != nil {
		file.Close()
		return fmt.Errorf("write archive temp file: %w", err)
	}
	if err := file.Close(); err != nil {
		return fmt.Errorf("close archive temp file: %w", err)
	}

	if err := os.Link(tempPath, chunkPath); err != nil {
		if errors.Is(err, os.ErrExist) {
			existing, err := storage.ReadChunk(ctx, workspaceID, threadID, chunkStartSeq)
			if err != nil {
				return fmt.Errorf("read existing archive chunk %s: %w", chunkPath, err)
			}
			if chunkMessagesEqual(existing, messages) {
				return nil
			}
			return fmt.Errorf("%w: %s", ErrChunkAlreadyExists, chunkPath)
		}
		return fmt.Errorf("link archive chunk: %w", err)
	}
	return nil
}

func filesystemChunkPath(root string, workspaceID string, threadID string, chunkStartSeq int64) (string, error) {
	key, err := ObjectKey("", workspaceID, threadID, chunkStartSeq)
	if err != nil {
		return "", err
	}
	return filepath.Join(root, filepath.FromSlash(key)), nil
}
