package methods

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/google/uuid"

	"github.com/nextlevelbuilder/goclaw/internal/gateway"
	"github.com/nextlevelbuilder/goclaw/internal/i18n"
	"github.com/nextlevelbuilder/goclaw/internal/store"
	"github.com/nextlevelbuilder/goclaw/internal/tools"
	"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)

// RegisterWorkspace adds workspace RPC handlers to the method router.
func (m *TeamsMethods) RegisterWorkspace(router *gateway.MethodRouter) {
	router.Register(protocol.MethodTeamsWorkspaceList, m.handleWorkspaceList)
	router.Register(protocol.MethodTeamsWorkspaceRead, m.handleWorkspaceRead)
	router.Register(protocol.MethodTeamsWorkspaceDelete, m.handleWorkspaceDelete)
}

// teamWorkspaceDir returns the base directory for a team's workspace files.
// Pattern: {dataDir}/teams/{teamID}/
// If chatID is provided, scopes to {dataDir}/teams/{teamID}/{chatID}/
func teamWorkspaceDir(dataDir string, teamID uuid.UUID, chatID string) string {
	if chatID != "" {
		return filepath.Join(dataDir, "teams", teamID.String(), chatID)
	}
	return filepath.Join(dataDir, "teams", teamID.String())
}

// workspaceFileEntry is the response shape for workspace file listing.
type workspaceFileEntry struct {
	Name      string `json:"name"`
	Path      string `json:"path"`
	Size      int64  `json:"size"`
	ChatID    string `json:"chat_id"`
	IsDir     bool   `json:"is_dir,omitempty"`
	UpdatedAt string `json:"updated_at,omitempty"`
}

// --- Workspace List ---

type workspaceListParams struct {
	TeamID string `json:"team_id"`
	ChatID string `json:"chat_id"`
}

func (m *TeamsMethods) handleWorkspaceList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
	locale := store.LocaleFromContext(ctx)

	var params workspaceListParams
	if err := json.Unmarshal(req.Params, &params); err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
		return
	}
	if params.TeamID == "" {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "team_id")))
		return
	}

	teamID, err := uuid.Parse(params.TeamID)
	if err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid team_id"))
		return
	}

	// Check if team uses shared workspace (no chatID scoping).
	shared := false
	if team, err := m.teamStore.GetTeam(ctx, teamID); err == nil {
		shared = tools.IsSharedWorkspace(team.Settings)
	}

	baseDir := teamWorkspaceDir(m.dataDir, teamID, "")
	var files []workspaceFileEntry

	if shared || params.ChatID != "" {
		// Shared mode: list team root directly. Scoped mode: list specific chatID.
		scopeDir := baseDir
		scopeChatID := ""
		if !shared && params.ChatID != "" {
			scopeDir = teamWorkspaceDir(m.dataDir, teamID, params.ChatID)
			scopeChatID = params.ChatID
		}
		files = walkDir(scopeDir, "", scopeChatID)
	} else {
		// Isolated + unscoped: list all chatID subdirectories with chatID as top-level folder.
		entries, err := os.ReadDir(baseDir)
		if err != nil {
			// Directory doesn't exist = empty workspace.
			client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
				"files": []workspaceFileEntry{},
				"count": 0,
			}))
			return
		}
		for _, entry := range entries {
			if !entry.IsDir() {
				continue
			}
			chatID := entry.Name()
			scopeDir := filepath.Join(baseDir, chatID)
			// Add the chatID directory itself as a top-level entry.
			files = append(files, workspaceFileEntry{
				Name:   chatID,
				Path:   scopeDir,
				ChatID: chatID,
				IsDir:  true,
			})
			// Prefix children with chatID so the tree nests under it.
			files = append(files, walkDir(scopeDir, chatID, chatID)...)
		}
	}

	if files == nil {
		files = []workspaceFileEntry{}
	}

	client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
		"files": files,
		"count": len(files),
	}))
}

// walkDir recursively lists files and directories, returning workspaceFileEntry slice
// with relative paths. prefix is the relative path prefix for nested entries.
func walkDir(baseDir, prefix, chatID string) []workspaceFileEntry {
	entries, err := os.ReadDir(baseDir)
	if err != nil {
		return nil
	}
	var files []workspaceFileEntry
	for _, entry := range entries {
		relPath := entry.Name()
		if prefix != "" {
			relPath = prefix + "/" + entry.Name()
		}
		if entry.IsDir() {
			files = append(files, workspaceFileEntry{
				Name:   relPath,
				Path:   filepath.Join(baseDir, entry.Name()),
				ChatID: chatID,
				IsDir:  true,
			})
			// Recurse into subdirectory.
			files = append(files, walkDir(filepath.Join(baseDir, entry.Name()), relPath, chatID)...)
			continue
		}
		info, err := entry.Info()
		if err != nil {
			continue
		}
		files = append(files, workspaceFileEntry{
			Name:      relPath,
			Path:      filepath.Join(baseDir, entry.Name()),
			Size:      info.Size(),
			ChatID:    chatID,
			UpdatedAt: info.ModTime().UTC().Format("2006-01-02T15:04:05Z"),
		})
	}
	return files
}

