package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"

	"chat-switchboard/knowledge"
	"chat-switchboard/models"
	"chat-switchboard/store"
	"chat-switchboard/workspace"
)

// ── Late Registration ────────────────────────
// Workspace tools need the workspace.FS and store for execution.
// Registered from main.go after workspace FS init.

// RegisterWorkspaceTools registers all workspace tools with captured dependencies.
func RegisterWorkspaceTools(stores store.Stores, wfs *workspace.FS) {
	Register(&workspaceLsTool{stores: stores, wfs: wfs})
	Register(&workspaceReadTool{stores: stores, wfs: wfs})
	Register(&workspaceWriteTool{stores: stores, wfs: wfs})
	Register(&workspaceRmTool{stores: stores, wfs: wfs})
	Register(&workspaceMvTool{stores: stores, wfs: wfs})
	Register(&workspacePatchTool{stores: stores, wfs: wfs})
}

// RegisterWorkspaceSearchTool registers the semantic search tool separately
// because it requires the embedder (which may not be configured).
func RegisterWorkspaceSearchTool(stores store.Stores, embedder *knowledge.Embedder) {
	Register(&workspaceSearchTool{stores: stores, embedder: embedder})
}

// ── Shared ─────────────────────────────────

// loadWorkspace resolves the workspace from the execution context.
func loadWorkspace(ctx context.Context, stores store.Stores, execCtx ExecutionContext) (*models.Workspace, error) {
	if execCtx.WorkspaceID == "" {
		return nil, fmt.Errorf("no workspace bound to this channel")
	}
	w, err := stores.Workspaces.GetByID(ctx, execCtx.WorkspaceID)
	if err != nil {
		return nil, fmt.Errorf("workspace not found: %w", err)
	}
	return w, nil
}

const (
	// maxReadBytes is the soft limit for workspace_read (50 KB).
	maxReadBytes = 50 * 1024
	// maxPatchFileBytes is the limit for files that can be patched (1 MB).
	maxPatchFileBytes = 1024 * 1024
)

// ═══════════════════════════════════════════
// workspace_ls
// ═══════════════════════════════════════════

type workspaceLsTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspaceLsTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_ls",
		DisplayName: "List Files",
		Description: "List files and directories in the workspace. Returns name, type, size, and content type for each entry. Use with path=\"\" or path=\".\" to list root.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"path":      Prop("string", "Directory path to list (empty or \".\" for root)"),
			"recursive": Prop("boolean", "If true, list all files recursively. Default false."),
		}, nil),
	}
}

func (t *workspaceLsTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Path      string `json:"path"`
		Recursive bool   `json:"recursive"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	entries, err := t.wfs.ListDir(ctx, w, args.Path, args.Recursive)
	if err != nil {
		return "", fmt.Errorf("list failed: %w", err)
	}

	type entry struct {
		Path        string `json:"path"`
		IsDir       bool   `json:"is_directory"`
		ContentType string `json:"content_type,omitempty"`
		SizeBytes   int64  `json:"size_bytes,omitempty"`
	}

	result := make([]entry, 0, len(entries))
	for _, e := range entries {
		result = append(result, entry{
			Path:        e.Path,
			IsDir:       e.IsDirectory,
			ContentType: e.ContentType,
			SizeBytes:   e.SizeBytes,
		})
	}

	b, _ := json.Marshal(map[string]interface{}{
		"path":    args.Path,
		"entries": result,
		"count":   len(result),
	})
	return string(b), nil
}

// ═══════════════════════════════════════════
// workspace_read
// ═══════════════════════════════════════════

type workspaceReadTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspaceReadTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_read",
		DisplayName: "Read File",
		Description: "Read the contents of a file in the workspace. Text files are returned as content; binary files return a summary. Large files are truncated at ~50KB with an indicator.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"path": Prop("string", "File path relative to workspace root"),
		}, []string{"path"}),
	}
}

func (t *workspaceReadTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Path string `json:"path"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}
	if args.Path == "" {
		return "", fmt.Errorf("path is required")
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	// Stat first for content type and directory check
	info, err := t.wfs.Stat(ctx, w, args.Path)
	if err != nil {
		return "", fmt.Errorf("file not found: %w", err)
	}
	if info.IsDirectory {
		return "", fmt.Errorf("cannot read a directory; use workspace_ls instead")
	}

	// Binary detection
	if !isTextContentType(info.ContentType) {
		b, _ := json.Marshal(map[string]interface{}{
			"path":         args.Path,
			"content_type": info.ContentType,
			"size_bytes":   info.SizeBytes,
			"binary":       true,
			"message":      fmt.Sprintf("Binary file (%s), %d bytes", info.ContentType, info.SizeBytes),
		})
		return string(b), nil
	}

	// Read content
	r, size, err := t.wfs.ReadFile(ctx, w, args.Path)
	if err != nil {
		return "", fmt.Errorf("read failed: %w", err)
	}
	defer r.Close()

	// Read with truncation guard
	buf := make([]byte, maxReadBytes+1)
	n, readErr := io.ReadFull(r, buf)
	if readErr != nil && readErr != io.ErrUnexpectedEOF && readErr != io.EOF {
		return "", fmt.Errorf("read error: %w", readErr)
	}

	truncated := n > maxReadBytes
	if truncated {
		n = maxReadBytes
	}

	resp := map[string]interface{}{
		"path":         args.Path,
		"content":      string(buf[:n]),
		"content_type": info.ContentType,
		"size_bytes":   size,
	}
	if truncated {
		resp["truncated"] = true
		resp["message"] = fmt.Sprintf("File truncated at %d bytes (total: %d bytes)", maxReadBytes, size)
	}

	b, _ := json.Marshal(resp)
	return string(b), nil
}

