package workflow

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"path/filepath"
	"strings"
	"time"
)

// WorkspaceFileAdapter implements FileAdapter using the online Workspace API.
//
// # Path Structure (matches online Workspace API)
//
// Every file operation is scoped by gid (workspace/project identifier):
//
//	POST /ih5/editor/workspace/readFile  {"gid":"xx","path":"Output/result.txt"}
//	POST /ih5/editor/workspace/writeFile {"gid":"xx","path":"Output/result.txt","content":"..."}
//
// The adapter uses the exact same gid concept as LocalFileAdapter, so the
// same workflow JSON and the same gid value work for both local and online runs.
//
// Authentication is via Cookie header (ih5bearer token), matching the
// DocCenter auth pattern used across the platform.
type WorkspaceFileAdapter struct {
	baseURL    string       // e.g. "https://editor.visuallogic.ai"
	gid        string       // workspace / project identifier
	cookie     string       // ih5bearer=<token>
	httpClient *http.Client
}

// WorkspaceFileConfig holds configuration for WorkspaceFileAdapter.
type WorkspaceFileConfig struct {
	// BaseURL is the platform base URL (e.g. "https://editor.visuallogic.ai").
	BaseURL string
	// GID is the workspace / project identifier.
	GID string
	// Cookie is the full cookie header value, e.g. "ih5bearer=<token>".
	// Read from /Users/ivx/Documents/docs/auth/.doccenter_cookie at runtime.
	Cookie string
	// Timeout for HTTP calls (default: 30s).
	Timeout time.Duration
}

// NewWorkspaceFileAdapter creates a WorkspaceFileAdapter.
func NewWorkspaceFileAdapter(cfg WorkspaceFileConfig) (*WorkspaceFileAdapter, error) {
	if cfg.BaseURL == "" {
		return nil, fmt.Errorf("WorkspaceFileAdapter: BaseURL is required")
	}
	if cfg.GID == "" {
		return nil, fmt.Errorf("WorkspaceFileAdapter: GID is required")
	}
	if cfg.Cookie == "" {
		return nil, fmt.Errorf("WorkspaceFileAdapter: Cookie is required")
	}
	timeout := cfg.Timeout
	if timeout == 0 {
		timeout = 30 * time.Second
	}
	return &WorkspaceFileAdapter{
		baseURL: strings.TrimSuffix(cfg.BaseURL, "/"),
		gid:     cfg.GID,
		cookie:  cfg.Cookie,
		httpClient: &http.Client{
			Timeout: timeout,
		},
	}, nil
}

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

// ── helpers ──────────────────────────────────────────────────────────────────

// normPath strips the leading slash used in workflow JSON "out" keys.
// "/Output/result.txt" → "Output/result.txt"
func (a *WorkspaceFileAdapter) normPath(path string) string {
	return strings.TrimLeft(path, "/")
}

// post sends a POST request to the workspace API and decodes the JSON response.
func (a *WorkspaceFileAdapter) post(ctx context.Context, endpoint string, body interface{}, out interface{}) error {
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return fmt.Errorf("marshal request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		a.baseURL+"/ih5/editor/workspace/"+endpoint,
		bytes.NewReader(bodyBytes))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", a.cookie)

	resp, err := a.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("POST %s: %w", endpoint, err)
	}
	defer resp.Body.Close()

	respBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("POST %s: read response: %w", endpoint, err)
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("POST %s: HTTP %d: %s", endpoint, resp.StatusCode, string(respBytes))
	}
	if out != nil {
		if err := json.Unmarshal(respBytes, out); err != nil {
			return fmt.Errorf("POST %s: decode response: %w", endpoint, err)
		}
	}
	return nil
}

// ensureFile creates the file on the server if it does not already exist.
// The online API requires createFile before the first writeFile.
func (a *WorkspaceFileAdapter) ensureFile(ctx context.Context, path string) error {
	err := a.post(ctx, "createFile", map[string]string{
		"gid":  a.gid,
		"path": path,
	}, nil)
	// Ignore "already exists" style errors (the server may return 200 or an
	// error body; treat any non-network error as "probably already exists").
	_ = err // best-effort create; writeFile will catch real failures
	return nil
}

// ── FileAdapter implementation ────────────────────────────────────────────────