// --- Workspace Read ---

type workspaceReadParams struct {
	TeamID   string `json:"team_id"`
	ChatID   string `json:"chat_id"`
	FileName string `json:"file_name"`
}

func (m *TeamsMethods) handleWorkspaceRead(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
	locale := store.LocaleFromContext(ctx)

	var params workspaceReadParams
	if err := json.Unmarshal(req.Params, &params); err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
		return
	}
	if params.TeamID == "" || params.FileName == "" {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "team_id, file_name")))
		return
	}
	// Security: reject path traversal (allow "/" for nested paths, reject "\" and "..").
	if strings.Contains(params.FileName, "..") || strings.Contains(params.FileName, "\\") {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid file_name"))
		return
	}

	teamID, err := uuid.Parse(params.TeamID)
	if err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid team_id"))
		return
	}

	// Shared workspace: read from team root. Isolated: require chatID.
	chatID := params.ChatID
	if team, err := m.teamStore.GetTeam(ctx, teamID); err == nil && tools.IsSharedWorkspace(team.Settings) {
		chatID = ""
	} else if chatID == "" {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "chat_id")))
		return
	}

	scopeDir := teamWorkspaceDir(m.dataDir, teamID, chatID)
	diskPath := filepath.Clean(filepath.Join(scopeDir, params.FileName))
	// Ensure resolved path stays within the workspace scope directory.
	if !strings.HasPrefix(diskPath, filepath.Clean(scopeDir)+string(os.PathSeparator)) && diskPath != filepath.Clean(scopeDir) {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid file_name"))
		return
	}
	data, err := os.ReadFile(diskPath)
	if err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, fmt.Sprintf("file not found: %s", params.FileName)))
		return
	}

	content := string(data)
	if len(content) > 500000 {
		content = content[:500000] + "\n\n[...truncated]"
	}

	info, _ := os.Stat(diskPath)
	file := workspaceFileEntry{
		Name:   params.FileName,
		Path:   diskPath,
		Size:   int64(len(data)),
		ChatID: chatID,
	}
	if info != nil {
		file.UpdatedAt = info.ModTime().UTC().Format("2006-01-02T15:04:05Z")
	}

	client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
		"file":    file,
		"content": content,
	}))
}

// --- Workspace Delete ---

type workspaceDeleteParams struct {
	TeamID   string `json:"team_id"`
	ChatID   string `json:"chat_id"`
	FileName string `json:"file_name"`
}

func (m *TeamsMethods) handleWorkspaceDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
	locale := store.LocaleFromContext(ctx)

	var params workspaceDeleteParams
	if err := json.Unmarshal(req.Params, &params); err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
		return
	}
	if params.TeamID == "" || params.FileName == "" {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "team_id, file_name")))
		return
	}
	// Security: reject path traversal (allow "/" for nested paths, reject "\" and "..").
	if strings.Contains(params.FileName, "..") || strings.Contains(params.FileName, "\\") {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid file_name"))
		return
	}

	teamID, err := uuid.Parse(params.TeamID)
	if err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid team_id"))
		return
	}

	// Shared workspace: delete from team root. Isolated: require chatID.
	chatID := params.ChatID
	if team, err := m.teamStore.GetTeam(ctx, teamID); err == nil && tools.IsSharedWorkspace(team.Settings) {
		chatID = ""
	} else if chatID == "" {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "chat_id")))
		return
	}

	scopeDir := teamWorkspaceDir(m.dataDir, teamID, chatID)
	diskPath := filepath.Clean(filepath.Join(scopeDir, params.FileName))
	// Ensure resolved path stays within the workspace scope directory.
	if !strings.HasPrefix(diskPath, filepath.Clean(scopeDir)+string(os.PathSeparator)) && diskPath != filepath.Clean(scopeDir) {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid file_name"))
		return
	}
	if err := os.Remove(diskPath); err != nil {
		client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, fmt.Sprintf("file not found: %s", params.FileName)))
		return
	}

	client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
		"deleted": params.FileName,
	}))
}
