package providers

import (
	"archive/zip"
	"bytes"
	"embed"
	"fmt"
	"io"
	"io/fs"
	"path"
	"strings"
	"sync"

	"github.com/bitechdev/ResolveSpec/pkg/server/zipfs"
)

// EmbedFSProvider serves files from an embedded filesystem.
// It supports both direct embedded directories and embedded zip files.
type EmbedFSProvider struct {
	embedFS     *embed.FS
	zipFile     string // Optional: path within embedded FS to zip file
	stripPrefix string // Optional: prefix to strip from requested paths (e.g., "/dist")
	zipReader   *zip.Reader
	fs          fs.FS
	mu          sync.RWMutex
}

// NewEmbedFSProvider creates a new EmbedFSProvider.
// If zipFile is empty, the embedded FS is used directly.
// If zipFile is specified, it's treated as a path to a zip file within the embedded FS.
// Use WithStripPrefix to configure path prefix stripping.
func NewEmbedFSProvider(embedFS fs.FS, zipFile string) (*EmbedFSProvider, error) {
	if embedFS == nil {
		return nil, fmt.Errorf("embedded filesystem cannot be nil")
	}

	// Try to cast to *embed.FS for tracking purposes
	var embedFSPtr *embed.FS
	if efs, ok := embedFS.(*embed.FS); ok {
		embedFSPtr = efs
	}

	provider := &EmbedFSProvider{
		embedFS: embedFSPtr,
		zipFile: zipFile,
	}

	// If zipFile is specified, open it as a zip archive
	if zipFile != "" {
		// Read the zip file from the embedded FS
		// We need to check if the FS supports ReadFile
		var data []byte
		var err error

		if readFileFS, ok := embedFS.(interface{ ReadFile(string) ([]byte, error) }); ok {
			data, err = readFileFS.ReadFile(zipFile)
		} else {
			// Fall back to Open and reading
			file, openErr := embedFS.Open(zipFile)
			if openErr != nil {
				return nil, fmt.Errorf("failed to open embedded zip file: %w", openErr)
			}
			defer file.Close()
			data, err = io.ReadAll(file)
		}
		if err != nil {
			return nil, fmt.Errorf("failed to read embedded zip file: %w", err)
		}

		// Create a zip reader from the data
		reader := bytes.NewReader(data)
		zipReader, err := zip.NewReader(reader, int64(len(data)))
		if err != nil {
			return nil, fmt.Errorf("failed to create zip reader: %w", err)
		}

		provider.zipReader = zipReader
		provider.fs = zipfs.NewZipFS(zipReader)
	} else {
		// Use the embedded FS directly
		provider.fs = embedFS
	}

	return provider, nil
}

// Open opens the named file from the embedded filesystem.
// If a strip prefix is configured, it prepends the prefix to the requested path.
// For example, with stripPrefix="/dist", requesting "/assets/style.css" will
// open "/dist/assets/style.css" from the embedded filesystem.
func (p *EmbedFSProvider) Open(name string) (fs.File, error) {
	p.mu.RLock()
	defer p.mu.RUnlock()

	if p.fs == nil {
		return nil, fmt.Errorf("embedded filesystem is closed")
	}

	// Apply prefix stripping by prepending the prefix to the requested path
	actualPath := name
	alternatePath := ""
	if p.stripPrefix != "" {
		// Clean the paths to handle leading/trailing slashes
		prefix := strings.Trim(p.stripPrefix, "/")
		cleanName := strings.TrimPrefix(name, "/")

		if prefix != "" {
			actualPath = path.Join(prefix, cleanName)
			alternatePath = cleanName
		} else {
			actualPath = cleanName
		}
	}
	// First try the actual path with prefix
	if file, err := p.fs.Open(actualPath); err == nil {
		return file, nil
	}

	// If alternate path is different, try it as well
	if alternatePath != "" && alternatePath != actualPath {
		if file, err := p.fs.Open(alternatePath); err == nil {
			return file, nil
		}
	}

	// If both attempts fail, return the error from the first attempt
	return nil, fmt.Errorf("file not found: %s", name)
}

// Close releases any resources held by the provider.
// For embedded filesystems, this is mostly a no-op since Go manages the lifecycle.
func (p *EmbedFSProvider) Close() error {
	p.mu.Lock()
	defer p.mu.Unlock()

	// Clear references to allow garbage collection
	p.fs = nil
	p.zipReader = nil

	return nil
}

// Type returns "embed" or "embed-zip" depending on the configuration.
func (p *EmbedFSProvider) Type() string {
	if p.zipFile != "" {
		return "embed-zip"
	}
	return "embed"
}

// ZipFile returns the path to the zip file within the embedded FS, if any.
func (p *EmbedFSProvider) ZipFile() string {
	return p.zipFile
}

// WithStripPrefix sets the prefix to strip from requested paths.
// For example, WithStripPrefix("/dist") will make files at "/dist/assets"
// accessible via "/assets".
func (p *EmbedFSProvider) WithStripPrefix(prefix string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.stripPrefix = prefix
}

// StripPrefix returns the configured strip prefix.
func (p *EmbedFSProvider) StripPrefix() string {
	p.mu.RLock()
	defer p.mu.RUnlock()
	return p.stripPrefix
}