// Read implements FileAdapter — fetches file content from the workspace API.
func (a *WorkspaceFileAdapter) Read(ctx context.Context, path string) ([]byte, error) {
	p := a.normPath(path)
	var resp struct {
		Version int    `json:"version"`
		Status  int    `json:"status"`
		Content string `json:"content"`
	}
	if err := a.post(ctx, "readFile", map[string]string{
		"gid":  a.gid,
		"path": p,
	}, &resp); err != nil {
		return nil, fmt.Errorf("WorkspaceFileAdapter.Read %q: %w", path, err)
	}
	return []byte(resp.Content), nil
}

// Write implements FileAdapter — writes content to the workspace API.
// The online API is append-by-version (every write creates a new version),
// so WriteMode semantics are emulated on the client side where possible.
func (a *WorkspaceFileAdapter) Write(ctx context.Context, path string, content []byte, mode WriteMode) error {
	p := a.normPath(path)

	switch mode {
	case WriteModeFailIfExists:
		exists, err := a.Exists(ctx, path)
		if err != nil {
			return fmt.Errorf("WorkspaceFileAdapter.Write(failIfExists) check: %w", err)
		}
		if exists {
			return &FileExistsError{Path: path}
		}
		// Fall through to create + write

	case WriteModeAppend:
		existing, err := a.Read(ctx, path)
		if err == nil {
			content = append(existing, content...)
		}
		// If file doesn't exist yet, treat as fresh write

	case WriteModePrepend:
		existing, err := a.Read(ctx, path)
		if err == nil {
			content = append(content, existing...)
		}
	}

	// Ensure file exists before writing (online API requires createFile first)
	_ = a.ensureFile(ctx, p)

	var writeResp struct {
		GID     string `json:"gid"`
		Path    string `json:"path"`
		Version int    `json:"version"`
	}
	if err := a.post(ctx, "writeFile", map[string]interface{}{
		"gid":     a.gid,
		"path":    p,
		"content": string(content),
	}, &writeResp); err != nil {
		return fmt.Errorf("WorkspaceFileAdapter.Write %q: %w", path, err)
	}
	return nil
}

// Exists implements FileAdapter — checks whether the file exists and has been written
// (version > 0) in the workspace.
func (a *WorkspaceFileAdapter) Exists(ctx context.Context, path string) (bool, error) {
	p := a.normPath(path)
	var resp struct {
		Files []struct {
			Path    string `json:"path"`
			Version int    `json:"version"`
		} `json:"files"`
	}
	if err := a.post(ctx, "listFile", map[string]string{
		"gid": a.gid,
	}, &resp); err != nil {
		return false, fmt.Errorf("WorkspaceFileAdapter.Exists: %w", err)
	}
	for _, f := range resp.Files {
		if f.Path == p && f.Version > 0 {
			return true, nil
		}
	}
	return false, nil
}

// List implements FileAdapter — returns all file paths in the workspace that
// match the given glob pattern. Pattern uses the same convention as
// LocalFileAdapter (e.g., "Output/*", "**/*.txt"). Leading slash is stripped.
// Returned paths have a leading slash to match workflow convention.
func (a *WorkspaceFileAdapter) List(ctx context.Context, pattern string) ([]string, error) {
	var resp struct {
		Files []struct {
			Path    string `json:"path"`
			Version int    `json:"version"`
		} `json:"files"`
	}
	if err := a.post(ctx, "listFile", map[string]string{
		"gid": a.gid,
	}, &resp); err != nil {
		return nil, fmt.Errorf("WorkspaceFileAdapter.List: %w", err)
	}

	// Convert pattern to a simple prefix/glob matcher
	cleanPat := strings.TrimLeft(pattern, "/")

	var result []string
	for _, f := range resp.Files {
		if f.Version == 0 {
			continue // file created but never written — skip
		}
		matched, err := matchGlob(cleanPat, f.Path)
		if err != nil || !matched {
			continue
		}
		result = append(result, "/"+f.Path)
	}
	return result, nil
}

// matchGlob checks whether path matches the given glob pattern.
// Uses the same registry.files glob convention as the workflow engine.
func matchGlob(pattern, path string) (bool, error) {
	// filepath.Match handles simple "*" and "?" wildcards.
	// For "**" (double-star) patterns, fall back to prefix match.
	if strings.Contains(pattern, "**") {
		prefix := strings.SplitN(pattern, "**", 2)[0]
		return strings.HasPrefix(path, prefix), nil
	}
	return filepath.Match(pattern, path)
}
