package workflow

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

// LocalFileAdapter implements FileAdapter for the local filesystem.
//
// # Path Structure (mirrors online Workspace API)
//
//	<root>/<gid>/<path>
//
// where:
//   - root  = base workspace root directory on disk
//   - gid   = workspace/project identifier (same as RunParams.WorkspaceID in online runs)
//   - path  = relative file path as written in workflow JSON "out" keys
//     (e.g., "/Output/result.txt" or "Output/result.txt")
//
// This mirrors the online workspace API structure where every file operation
// requires both a gid (space identifier) and a relative path:
//
//	POST /ih5/editor/workspace/writeFile {"gid":"xx","path":"Output/result.txt"}
//
// Using the same gid concept locally means the same workflow JSON runs
// identically both online and offline — only the adapter differs.
type LocalFileAdapter struct {
	root string // absolute path to base workspace root
	gid  string // workspace / project identifier (subdirectory under root)
}

// NewLocalFileAdapter creates a LocalFileAdapter.
//
//   - root: base directory that holds all workspaces (e.g. "/Users/alice/workspace")
//   - gid:  workspace / project identifier (e.g. "project-abc" or a UUID)
//
// Files are stored as:  root / gid / <relative-path>
func NewLocalFileAdapter(root, gid string) (*LocalFileAdapter, error) {
	if root == "" {
		return nil, fmt.Errorf("LocalFileAdapter: root directory is required")
	}
	if gid == "" {
		return nil, fmt.Errorf("LocalFileAdapter: gid (workspace identifier) is required")
	}
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return nil, fmt.Errorf("LocalFileAdapter: invalid root %q: %w", root, err)
	}
	return &LocalFileAdapter{root: absRoot, gid: gid}, nil
}

// Root returns the base workspace root directory.
func (a *LocalFileAdapter) Root() string { return a.root }

// GID returns the workspace / project identifier.
func (a *LocalFileAdapter) GID() string { return a.gid }

// WorkspaceDir returns the absolute path to this workspace (root/gid).
func (a *LocalFileAdapter) WorkspaceDir() string {
	return filepath.Join(a.root, a.gid)
}

// resolvePath converts a workflow-relative path to an absolute local path.
// Leading slashes are stripped (workflow "out" keys use "/Output/file.txt").
// Path traversal outside the workspace boundary is rejected.
func (a *LocalFileAdapter) resolvePath(path string) (string, error) {
	// Strip leading slash from workflow convention ("/Output/file.txt" → "Output/file.txt")
	clean := strings.TrimLeft(filepath.ToSlash(path), "/")
	if clean == "" {
		return "", fmt.Errorf("empty file path")
	}
	resolved := filepath.Join(a.WorkspaceDir(), filepath.FromSlash(clean))

	// Guard: resolved path must remain inside the workspace directory
	wsDir := a.WorkspaceDir()
	if !strings.HasPrefix(resolved+string(filepath.Separator), wsDir+string(filepath.Separator)) {
		return "", fmt.Errorf("path %q escapes workspace boundary", path)
	}
	return resolved, nil
}

// Read implements FileAdapter — reads a file from root/gid/path.
func (a *LocalFileAdapter) Read(ctx context.Context, path string) ([]byte, error) {
	resolved, err := a.resolvePath(path)
	if err != nil {
		return nil, fmt.Errorf("LocalFileAdapter.Read: %w", err)
	}
	data, err := os.ReadFile(resolved)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, &FileNotFoundError{Path: path}
		}
		return nil, fmt.Errorf("LocalFileAdapter.Read %q: %w", path, err)
	}
	return data, nil
}

// Write implements FileAdapter — writes content to root/gid/path.
// Parent directories are created automatically.
func (a *LocalFileAdapter) Write(ctx context.Context, path string, content []byte, mode WriteMode) error {
	resolved, err := a.resolvePath(path)
	if err != nil {
		return fmt.Errorf("LocalFileAdapter.Write: %w", err)
	}
	// Auto-create parent directories (mirrors online workspace behaviour)
	if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
		return fmt.Errorf("LocalFileAdapter.Write %q: cannot create parent dirs: %w", path, err)
	}

	switch mode {
	case WriteModeFailIfExists:
		if _, err := os.Stat(resolved); err == nil {
			return &FileExistsError{Path: path}
		}
		return writeFile(resolved, content)

	case WriteModeAppend:
		f, err := os.OpenFile(resolved, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
		if err != nil {
			return fmt.Errorf("LocalFileAdapter.Write(append) %q: %w", path, err)
		}
		defer f.Close()
		_, err = f.Write(content)
		return err

	case WriteModePrepend:
		existing, _ := os.ReadFile(resolved) // ignore not-found → treat as empty
		return writeFile(resolved, append(content, existing...))

	default: // WriteModeOverwrite (and any future mode)
		return writeFile(resolved, content)
	}
}

// Exists implements FileAdapter — returns true if root/gid/path exists as a file.
func (a *LocalFileAdapter) Exists(ctx context.Context, path string) (bool, error) {
	resolved, err := a.resolvePath(path)
	if err != nil {
		return false, fmt.Errorf("LocalFileAdapter.Exists: %w", err)
	}
	info, err := os.Stat(resolved)
	if err == nil {
		return !info.IsDir(), nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, fmt.Errorf("LocalFileAdapter.Exists %q: %w", path, err)
}

// List implements FileAdapter — returns all files matching a glob pattern
// relative to the workspace directory. The pattern follows the same convention
// as the workflow registry.files declarations (e.g., "Output/*", "**/*.txt").
// Returned paths have a leading slash to match workflow convention.
func (a *LocalFileAdapter) List(ctx context.Context, pattern string) ([]string, error) {
	wsDir := a.WorkspaceDir()
	cleanPat := strings.TrimLeft(filepath.ToSlash(pattern), "/")
	absPattern := filepath.Join(wsDir, filepath.FromSlash(cleanPat))

	matches, err := filepath.Glob(absPattern)
	if err != nil {
		return nil, fmt.Errorf("LocalFileAdapter.List(%q): %w", pattern, err)
	}

	result := make([]string, 0, len(matches))
	for _, m := range matches {
		info, err := os.Stat(m)
		if err != nil || info.IsDir() {
			continue // skip directories and unreadable entries
		}
		rel, err := filepath.Rel(wsDir, m)
		if err != nil {
			continue
		}
		// Return with leading slash to match workflow "out" key convention
		result = append(result, "/"+filepath.ToSlash(rel))
	}
	return result, nil
}

// writeFile is a thin wrapper around os.WriteFile with 0644 permissions.
func writeFile(path string, content []byte) error {
	return os.WriteFile(path, content, 0o644)
}