// isTextContentType returns true for text/* and known source code types.
func isTextContentType(ct string) bool {
	if strings.HasPrefix(ct, "text/") {
		return true
	}
	switch ct {
	case "application/json", "application/xml", "application/javascript",
		"application/x-yaml", "application/toml", "application/x-sh",
		"application/sql", "application/graphql":
		return true
	}
	return false
}

// ═══════════════════════════════════════════
// workspace_write
// ═══════════════════════════════════════════

type workspaceWriteTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspaceWriteTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_write",
		DisplayName: "Write File",
		Description: "Create or overwrite a file in the workspace. Parent directories are created automatically. Returns the path and size of the written file.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"path":    Prop("string", "File path relative to workspace root"),
			"content": Prop("string", "File content to write"),
		}, []string{"path", "content"}),
	}
}

func (t *workspaceWriteTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Path    string `json:"path"`
		Content string `json:"content"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}
	if args.Path == "" {
		return "", fmt.Errorf("path is required")
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	if err := t.wfs.WriteFile(ctx, w, args.Path, strings.NewReader(args.Content), int64(len(args.Content))); err != nil {
		return "", fmt.Errorf("write failed: %w", err)
	}

	size := len(args.Content)
	log.Printf("📝 workspace_write: %s (%d bytes)", args.Path, size)

	b, _ := json.Marshal(map[string]interface{}{
		"path":       args.Path,
		"size_bytes": size,
		"message":    fmt.Sprintf("Written %d bytes to %s", size, args.Path),
	})
	return string(b), nil
}

// ═══════════════════════════════════════════
// workspace_rm
// ═══════════════════════════════════════════

type workspaceRmTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspaceRmTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_rm",
		DisplayName: "Delete File",
		Description: "Delete a file or directory from the workspace. For non-empty directories, set recursive=true. This cannot be undone.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"path":      Prop("string", "File or directory path to delete"),
			"recursive": Prop("boolean", "Required for non-empty directories. Default false."),
		}, []string{"path"}),
	}
}

func (t *workspaceRmTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Path      string `json:"path"`
		Recursive bool   `json:"recursive"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}
	if args.Path == "" {
		return "", fmt.Errorf("path is required")
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	if err := t.wfs.DeleteFile(ctx, w, args.Path, args.Recursive); err != nil {
		return "", fmt.Errorf("delete failed: %w", err)
	}

	log.Printf("🗑️ workspace_rm: %s (recursive=%v)", args.Path, args.Recursive)

	b, _ := json.Marshal(map[string]interface{}{
		"path":    args.Path,
		"message": fmt.Sprintf("Deleted %s", args.Path),
	})
	return string(b), nil
}

// ═══════════════════════════════════════════
// workspace_mv
// ═══════════════════════════════════════════

type workspaceMvTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspaceMvTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_mv",
		DisplayName: "Move/Rename File",
		Description: "Move or rename a file or directory within the workspace. Destination parent directories are created automatically.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"source":      Prop("string", "Current file path"),
			"destination": Prop("string", "New file path"),
		}, []string{"source", "destination"}),
	}
}

func (t *workspaceMvTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Source      string `json:"source"`
		Destination string `json:"destination"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}
	if args.Source == "" || args.Destination == "" {
		return "", fmt.Errorf("source and destination are required")
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	if err := t.wfs.MoveFile(ctx, w, args.Source, args.Destination); err != nil {
		return "", fmt.Errorf("move failed: %w", err)
	}

	log.Printf("📦 workspace_mv: %s → %s", args.Source, args.Destination)

	b, _ := json.Marshal(map[string]interface{}{
		"source":      args.Source,
		"destination": args.Destination,
		"message":     fmt.Sprintf("Moved %s → %s", args.Source, args.Destination),
	})
	return string(b), nil
}

// ═══════════════════════════════════════════
// workspace_patch
// ═══════════════════════════════════════════

type workspacePatchTool struct {
	workspaceToolBase
	stores store.Stores
	wfs    *workspace.FS
}

func (t *workspacePatchTool) Definition() ToolDef {
	return ToolDef{
		Name:        "workspace_patch",
		DisplayName: "Patch File",
		Description: "Apply find-and-replace operations to a text file. Each operation's 'find' string must match exactly once in the file. Operations are applied sequentially. Use empty 'replace' to delete matched text.",
		Category:    "workspace",
		Parameters: JSONSchema(map[string]interface{}{
			"path": Prop("string", "File path to patch"),
			"operations": map[string]interface{}{
				"type":        "array",
				"description": "List of find/replace operations to apply sequentially",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"find":    Prop("string", "Exact string to find (must match exactly once)"),
						"replace": Prop("string", "String to replace with (empty to delete)"),
					},
					"required": []string{"find", "replace"},
				},
			},
		}, []string{"path", "operations"}),
	}
}

func (t *workspacePatchTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Path       string `json:"path"`
		Operations []struct {
			Find    string `json:"find"`
			Replace string `json:"replace"`
		} `json:"operations"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}
	if args.Path == "" {
		return "", fmt.Errorf("path is required")
	}
	if len(args.Operations) == 0 {
		return "", fmt.Errorf("at least one operation is required")
	}

	w, err := loadWorkspace(ctx, t.stores, execCtx)
	if err != nil {
		return "", err
	}

	// Read current content
	r, size, err := t.wfs.ReadFile(ctx, w, args.Path)
	if err != nil {
		return "", fmt.Errorf("read failed: %w", err)
	}
	defer r.Close()

	if size > maxPatchFileBytes {
		return "", fmt.Errorf("file too large for patching (%d bytes, max %d)", size, maxPatchFileBytes)
	}

	data, err := io.ReadAll(r)
	if err != nil {
		return "", fmt.Errorf("read error: %w", err)
	}
	content := string(data)

	// Apply operations sequentially
	applied := make([]string, 0, len(args.Operations))
	for i, op := range args.Operations {
		if op.Find == "" {
			return "", fmt.Errorf("operation %d: find string is empty", i+1)
		}

		count := strings.Count(content, op.Find)
		if count == 0 {
			return "", fmt.Errorf("operation %d: find string not found in file", i+1)
		}
		if count > 1 {
			return "", fmt.Errorf("operation %d: find string matches %d times (must match exactly once)", i+1, count)
		}

		content = strings.Replace(content, op.Find, op.Replace, 1)
		applied = append(applied, fmt.Sprintf("op %d: replaced %d chars → %d chars", i+1, len(op.Find), len(op.Replace)))
	}

	// Write back
	if err := t.wfs.WriteFile(ctx, w, args.Path, strings.NewReader(content), int64(len(content))); err != nil {
		return "", fmt.Errorf("write failed: %w", err)
	}

	log.Printf("🔧 workspace_patch: %s (%d ops, %d bytes)", args.Path, len(args.Operations), len(content))

	b, _ := json.Marshal(map[string]interface{}{
		"path":       args.Path,
		"operations": applied,
		"size_bytes": len(content),
		"message":    fmt.Sprintf("Applied %d patch(es) to %s", len(args.Operations), args.Path),
	})
	return string(b), nil
}
